Files
zyt/app/tests/test_repository_parity.py
T
2026-08-11 10:02:40 +08:00

435 lines
15 KiB
Python

"""No-network contracts for the audited five doctor workspaces."""
from __future__ import annotations
from datetime import date
from typing import Any
import pytest
from doctor_workstation.core.models import Appointment, Consultation, PageResult, Prescription
from doctor_workstation.services.mock_repository import DemoDoctorRepository
from doctor_workstation.services.repository import (
PRESCRIPTION_LIBRARY_PERMISSIONS,
PRESCRIPTION_PERMISSIONS,
RemoteDoctorRepository,
)
class RecordingClient:
"""Small API client double retaining exact endpoint and DTO calls."""
def __init__(self) -> None:
self.base_url = "https://example.test/adminapi/"
self.token = "token"
self.get_calls: list[tuple[str, dict[str, Any]]] = []
self.post_calls: list[tuple[str, dict[str, Any]]] = []
def set_token(self, token: str) -> None:
"""Set the current synthetic token."""
self.token = token
def clear_token(self) -> None:
"""Clear the current synthetic token."""
self.token = ""
def get(self, endpoint: str, params: dict[str, Any] | None = None) -> Any:
"""Record a GET and return a shape appropriate for its contract."""
self.get_calls.append((endpoint, dict(params or {})))
if endpoint == "auth.admin/mySelf":
return {
"user": {"id": 1, "name": "Doctor", "role_ids": [1]},
"permissions": ["tcm.diagnosis/lists"],
"menu": [
{
"name": "问诊列表",
"component": "tcm/diagnosis/index",
"future": {"badge": 3},
"children": [{"name": "只读", "perms": "tcm.diagnosis/readonlyDetail"}],
"unsafe": lambda: None,
7: "non-string key",
}
],
}
if endpoint in {
"tcm.diagnosis/detail",
"tcm.diagnosis/readonlyDetail",
"firstvisit.myPatient/orderDetail",
"tcm.prescriptionOrder/detail",
}:
return {"id": int((params or {}).get("id", 0)), "patient_name": "测试患者"}
if endpoint == "tcm.prescription/getByAppointment":
return {}
if endpoint == "doctor.appointment/availableSlots":
return {"slots": [{"time": "09:00", "available": True}]}
if endpoint == "tcm.diagnosis/watchCall":
return {
"sdkAppId": 1400123456,
"userId": "doctor_20",
"userSig": "short-lived",
"roomId": 9001,
}
if endpoint == "tcm.prescriptionOrder/paidPayOrders":
return {"lists": [{"id": 9}], "deposit_min_amount": 50}
return {"lists": [], "count": 0, "extend": {"scope": {"label": "server"}}}
def post(self, endpoint: str, payload: dict[str, Any] | None = None) -> Any:
"""Record a POST and return a stable synthetic mutation result."""
body = dict(payload or {})
self.post_calls.append((endpoint, body))
if endpoint in {"tcm.prescription/add", "tcm.prescriptionOrder/create"}:
return {"id": 88}
return {"ok": True}
def test_page_result_preserves_outer_and_nested_extend() -> None:
"""Server scope metadata must survive nested data pagination envelopes."""
page = PageResult.from_payload(
{
"extend": {"scope": {"label": "doctor"}},
"data": {
"lists": [{"id": 1}],
"count": 4,
"extend": {"schedule_mode": "roster"},
},
},
Appointment.from_dict,
)
assert page.total == 4
assert page.extend == {
"scope": {"label": "doctor"},
"schedule_mode": "roster",
}
def test_consultation_keeps_diagnosis_and_appointment_status_separate() -> None:
"""Video eligibility fields cannot be overwritten by diagnosis enablement."""
row = Consultation.from_dict(
{
"id": 3,
"status": 1,
"status_desc": "启用",
"has_appointment": 1,
"appointment_status": 4,
"appointment_status_text": "已过号",
"appointments": [{"id": 8, "status": 4}],
"DiagnosisViewRecord": [{"is_confirmed": 1}],
}
)
assert row.status == 1
assert row.appointment_status == 4
assert row.has_appointment
assert row.confirmed
assert row.appointments == [{"id": 8, "status": 4}]
def test_prescription_round_trips_appointment_and_case_record() -> None:
"""Appointment authority and immutable case snapshot survive model DTOs."""
prescription = Prescription.from_dict(
{
"id": 8,
"diagnosis_id": 5,
"appointment_id": 17,
"case_record": {
"diagnosis_id": 5,
"appointment_id": 17,
"clinical_diagnosis": "气阴两虚证",
},
}
)
assert prescription.appointment_id == 17
assert prescription.case_record["clinical_diagnosis"] == "气阴两虚证"
payload = prescription.to_api_dict()
assert payload["appointment_id"] == 17
assert payload["case_record"] == {
"diagnosis_id": 5,
"appointment_id": 17,
"clinical_diagnosis": "气阴两虚证",
}
def test_remote_reception_is_forcibly_scoped_to_today() -> None:
"""Status 1/4 queue reads always carry the audited same-day date range."""
client = RecordingClient()
repository = RemoteDoctorRepository(client) # type: ignore[arg-type]
repository.list_appointments(status=1, keyword="林", page_no=2, page_size=15)
endpoint, params = client.get_calls[-1]
assert endpoint == "doctor.appointment/lists"
assert params == {
"status": 1,
"patient_name": "林",
"start_date": date.today().isoformat(),
"end_date": date.today().isoformat(),
"page_no": 2,
"page_size": 15,
}
def test_remote_new_contracts_use_exact_admin_endpoints_and_dtos() -> None:
"""Prescription, patient and diagnosis methods remain thin endpoint adapters."""
client = RecordingClient()
repository = RemoteDoctorRepository(client) # type: ignore[arg-type]
repository.create_prescription(
patient_name="测试患者",
diagnosis_id=5,
herbs=[{"medicine_id": 1, "name": "黄芪", "dosage": 10}],
)
repository.patch_prescription_patient(8, patient_name="修正患者", phone="13800000000", gender=2)
repository.audit_prescription(8, action="reject", remark="剂量不符")
repository.create_prescription_order(
prescription_id=8,
diagnosis_id=5,
recipient_name="修正患者",
recipient_phone="13800000000",
)
repository.list_paid_prescription_orders(5, prescription_order_id=3)
repository.list_medicines(name="黄芪")
repository.patient_orders(page_no=1, page_size=15, fulfillment_status=2)
repository.patient_progress(page_no=1, page_size=15, status=1)
repository.patient_detail(5)
repository.appointment_history(5)
repository.assign_history(5)
repository.assign_patient(5, 20, is_inherit=1)
repository.fill_patient_id_card(5, "410000199001010000")
repository.book_patient_appointment({"diagnosis_id": 5, "appointment_date": "2026-08-10"})
repository.create_diagnosis_appointment(
{
"diagnosis_id": 5,
"patient_id": 5,
"doctor_id": 9,
"appointment_date": "2026-08-11",
}
)
repository.cancel_patient_appointment(7)
watch_ticket = repository.get_assistant_watch_ticket(5)
repository.create_diagnosis(
{
"patient_name": " 林晓岚 ",
"phone": "13800138000",
"gender": 0,
"age": 36,
"diagnosis_type": "复诊",
"local_hospital_name": "杭州市第一人民医院",
}
)
repository.update_diagnosis(5, {"clinical_diagnosis": "气虚证"})
repository.list_appointment_rosters(
doctor_id=1,
start_date="2026-08-10",
end_date="2026-08-16",
)
slots = repository.get_available_appointment_slots(
doctor_id=1,
appointment_date="2026-08-10",
)
assert (
"tcm.prescription/patchPatient",
{
"id": 8,
"patient_name": "修正患者",
"phone": "13800000000",
"gender": 2,
},
) in client.post_calls
assert (
"tcm.prescription/audit",
{
"id": 8,
"action": "reject",
"remark": "剂量不符",
},
) in client.post_calls
assert (
"firstvisit.myPatient/assign",
{
"id": 5,
"assistant_id": 20,
"is_inherit": 1,
},
) in client.post_calls
assert ("tcm.diagnosis/edit", {"id": 5, "clinical_diagnosis": "气虚证"}) in client.post_calls
assert (
"doctor.appointment/create",
{
"diagnosis_id": 5,
"patient_id": 5,
"doctor_id": 9,
"appointment_date": "2026-08-11",
},
) in client.post_calls
assert (
"tcm.diagnosis/add",
{
"patient_name": "林晓岚",
"phone": "13800138000",
"gender": 0,
"age": 36,
"diagnosis_type": "复诊",
"local_hospital_name": "杭州市第一人民医院",
},
) in client.post_calls
assert slots == {"slots": [{"time": "09:00", "available": True}]}
assert watch_ticket["roomId"] == 9001
get_endpoints = {endpoint for endpoint, _ in client.get_calls}
assert {
"tcm.prescriptionOrder/paidPayOrders",
"doctor.medicine/lists",
"firstvisit.myPatient/orders",
"firstvisit.myPatient/progress",
"tcm.diagnosis/readonlyDetail",
"doctor.appointment/lists",
"tcm.diagnosis/assignLogList",
"doctor.roster/lists",
"doctor.appointment/availableSlots",
"tcm.diagnosis/watchCall",
} <= get_endpoints
@pytest.mark.parametrize(
"unsafe_reference",
[r"C:\records\tongue.jpg", r"\\server\share\report.pdf", "file:///tmp/a.jpg"],
)
def test_remote_note_rejects_local_material_references(unsafe_reference: str) -> None:
"""No drive, UNC or file URI can reach addDoctorNote JSON."""
client = RecordingClient()
repository = RemoteDoctorRepository(client) # type: ignore[arg-type]
with pytest.raises(ValueError, match="server uri/url"):
repository.add_doctor_note(5, tongue_images=[unsafe_reference])
assert not any(
endpoint == "doctor.appointment/addDoctorNote" for endpoint, _payload in client.post_calls
)
@pytest.mark.parametrize(
("changes", "message"),
[
({"gender": 2}, "gender"),
({"local_hospital_name": ""}, "local_hospital_name"),
],
)
def test_remote_diagnosis_create_rejects_invalid_production_dto_before_transport(
changes: dict[str, Any], message: str
) -> None:
client = RecordingClient()
repository = RemoteDoctorRepository(client) # type: ignore[arg-type]
payload = {
"patient_name": "林晓岚",
"phone": "13800138000",
"gender": 0,
"age": 36,
"diagnosis_type": "复诊",
"local_hospital_name": "杭州市第一人民医院",
**changes,
}
with pytest.raises(ValueError, match=message):
repository.create_diagnosis(payload)
assert not client.post_calls
def test_remote_dynamic_menu_preserves_json_metadata_but_drops_runtime_objects() -> None:
"""Future menu fields pass through safely without evaluating arbitrary values."""
repository = RemoteDoctorRepository(RecordingClient()) # type: ignore[arg-type]
menu = repository.get_session().menu
assert menu[0]["component"] == "tcm/diagnosis/index"
assert menu[0]["future"] == {"badge": 3}
assert menu[0]["children"][0]["perms"] == "tcm.diagnosis/readonlyDetail"
assert "unsafe" not in menu[0]
assert 7 not in menu[0]
def test_canonical_prescription_permissions_match_routed_views() -> None:
"""Service exports one canonical spelling for each routed action."""
assert PRESCRIPTION_LIBRARY_PERMISSIONS == {
"create": "wcf.prescription/add",
"read": "wcf.prescription/read",
"update": "wcf.prescription/edit",
"delete": "wcf.prescription/delete",
}
assert PRESCRIPTION_PERMISSIONS["delete"] == "cf.prescription/del"
assert PRESCRIPTION_PERMISSIONS["patch_patient"] == "tcm.prescription/patchPatient"
def test_demo_mutates_prescriptions_orders_and_patient_workspaces() -> None:
"""Offline mode supports the full workflow rather than static placeholders."""
repository = DemoDoctorRepository(today=date(2026, 8, 10))
created = repository.create_prescription(
diagnosis_id=501,
patient_name="林晓岚",
phone="13800131203",
gender=2,
herbs=[{"medicine_id": 17, "name": "黄芪", "dosage": 20}],
doctor_name="陈医生(演示)",
doctor_signature="data:image/png;base64,demo",
)
updated = repository.update_prescription(created.id, {"clinical_diagnosis": "气虚证"})
repository.patch_prescription_patient(
created.id, patient_name="林晓岚(修正)", phone="13800131203", gender=2
)
audit = repository.audit_prescription(created.id, action="approve")
order = repository.create_prescription_order(
prescription_id=created.id,
diagnosis_id=501,
recipient_name="林晓岚(修正)",
recipient_phone="13800131203",
amount=268,
)
assert updated.clinical_diagnosis == "气虚证"
assert audit["audit_status"] == 1
assert repository.get_prescription(created.id).has_prescription_order
assert repository.get_prescription_order(order["id"])["prescription_id"] == created.id
assert repository.patient_orders().extend["summary"]["order_count"] == 2
assigned = repository.assign_patient(501, 2002)
repository.fill_patient_id_card(501, "410000199001010000")
appointment = repository.book_patient_appointment(
diagnosis_id=501,
appointment_date="2026-08-11",
appointment_time="15:00-15:30",
)
repository.update_diagnosis(501, {"chief_complaint": "乏力"})
assert assigned["assistant_name"] == "许医助"
assert repository.assign_history(501).total == 2
assert repository.patient_detail(501)["diagnosis"]["chief_complaint"] == "乏力"
assert repository.appointment_history(501).total == 2
repository.cancel_patient_appointment(appointment["id"])
assert repository.appointment_history(501).items[-1].status == 2
assert repository.list_medicines(name="黄芪").items[0]["name"] == "黄芪"
def test_reject_actions_require_a_remark() -> None:
"""Audit rejection mirrors the admin dialog's mandatory reason boundary."""
repository = DemoDoctorRepository(today=date(2026, 8, 10))
with pytest.raises(ValueError, match="remark"):
repository.audit_prescription(802, action="reject")
with pytest.raises(ValueError, match="remark"):
repository.audit_patient_order_payment(901, "reject")