2026-05-29 15:58:00 +08:00
|
|
|
|
"""病例域 2 条 happy-path 流程测试。"""
|
|
|
|
|
|
|
|
|
|
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
|
|
|
|
|
|
|
|
from django.core.cache import cache
|
|
|
|
|
|
|
2026-06-03 17:34:47 +08:00
|
|
|
|
from apps.case.models import CaseBase, TraditionalCase, ScoringRule, CaseExamItem
|
2026-05-29 15:58:00 +08:00
|
|
|
|
from apps.user.throttling import PdfParseUserThrottle, ScoringRuleGenerateUserThrottle
|
|
|
|
|
|
from .conftest import (
|
|
|
|
|
|
CacheTestCase,
|
|
|
|
|
|
CASE_PARSE_URL, CASE_GENERATE_RULES_URL, CASE_FULL_CREATE_URL,
|
|
|
|
|
|
case_full_url, create_test_user, get_auth_client, ensure_department,
|
|
|
|
|
|
build_traditional_payload, make_fake_pdf,
|
|
|
|
|
|
MOCK_C1_PARSE_RESULT, MOCK_C2_SCORING_RULES,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CaseFormHappyPathTest(CacheTestCase):
|
|
|
|
|
|
"""HP-5: 表单录入 → full-create → GET → PATCH → GET 验证"""
|
|
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
super().setUp()
|
|
|
|
|
|
self.user = create_test_user(phone='13900100001', password='CaseTest1')
|
|
|
|
|
|
self.client = get_auth_client(self.user)
|
|
|
|
|
|
ensure_department('儿科')
|
|
|
|
|
|
|
|
|
|
|
|
def test_flow_form_create_read_update(self):
|
|
|
|
|
|
"""HP-5: C3 full-create → C4 GET full → C5 PATCH → C4 GET verify"""
|
|
|
|
|
|
# C3: full-create(2 条评分规则)
|
2026-06-03 17:34:47 +08:00
|
|
|
|
payload = build_traditional_payload(
|
|
|
|
|
|
department_name='儿科', scoring_rules_count=2, with_exam_items=True,
|
|
|
|
|
|
)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
resp = self.client.post(CASE_FULL_CREATE_URL, payload, format='json')
|
|
|
|
|
|
self.assertEqual(resp.status_code, 201, resp.content)
|
|
|
|
|
|
|
|
|
|
|
|
created = resp.json()
|
|
|
|
|
|
case_id = created['case']['id']
|
|
|
|
|
|
self.assertEqual(created['case']['case_type'], 'traditional')
|
|
|
|
|
|
self.assertEqual(created['case']['publish_status'], 0) # 草稿
|
|
|
|
|
|
self.assertIn('traditional', created)
|
|
|
|
|
|
self.assertIsNotNone(created['traditional'])
|
|
|
|
|
|
self.assertEqual(len(created['scoring_rules']), 2)
|
2026-06-03 17:34:47 +08:00
|
|
|
|
self.assertEqual(len(created['exam_items']), 1)
|
|
|
|
|
|
self.assertEqual(created['exam_items'][0]['item_code'], 'blood_routine')
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
# C4: GET full
|
|
|
|
|
|
resp = self.client.get(case_full_url(case_id))
|
|
|
|
|
|
self.assertEqual(resp.status_code, 200, resp.content)
|
|
|
|
|
|
full = resp.json()
|
|
|
|
|
|
self.assertEqual(full['case']['title'], payload['title'])
|
|
|
|
|
|
self.assertEqual(len(full['scoring_rules']), 2)
|
2026-06-03 17:34:47 +08:00
|
|
|
|
self.assertEqual(len(full['exam_items']), 1)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
# C5: PATCH(改标题 + 改子表 + 替换为 1 条评分规则)
|
|
|
|
|
|
patch_data = {
|
|
|
|
|
|
'title': '更新后的标题',
|
|
|
|
|
|
'traditional': {
|
|
|
|
|
|
'standard_diagnosis': '更新后的诊断',
|
|
|
|
|
|
},
|
|
|
|
|
|
'scoring_rules': [
|
|
|
|
|
|
{
|
|
|
|
|
|
'dimension': '更新后的维度',
|
|
|
|
|
|
'score_weight': 1.0,
|
|
|
|
|
|
'ai_auto_score': False,
|
|
|
|
|
|
'scoring_standard': '更新后的标准',
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
resp = self.client.patch(case_full_url(case_id), patch_data, format='json')
|
|
|
|
|
|
self.assertEqual(resp.status_code, 200, resp.content)
|
|
|
|
|
|
|
|
|
|
|
|
# C4: GET 验证更新
|
|
|
|
|
|
resp = self.client.get(case_full_url(case_id))
|
|
|
|
|
|
self.assertEqual(resp.status_code, 200, resp.content)
|
|
|
|
|
|
full = resp.json()
|
|
|
|
|
|
self.assertEqual(full['case']['title'], '更新后的标题')
|
|
|
|
|
|
self.assertEqual(full['traditional']['standard_diagnosis'], '更新后的诊断')
|
|
|
|
|
|
self.assertEqual(len(full['scoring_rules']), 1)
|
|
|
|
|
|
self.assertEqual(full['scoring_rules'][0]['dimension'], '更新后的维度')
|
|
|
|
|
|
|
|
|
|
|
|
# 验证 DB
|
|
|
|
|
|
case = CaseBase.objects.get(id=case_id)
|
|
|
|
|
|
self.assertEqual(case.title, '更新后的标题')
|
|
|
|
|
|
self.assertEqual(ScoringRule.objects.filter(case_id=case_id).count(), 1)
|
2026-06-03 17:34:47 +08:00
|
|
|
|
self.assertEqual(CaseExamItem.objects.filter(case_id=case_id).count(), 1)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
tc = TraditionalCase.objects.get(case_id=case_id)
|
|
|
|
|
|
self.assertEqual(tc.standard_diagnosis, '更新后的诊断')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CasePdfMockHappyPathTest(CacheTestCase):
|
|
|
|
|
|
"""HP-6: PDF 解析(mock AI)→ 生成评分规则 → full-create → GET → PATCH"""
|
|
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
super().setUp()
|
|
|
|
|
|
self.user = create_test_user(phone='13900100002', password='CaseTest2')
|
|
|
|
|
|
self.client = get_auth_client(self.user)
|
|
|
|
|
|
ensure_department('儿科')
|
|
|
|
|
|
|
|
|
|
|
|
@patch('apps.case.services.case_importer.extract_text_from_pdfs',
|
|
|
|
|
|
return_value='患儿,男,4岁,因发热3天就诊。体温38.5°C...')
|
|
|
|
|
|
def test_flow_pdf_mock_full_pipeline(self, mock_pdf):
|
|
|
|
|
|
"""HP-6: C1 parse-pdf → C2 generate-scoring-rules → C3 full-create → C4 GET → C5 PATCH"""
|
|
|
|
|
|
|
|
|
|
|
|
# mock call_deepseek: 第 1 次返回 C1 解析结果,第 2 次返回 C2 评分规则
|
|
|
|
|
|
call_count = {'n': 0}
|
|
|
|
|
|
def mock_deepseek(system_prompt, user_content):
|
|
|
|
|
|
call_count['n'] += 1
|
|
|
|
|
|
if call_count['n'] == 1:
|
|
|
|
|
|
return MOCK_C1_PARSE_RESULT
|
|
|
|
|
|
return MOCK_C2_SCORING_RULES
|
|
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
|
patch('apps.case.services.deepseek_client.call_deepseek', side_effect=mock_deepseek),
|
|
|
|
|
|
patch.object(PdfParseUserThrottle, 'allow_request', return_value=True),
|
|
|
|
|
|
patch.object(ScoringRuleGenerateUserThrottle, 'allow_request', return_value=True),
|
|
|
|
|
|
):
|
|
|
|
|
|
# C1: parse-pdf
|
|
|
|
|
|
fake_pdf = make_fake_pdf()
|
|
|
|
|
|
resp = self.client.post(
|
|
|
|
|
|
CASE_PARSE_URL,
|
|
|
|
|
|
{'files': fake_pdf, 'case_type': 'traditional'},
|
|
|
|
|
|
format='multipart',
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(resp.status_code, 200, resp.content)
|
|
|
|
|
|
parse_result = resp.json()
|
|
|
|
|
|
self.assertIn('parse_id', parse_result)
|
|
|
|
|
|
self.assertEqual(parse_result['case_type'], 'traditional')
|
|
|
|
|
|
data = parse_result['data']
|
|
|
|
|
|
self.assertEqual(data['case_type'], 'traditional')
|
|
|
|
|
|
self.assertIn('traditional', data)
|
|
|
|
|
|
self.assertNotIn('scoring_rules', data)
|
|
|
|
|
|
|
|
|
|
|
|
# C2: generate-scoring-rules
|
|
|
|
|
|
resp = self.client.post(
|
|
|
|
|
|
CASE_GENERATE_RULES_URL,
|
|
|
|
|
|
data,
|
|
|
|
|
|
format='json',
|
|
|
|
|
|
)
|
|
|
|
|
|
self.assertEqual(resp.status_code, 200, resp.content)
|
|
|
|
|
|
gen_result = resp.json()
|
|
|
|
|
|
self.assertGreaterEqual(gen_result['generated'], 1)
|
|
|
|
|
|
scoring_rules = gen_result['scoring_rules']
|
|
|
|
|
|
|
|
|
|
|
|
# C3: full-create(组装 C1 data + C2 scoring_rules)
|
|
|
|
|
|
create_payload = {**data}
|
|
|
|
|
|
create_payload['scoring_rules'] = scoring_rules
|
|
|
|
|
|
create_payload['parse_id'] = parse_result['parse_id']
|
|
|
|
|
|
resp = self.client.post(CASE_FULL_CREATE_URL, create_payload, format='json')
|
|
|
|
|
|
self.assertEqual(resp.status_code, 201, resp.content)
|
|
|
|
|
|
created = resp.json()
|
|
|
|
|
|
case_id = created['case']['id']
|
|
|
|
|
|
self.assertEqual(len(created['scoring_rules']), len(scoring_rules))
|
2026-06-03 17:34:47 +08:00
|
|
|
|
self.assertEqual(len(created['exam_items']), 1)
|
2026-05-29 15:58:00 +08:00
|
|
|
|
|
|
|
|
|
|
# C4: GET full
|
|
|
|
|
|
resp = self.client.get(case_full_url(case_id))
|
|
|
|
|
|
self.assertEqual(resp.status_code, 200, resp.content)
|
|
|
|
|
|
full = resp.json()
|
|
|
|
|
|
self.assertEqual(full['case']['id'], case_id)
|
|
|
|
|
|
self.assertEqual(full['case']['case_type'], 'traditional')
|
|
|
|
|
|
|
|
|
|
|
|
# C5: PATCH
|
|
|
|
|
|
resp = self.client.patch(case_full_url(case_id), {
|
|
|
|
|
|
'title': 'AI-更新标题',
|
|
|
|
|
|
}, format='json')
|
|
|
|
|
|
self.assertEqual(resp.status_code, 200, resp.content)
|
|
|
|
|
|
|
|
|
|
|
|
# 验证 PATCH 生效
|
|
|
|
|
|
resp = self.client.get(case_full_url(case_id))
|
|
|
|
|
|
self.assertEqual(resp.status_code, 200, resp.content)
|
|
|
|
|
|
self.assertEqual(resp.json()['case']['title'], 'AI-更新标题')
|