vignette/apps/api/app/test_session_share.py
2026-06-28 23:52:18 +09:00

158 lines
5.4 KiB
Python

"""Session review public share regression tests."""
from __future__ import annotations
import unittest
from starlette.requests import Request
from . import session_persistence
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)