262 lines
10 KiB
Python
262 lines
10 KiB
Python
"""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)
|