"""Current-user support ticket visibility tests.""" from __future__ import annotations import unittest from datetime import datetime, timedelta, timezone from unittest.mock import AsyncMock, patch from .deps import Principal, Role from .routes import users as user_routes from .services.support_tickets import support_ticket_fingerprint class _Acquire: def __init__(self, conn): self.conn = conn async def __aenter__(self): return self.conn async def __aexit__(self, exc_type, exc, tb): return None class UserSupportTicketTest(unittest.IsolatedAsyncioTestCase): async def test_create_support_ticket_stores_fingerprint_without_audit_body(self) -> None: ticket_id = "00000000-0000-0000-0000-000000000101" audit_calls: list[tuple[str, tuple[object, ...]]] = [] class Conn: async def fetchrow(self, query, *args, **kwargs): self.query = query self.args = args return { "id": ticket_id, "category": args[4], "priority": args[5], "subject": args[6], "created_at": 1234.0, } async def execute(self, query, *args, **kwargs): audit_calls.append((query, args)) principal = Principal( user_id="00000000-0000-0000-0000-000000000201", role=Role.LEARNER, email="learner@hs.ac.kr", display_name="Learner", ) body = user_routes.UserSupportTicketRequest( category="voice_browser", priority="high", subject="마이크 권한 오류", body="브라우저에서 마이크 권한을 눌렀는데 다시 실패합니다.", source_path="/learn/session/voice", ) expected_fingerprint = support_ticket_fingerprint( category=body.category, subject=body.subject, body=body.body, source_path=body.source_path, ) profile = user_routes.UserProfileResponse( user_id=principal.user_id, email=principal.email, display_name="Learner", role="learner", cohort_ids=[], affiliation="한신대학교", ) conn = Conn() with ( patch.object(user_routes, "acquire", return_value=_Acquire(conn)), patch.object(user_routes, "_profile_for", AsyncMock(return_value=profile)), ): created = await user_routes.create_support_ticket(body, principal) self.assertEqual(created.ticket_id, ticket_id) self.assertIn("fingerprint", conn.query) self.assertEqual(conn.args[9], expected_fingerprint) self.assertEqual(len(audit_calls), 1) detail = audit_calls[0][1][4] self.assertEqual(detail["fingerprint"], expected_fingerprint) self.assertNotIn("body", detail) self.assertNotIn("subject", detail) async def test_user_ticket_list_scopes_to_current_reporter_without_body(self) -> None: now = datetime.now(timezone.utc) class Conn: async def fetch(self, query, *args, **kwargs): self.query = query self.args = args return [ { "id": "00000000-0000-0000-0000-000000000101", "category": "session_review", "priority": "high", "status": "resolved", "subject": "리뷰 생성 지연", "source_path": "/learn/session/1/review", "assigned_group": "서비스 운영자", "resolution_note": "재처리 완료", "created_at": (now - timedelta(days=1)).timestamp(), "updated_at": now.timestamp(), "resolved_at": now.timestamp(), } ] conn = Conn() principal = Principal( user_id="00000000-0000-0000-0000-000000000201", role=Role.LEARNER, email="learner@hs.ac.kr", display_name="Learner", ) with patch.object(user_routes, "acquire", return_value=_Acquire(conn)): tickets = await user_routes._support_tickets_for_user(principal) self.assertIn("WHERE reporter_id = $1::uuid", conn.query) self.assertNotIn(" body", conn.query.lower()) self.assertEqual(conn.args, (principal.user_id, 20)) self.assertEqual(tickets.source, "database") self.assertTrue(tickets.durable) self.assertEqual(len(tickets.tickets), 1) ticket = tickets.tickets[0] self.assertEqual(ticket.status, "resolved") self.assertEqual(ticket.resolution_note, "재처리 완료") self.assertFalse(hasattr(ticket, "body")) class UserPrepostMeasureTest(unittest.IsolatedAsyncioTestCase): async def test_upsert_prepost_measure_scopes_to_current_user_without_score_in_audit(self) -> None: measure_id = "00000000-0000-0000-0000-000000000301" audit_calls: list[tuple[str, tuple[object, ...]]] = [] class Conn: async def fetchrow(self, query, *args, **kwargs): self.query = query self.args = args return { "id": measure_id, "pilot_id": args[1], "measure_name": args[2], "timepoint": args[3], "raw_score": args[4], "min_score": args[5], "max_score": args[6], "instrument_version": args[7], "item_count": args[8], "collected_at": 1234.0, "updated_at": 1235.0, } async def execute(self, query, *args, **kwargs): audit_calls.append((query, args)) principal = Principal( user_id="00000000-0000-0000-0000-000000000201", role=Role.LEARNER, email="learner@hs.ac.kr", display_name="Learner", ) body = user_routes.UserPrepostMeasureRequest( pilot_id="phase3-pilot-local", measure_name="self_efficacy", timepoint="pre", raw_score=4.0, min_score=1.0, max_score=5.0, instrument_version="draft-scale-v1", item_count=6, ) conn = Conn() with patch.object(user_routes, "acquire", return_value=_Acquire(conn)): saved = await user_routes.upsert_my_prepost_measure(body, principal) self.assertIn("learner_prepost_measure", conn.query) self.assertEqual(conn.args[0], principal.user_id) self.assertEqual(conn.args[1], "phase3-pilot-local") self.assertEqual(conn.args[2], "self_efficacy") self.assertEqual(conn.args[3], "pre") self.assertEqual(saved.normalized_score, 75.0) self.assertEqual(len(audit_calls), 1) detail = audit_calls[0][1][4] self.assertEqual(detail["measure_name"], "self_efficacy") self.assertEqual(detail["timepoint"], "pre") self.assertTrue(detail["score_recorded"]) self.assertNotIn("raw_score", detail) self.assertNotIn("score", detail) async def test_prepost_measure_list_scopes_to_current_user_and_counts_pairs(self) -> None: class Conn: async def fetch(self, query, *args, **kwargs): self.query = query self.args = args return [ { "id": "00000000-0000-0000-0000-000000000301", "pilot_id": "phase3-pilot-local", "measure_name": "self_efficacy", "timepoint": "pre", "raw_score": 3.0, "min_score": 1.0, "max_score": 5.0, "instrument_version": "draft-scale-v1", "item_count": 6, "collected_at": 1234.0, "updated_at": 1234.0, }, { "id": "00000000-0000-0000-0000-000000000302", "pilot_id": "phase3-pilot-local", "measure_name": "self_efficacy", "timepoint": "post", "raw_score": 4.0, "min_score": 1.0, "max_score": 5.0, "instrument_version": "draft-scale-v1", "item_count": 6, "collected_at": 2234.0, "updated_at": 2234.0, }, { "id": "00000000-0000-0000-0000-000000000303", "pilot_id": "phase3-pilot-local", "measure_name": "skill_proficiency", "timepoint": "pre", "raw_score": 2.0, "min_score": 1.0, "max_score": 5.0, "instrument_version": "draft-scale-v1", "item_count": 6, "collected_at": 3234.0, "updated_at": 3234.0, }, ] principal = Principal( user_id="00000000-0000-0000-0000-000000000201", role=Role.LEARNER, email="learner@hs.ac.kr", display_name="Learner", ) conn = Conn() with patch.object(user_routes, "acquire", return_value=_Acquire(conn)): response = await user_routes._prepost_measures_for_user( principal, pilot_id="phase3-pilot-local", ) self.assertIn("WHERE learner_id = $1::uuid", conn.query) self.assertEqual(conn.args, (principal.user_id, "phase3-pilot-local")) self.assertEqual(response.source, "database") self.assertTrue(response.durable) self.assertEqual(response.complete_measure_pairs, 1) self.assertEqual(len(response.measures), 3) self.assertEqual(response.measures[0].normalized_score, 50.0)