"""病例域 2 条 happy-path 流程测试。""" from unittest.mock import patch, MagicMock from django.core.cache import cache from apps.case.models import CaseBase, TraditionalCase, ScoringRule, CaseExamItem 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 条评分规则) payload = build_traditional_payload( department_name='儿科', scoring_rules_count=2, with_exam_items=True, ) 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) self.assertEqual(len(created['exam_items']), 1) self.assertEqual(created['exam_items'][0]['item_code'], 'blood_routine') # 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) self.assertEqual(len(full['exam_items']), 1) # 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) self.assertEqual(CaseExamItem.objects.filter(case_id=case_id).count(), 1) 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)) self.assertEqual(len(created['exam_items']), 1) # 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-更新标题')