Files

299 lines
11 KiB
Python
Raw Permalink Normal View History

2026-05-29 15:58:00 +08:00
"""D8 测试共享工具:URL 常量、用户/科室夹具、SMS 注入、载荷构建。"""
from django.core.cache import cache
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, TransactionTestCase
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
from apps.user.models import User, Institution, Department, TeacherStudentRelation
class CacheTestCase(TestCase):
"""所有测试基类:setUp 自动 clear Redis,隔离 SMS 验证码/限流/JWT 黑名单。"""
def setUp(self):
super().setUp()
cache.clear()
class CacheTransactionTestCase(TransactionTestCase):
"""事务测试基类:setUp 自动 clear Redis。"""
def setUp(self):
super().setUp()
cache.clear()
# ─── URL 常量 ─────────────────────────────────────────────────────────────────
# 用户认证
USER_SEND_CODE_URL = '/api/user/auth/send-code/'
USER_REGISTER_URL = '/api/user/auth/register/'
USER_LOGIN_URL = '/api/user/auth/login/'
USER_LOGIN_CODE_URL = '/api/user/auth/login-code/'
USER_LOGOUT_URL = '/api/user/auth/logout/'
USER_REFRESH_URL = '/api/user/auth/refresh/'
USER_RESET_PWD_URL = '/api/user/auth/reset-password/'
USER_CHANGE_PWD_URL = '/api/user/users/change-password/'
USER_ME_URL = '/api/user/users/me/'
USER_LIST_URL = '/api/user/users/'
2026-06-05 15:36:31 +08:00
USER_INSTITUTION_LIST_URL = '/api/user/institution_list/'
2026-06-08 17:36:03 +08:00
USER_INSTITUTION_INFO_URL = '/api/user/institution_info/'
USER_MY_DEPARTMENTS_URL = '/api/user/my_departments/'
USER_PROFILE_CONFIG_URL = '/api/user/profile/config/'
2026-05-29 15:58:00 +08:00
# 病例
CASE_PARSE_URL = '/api/case/cases/parse-pdf/'
CASE_GENERATE_RULES_URL = '/api/case/cases/generate-scoring-rules/'
CASE_FULL_CREATE_URL = '/api/case/cases/full-create/'
def user_detail_url(user_id):
return f'/api/user/users/{user_id}/'
def case_full_url(case_id):
return f'/api/case/cases/{case_id}/full/'
# ─── SMS 注入 ─────────────────────────────────────────────────────────────────
def inject_sms_code(phone, scene, code='123456'):
"""直接将验证码写入 Redis,绕过 SMS 服务。"""
cache.set(f'sms:{scene}:{phone}', code, timeout=300)
# ─── 默认测试机构 ─────────────────────────────────────────────────────────────
DEFAULT_INSTITUTION_CODE = 'TEST-HOSP-001'
DEFAULT_INSTITUTION_NAME = '测试医院'
2026-05-29 15:58:00 +08:00
# ─── 用户工具 ─────────────────────────────────────────────────────────────────
def create_test_user(phone='13900000001', password='TestPass1',
2026-06-05 15:36:31 +08:00
real_name='测试用户', role_type='student', status=1,
institution=None):
2026-05-29 15:58:00 +08:00
"""创建测试用户(已知密码),返回 User 实例。"""
user = User.objects.create_user(
username=phone,
password=password,
phone=phone,
real_name=real_name,
role_type=role_type,
status=status,
2026-06-05 15:36:31 +08:00
institution=institution,
2026-05-29 15:58:00 +08:00
)
return user
def get_tokens(user):
"""返回 {'access': '...', 'refresh': '...'} 字符串。"""
refresh = RefreshToken.for_user(user)
return {'access': str(refresh.access_token), 'refresh': str(refresh)}
def get_auth_client(user):
"""返回已携带 JWT Bearer 的 APIClient。"""
tokens = get_tokens(user)
client = APIClient()
client.credentials(HTTP_AUTHORIZATION=f'Bearer {tokens["access"]}')
return client
# ─── 师生关系 ─────────────────────────────────────────────────────────────────
def create_teacher_student_relation(teacher, student, status=1):
"""创建师生关系记录。"""
return TeacherStudentRelation.objects.create(
teacher=teacher,
student=student,
relation_type='指导',
status=status,
)
# ─── 科室工具 ─────────────────────────────────────────────────────────────────
def ensure_institution(name='测试医院', code='TEST-HOSP-001'):
2026-05-29 15:58:00 +08:00
inst, _ = Institution.objects.get_or_create(
code=code,
defaults={'name': name, 'type': 'hospital', 'province': '北京', 'city': '北京'},
2026-05-29 15:58:00 +08:00
)
return inst
def ensure_department(name='儿科'):
"""科室为全局表,与机构无关。"""
2026-05-29 15:58:00 +08:00
dept, _ = Department.objects.get_or_create(
name=name,
defaults={'category': '临床'},
2026-05-29 15:58:00 +08:00
)
return dept
# ─── 病例载荷构建 ─────────────────────────────────────────────────────────────
def sample_exam_items():
"""示例检查项(用于 C3 full-create)。"""
return [
{
'item_code': 'blood_routine',
'item_name': '血常规',
'item_type': 'lab',
'category': '实验室检查',
'result_text': 'WBC 10×10^9/L',
'result_structured': {'wbc': '10×10^9/L'},
'is_key': True,
'is_abnormal': False,
'score_weight': 1.0,
'display_order': 1,
},
]
def build_traditional_payload(department_name='儿科', scoring_rules_count=2, with_exam_items=False):
2026-05-29 15:58:00 +08:00
"""构建合法的传统病例 full-create 载荷。"""
rules = [
{
'dimension': f'测试维度{i + 1}',
'score_weight': round(1.0 / scoring_rules_count, 2),
'ai_auto_score': True,
'scoring_standard': f'评分标准{i + 1}',
}
for i in range(scoring_rules_count)
]
payload = {
2026-05-29 15:58:00 +08:00
'title': '测试传统病例-表单录入',
'case_type': 'traditional',
'difficulty': 'medium',
'chief_complaint': '发热 3 天',
'description': '患儿,男,4 岁,因发热 3 天就诊。',
'patient_age': 4,
'patient_gender': 'male',
'department_name': department_name,
'estimated_minutes': 30,
'osce_enabled': False,
'tags': '儿科,发热',
'traditional': {
'standard_diagnosis': '上呼吸道感染',
'standard_treatment': '对症治疗,退热处理',
'guideline_reference': '《儿科学》第 9 版',
},
'scoring_rules': rules,
}
if with_exam_items:
payload['exam_items'] = sample_exam_items()
return payload
2026-05-29 15:58:00 +08:00
def build_teaching_payload(department_name='儿科', scoring_rules_count=2):
"""构建合法的教学病例 full-create 载荷。"""
rules = [
{
'dimension': f'教学维度{i + 1}',
'score_weight': round(1.0 / scoring_rules_count, 2),
'ai_auto_score': False,
'scoring_standard': f'教学评分标准{i + 1}',
}
for i in range(scoring_rules_count)
]
return {
'title': '测试教学病例-表单录入',
'case_type': 'teaching',
'difficulty': 'hard',
'chief_complaint': '腹痛 2 天',
'description': '患者,女,28 岁,因腹痛 2 天就诊。',
'patient_age': 28,
'patient_gender': 'female',
'department_name': department_name,
'estimated_minutes': 45,
'osce_enabled': False,
'teaching': {
'teaching_goal': '掌握急腹症鉴别诊断',
'discussion_questions': '如何鉴别急性阑尾炎与其他急腹症?',
'teacher_guide': '引导学生按 SOAP 格式分析',
'scoring_focus': '鉴别诊断思路',
},
'scoring_rules': rules,
}
# ─── AI Mock 数据 ─────────────────────────────────────────────────────────────
MOCK_C1_PARSE_RESULT = {
'data': {
'title': 'Mock-儿科发热病例',
'case_type': 'traditional',
'difficulty': 'medium',
'chief_complaint': '发热 3 天',
'description': '患儿,男,4 岁,因发热 3 天就诊。',
'patient_age': 4,
'patient_gender': 'male',
'department_name': '儿科',
'estimated_minutes': 30,
'osce_enabled': False,
'tags': '儿科,发热',
'traditional': {
'standard_diagnosis': 'Mock 上呼吸道感染',
'standard_treatment': 'Mock 对症治疗',
'guideline_reference': 'Mock 指南',
},
'exam_items': [
{
'item_code': 'blood_routine',
'item_name': '血常规',
'item_type': 'lab',
'category': '实验室检查',
'result_text': 'WBC 10×10^9/L',
'is_key': True,
'is_abnormal': False,
'score_weight': 1.0,
'display_order': 1,
},
],
2026-05-29 15:58:00 +08:00
},
'usage': {'prompt_tokens': 100, 'completion_tokens': 200, 'total_tokens': 300},
}
MOCK_C2_SCORING_RULES = {
'data': {
'scoring_rules': [
{
'dimension': '诊断准确性',
'competency_dimension': '临床推理',
'score_weight': 0.4,
'ai_auto_score': True,
'osce_dimension': False,
'scoring_standard': '能准确判断上呼吸道感染',
},
{
'dimension': '治疗方案合理性',
'competency_dimension': '治疗决策',
'score_weight': 0.3,
'ai_auto_score': True,
'osce_dimension': False,
'scoring_standard': '治疗方案符合指南推荐',
},
{
'dimension': '医患沟通',
'competency_dimension': '沟通技巧',
'score_weight': 0.3,
'ai_auto_score': False,
'osce_dimension': True,
'scoring_standard': '能向家属解释病情和治疗方案',
},
],
},
'usage': {'prompt_tokens': 150, 'completion_tokens': 250, 'total_tokens': 400},
}
def make_fake_pdf():
"""创建一个假 PDF 上传文件(仅用于触发 multipart 解析)。"""
return SimpleUploadedFile(
'test.pdf',
b'%PDF-1.4 fake content for testing',
content_type='application/pdf',
)