183 lines
6.3 KiB
Python
183 lines
6.3 KiB
Python
"""Session review public share regression tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from starlette.requests import Request
|
|
|
|
from . import session_persistence
|
|
from . import auth_sessions
|
|
from .deps import Principal, Role
|
|
from .routes import sessions
|
|
from .routes import share as share_routes
|
|
from .services import persona as persona_service, state_machine
|
|
from .store import InProcSession, TurnRecord, store
|
|
|
|
|
|
def _principal() -> Principal:
|
|
return Principal(
|
|
user_id="00000000-0000-0000-0000-000000000201",
|
|
role=Role.LEARNER,
|
|
cohort_ids=[],
|
|
email="share-test@hs.ac.kr",
|
|
display_name="Share Test",
|
|
consent_at=1.0,
|
|
)
|
|
|
|
|
|
def _request(path: str = "/sessions/share-test-session/share") -> Request:
|
|
return Request(
|
|
{
|
|
"type": "http",
|
|
"method": "POST",
|
|
"path": path,
|
|
"scheme": "https",
|
|
"server": ("api.test", 443),
|
|
"headers": [(b"host", b"api.test")],
|
|
}
|
|
)
|
|
|
|
|
|
def _ended_session(principal: Principal) -> InProcSession:
|
|
card = persona_service.P1
|
|
sess = InProcSession(
|
|
session_id="share-test-session",
|
|
case_id="share-test-case",
|
|
learner_id=principal.user_id,
|
|
persona_code=card.code,
|
|
theory_mode="humanistic",
|
|
persona=card,
|
|
state=state_machine.SessionState(
|
|
resistance=card.base_resistance(),
|
|
ideation_stage=card.ideation_baseline(),
|
|
),
|
|
ended=True,
|
|
ended_at=1_800_000_100,
|
|
)
|
|
sess.turns.extend(
|
|
[
|
|
TurnRecord(
|
|
turn_seq=1,
|
|
speaker="counselor",
|
|
stage="라포",
|
|
text="원문 학습자 민감 발화",
|
|
text_masked="원문 학습자 민감 발화",
|
|
created_at=1_800_000_000,
|
|
),
|
|
TurnRecord(
|
|
turn_seq=2,
|
|
speaker="client",
|
|
stage="라포",
|
|
text="내담자 응답",
|
|
text_masked="내담자 응답",
|
|
created_at=1_800_000_030,
|
|
),
|
|
]
|
|
)
|
|
store.put(sess)
|
|
return sess
|
|
|
|
|
|
class SessionShareTest(unittest.IsolatedAsyncioTestCase):
|
|
async def asyncSetUp(self) -> None:
|
|
store._sessions.clear()
|
|
session_persistence._SESSION_SHARE_CACHE.clear()
|
|
session_persistence._SESSION_SHARE_TOKEN_INDEX.clear()
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
store._sessions.clear()
|
|
session_persistence._SESSION_SHARE_CACHE.clear()
|
|
session_persistence._SESSION_SHARE_TOKEN_INDEX.clear()
|
|
|
|
async def test_create_share_returns_public_summary_without_transcript(self) -> None:
|
|
principal = _principal()
|
|
_ended_session(principal)
|
|
|
|
response = await sessions.create_session_share(
|
|
"share-test-session",
|
|
_request(),
|
|
principal,
|
|
)
|
|
self.assertTrue(response.shareUrl.startswith("https://api.test/share/session/"))
|
|
token = response.shareUrl.rsplit("/", 1)[-1]
|
|
|
|
summary = await share_routes._load_share_or_404(token)
|
|
self.assertIn("Vignette 회기 리뷰", summary.title)
|
|
self.assertIn(summary.reachedPhase, {"라포", "탐색", "개입", "정리"})
|
|
self.assertNotIn("원문 학습자 민감 발화", summary.model_dump_json())
|
|
self.assertIn("저장된 실제 축어록 2개", summary.summary)
|
|
|
|
async def test_public_share_normalizes_legacy_reached_phase(self) -> None:
|
|
coded = share_routes._safe_payload({"reachedPhase": "rapport"})
|
|
blank = share_routes._safe_payload({"reachedPhase": ""})
|
|
invalid = share_routes._safe_payload({"reachedPhase": "unknown-stage"})
|
|
missing = share_routes._safe_payload({})
|
|
|
|
self.assertEqual(coded.reachedPhase, "라포")
|
|
self.assertIsNone(blank.reachedPhase)
|
|
self.assertIsNone(invalid.reachedPhase)
|
|
self.assertIsNone(missing.reachedPhase)
|
|
|
|
async def test_public_share_lookup_tolerates_legacy_blank_reached_phase(self) -> None:
|
|
token = "legacyShareTokenValue000000000000"
|
|
token_hash = session_persistence.share_token_hash(token)
|
|
session_persistence._SESSION_SHARE_CACHE["legacy-share-session"] = {
|
|
"session_id": "legacy-share-session",
|
|
"token_hash": token_hash,
|
|
"payload": {
|
|
"title": "Legacy share",
|
|
"description": "Legacy summary",
|
|
"reachedPhase": "",
|
|
},
|
|
"created_at": 1_800_000_000.0,
|
|
"updated_at": 1_800_000_000.0,
|
|
"revoked_at": None,
|
|
}
|
|
session_persistence._SESSION_SHARE_TOKEN_INDEX[token_hash] = "legacy-share-session"
|
|
|
|
summary = await share_routes._load_share_or_404(token)
|
|
|
|
self.assertEqual(summary.title, "Legacy share")
|
|
self.assertIsNone(summary.reachedPhase)
|
|
|
|
async def test_revoke_share_blocks_public_lookup(self) -> None:
|
|
principal = _principal()
|
|
_ended_session(principal)
|
|
|
|
response = await sessions.create_session_share(
|
|
"share-test-session",
|
|
_request(),
|
|
principal,
|
|
)
|
|
token = response.shareUrl.rsplit("/", 1)[-1]
|
|
revoked = await sessions.revoke_session_share("share-test-session", principal)
|
|
self.assertTrue(revoked.revoked)
|
|
|
|
with self.assertRaises(share_routes.HTTPException) as caught:
|
|
await share_routes._load_share_or_404(token)
|
|
self.assertEqual(caught.exception.status_code, 404)
|
|
|
|
async def test_current_feedback_off_blocks_an_existing_public_share(self) -> None:
|
|
principal = _principal()
|
|
_ended_session(principal)
|
|
response = await sessions.create_session_share(
|
|
"share-test-session",
|
|
_request(),
|
|
principal,
|
|
)
|
|
token = response.shareUrl.rsplit("/", 1)[-1]
|
|
|
|
with patch.object(
|
|
auth_sessions,
|
|
"get_managed_user",
|
|
AsyncMock(
|
|
return_value=SimpleNamespace(learner_feedback_enabled=False)
|
|
),
|
|
):
|
|
with self.assertRaises(share_routes.HTTPException) as caught:
|
|
await share_routes._load_share_or_404(token)
|
|
|
|
self.assertEqual(caught.exception.status_code, 404)
|