""" 输入验证测试(Pydantic Schema 边界条件) 测试各 API Schema 的字段校验规则: - 必填字段缺失 - 字段长度限制 - 数值范围约束 - 无效枚举值 """ import pytest from pydantic import ValidationError from app.schemas.consultation import ( ConsultationStartRequest, ConsultationUpdateRequest, ConsultationListRequest, ) from app.schemas.soap import ( SoapUpdateRequest, SoapResponse, GenerateDischargeInstructionsRequest, GenerateReferralLetterRequest, TemplateTypeEnum, ) from app.schemas.patient import PatientCreate, PatientUpdate # ============================================================ # ConsultationStartRequest # ============================================================ class TestConsultationStartValidation: """问诊开始请求校验""" def test_minimal_valid(self): """最简有效字段应通过校验""" req = ConsultationStartRequest(animal_name="大黄", species="犬") assert req.animal_name == "大黄" assert req.species == "犬" def test_empty_all_optional(self): """所有字段留空(使用默认值)应通过校验""" req = ConsultationStartRequest() assert req.animal_name == "" assert req.species == "" def test_animal_name_too_long(self): """动物名称超长应报错""" with pytest.raises(ValidationError): ConsultationStartRequest(animal_name="A" * 101) def test_animal_name_max_length_ok(self): """动物名称等于最大长度应通过""" req = ConsultationStartRequest(animal_name="A" * 100) assert len(req.animal_name) == 100 def test_species_too_long(self): """物种超长应报错""" with pytest.raises(ValidationError): ConsultationStartRequest(species="A" * 51) def test_breed_too_long(self): """品种超长应报错""" with pytest.raises(ValidationError): ConsultationStartRequest(breed="A" * 101) def test_weight_negative(self): """体重为负数应报错""" with pytest.raises(ValidationError): ConsultationStartRequest(animal_name="大黄", weight=-1.0) def test_weight_zero_ok(self): """体重为 0 应通过(Pydantic ge=0)""" req = ConsultationStartRequest(animal_name="大黄", weight=0.0) assert req.weight == 0.0 def test_weight_positive_ok(self): """正数体重应通过""" req = ConsultationStartRequest(animal_name="大黄", weight=25.5) assert req.weight == 25.5 def test_full_fields_valid(self): """完整字段应通过校验""" req = ConsultationStartRequest( animal_name="小白", species="猫", breed="英短", age="2岁", sex="雌性已绝育", weight=4.5, bcs="5", owner_name="李女士", hospital_type="综合医院", ) assert req.animal_name == "小白" assert req.weight == 4.5 # ============================================================ # ConsultationUpdateRequest # ============================================================ class TestConsultationUpdateValidation: """问诊更新请求校验""" def test_empty_update_ok(self): """空更新(所有字段 None)应通过校验""" req = ConsultationUpdateRequest() assert req.animal_name is None assert req.species is None def test_partial_update_ok(self): """部分字段更新应通过""" req = ConsultationUpdateRequest(animal_name="新名字", weight=10.0) assert req.animal_name == "新名字" assert req.weight == 10.0 assert req.species is None # 未传的应为 None def test_weight_negative_rejected(self): """体重负数应报错""" with pytest.raises(ValidationError): ConsultationUpdateRequest(weight=-1.0) def test_animal_name_empty_ok(self): """空字符串动物名应通过(Optional 字段)""" req = ConsultationUpdateRequest(animal_name="") assert req.animal_name == "" def test_name_too_long(self): """名称超长应报错""" with pytest.raises(ValidationError): ConsultationUpdateRequest(animal_name="A" * 101) # ============================================================ # ConsultationListRequest # ============================================================ class TestConsultationListValidation: """问诊列表查询参数校验""" def test_defaults(self): """默认值应正确""" req = ConsultationListRequest() assert req.page == 1 assert req.page_size == 20 assert req.sort_by == "created_at" assert req.sort_order == "desc" def test_page_zero_rejected(self): """page 为 0 应报错(ge=1)""" with pytest.raises(ValidationError): ConsultationListRequest(page=0) def test_page_negative_rejected(self): """page 为负数应报错""" with pytest.raises(ValidationError): ConsultationListRequest(page=-1) def test_page_size_zero_rejected(self): """page_size 为 0 应报错""" with pytest.raises(ValidationError): ConsultationListRequest(page_size=0) def test_page_size_over_100_rejected(self): """page_size 超过 100 应报错""" with pytest.raises(ValidationError): ConsultationListRequest(page_size=101) def test_page_size_max_ok(self): """page_size 等于 100 应通过""" req = ConsultationListRequest(page_size=100) assert req.page_size == 100 def test_sort_order_invalid(self): """任意 sort_order 字符串应通过(无枚举约束)""" req = ConsultationListRequest(sort_order="random") assert req.sort_order == "random" def test_custom_values_ok(self): """自定义合法值应通过""" req = ConsultationListRequest( page=3, page_size=50, keyword="大黄", status="completed", sort_by="updated_at", sort_order="asc", ) assert req.page == 3 assert req.keyword == "大黄" assert req.status == "completed" # ============================================================ # PatientCreate # ============================================================ class TestPatientCreateValidation: """患者创建请求校验""" def test_minimal_valid(self): """必填字段存在应通过""" req = PatientCreate(owner_name="张三", animal_name="旺财") assert req.owner_name == "张三" assert req.animal_name == "旺财" def test_missing_owner_name(self): """缺少 owner_name 应报错""" with pytest.raises(ValidationError): PatientCreate(animal_name="旺财") def test_missing_animal_name(self): """缺少 animal_name 应报错""" with pytest.raises(ValidationError): PatientCreate(owner_name="张三") def test_owner_name_empty_rejected(self): """owner_name 为空字符串应报错(min_length=1)""" with pytest.raises(ValidationError): PatientCreate(owner_name="", animal_name="旺财") def test_animal_name_empty_rejected(self): """animal_name 为空字符串应报错""" with pytest.raises(ValidationError): PatientCreate(owner_name="张三", animal_name="") def test_owner_name_too_long(self): """owner_name 超长应报错""" with pytest.raises(ValidationError): PatientCreate(owner_name="A" * 101, animal_name="旺财") def test_animal_name_too_long(self): """animal_name 超长应报错""" with pytest.raises(ValidationError): PatientCreate(owner_name="张三", animal_name="A" * 101) def test_owner_name_exact_max_ok(self): """owner_name 等于最大长度应通过""" req = PatientCreate(owner_name="A" * 100, animal_name="旺财") assert len(req.owner_name) == 100 def test_optional_fields_default(self): """可选字段默认为空字符串""" req = PatientCreate(owner_name="张三", animal_name="旺财") assert req.species == "" assert req.breed == "" assert req.sex == "" assert req.age == "" assert req.weight == "" def test_full_fields_valid(self): """完整字段应通过""" req = PatientCreate( owner_name="李医生", animal_name="咪咪", species="猫", breed="布偶", sex="雌性已绝育", is_neutered="已绝育", age="3岁", weight="4.5", bcs="5", immune_history="疫苗齐全,每年加强", allergy_history="无", medical_history="2024年因肠胃炎就诊", notes="性格温顺,体检配合", ) assert req.owner_name == "李医生" assert req.immune_history == "疫苗齐全,每年加强" # ============================================================ # PatientUpdate # ============================================================ class TestPatientUpdateValidation: """患者更新请求校验""" def test_empty_update_ok(self): """空更新应通过(所有字段 optional)""" req = PatientUpdate() assert req.owner_name is None def test_partial_update_ok(self): """部分更新应通过""" req = PatientUpdate(owner_name="新主人", age="4岁") assert req.owner_name == "新主人" assert req.age == "4岁" assert req.animal_name is None def test_owner_name_empty_rejected(self): """owner_name 为空应报错(min_length=1 对 Optional 仍生效)""" with pytest.raises(ValidationError): PatientUpdate(owner_name="") def test_animal_name_empty_rejected(self): """animal_name 为空应报错""" with pytest.raises(ValidationError): PatientUpdate(animal_name="") def test_name_too_long_rejected(self): """名称超长应报错""" with pytest.raises(ValidationError): PatientUpdate(animal_name="A" * 101) def test_species_too_long_rejected(self): """物种超长应报错""" with pytest.raises(ValidationError): PatientUpdate(species="A" * 51) # ============================================================ # SoapUpdateRequest # ============================================================ class TestSoapUpdateValidation: """SOAP 更新请求校验""" def test_defaults(self): """默认值应正确""" req = SoapUpdateRequest() assert req.confirm is False assert req.subjective is None def test_partial_update_ok(self): """部分字段更新应通过""" req = SoapUpdateRequest(subjective="S段内容", confirm=True) assert req.subjective == "S段内容" assert req.confirm is True def test_all_sections_update_ok(self): """所有段落更新应通过""" req = SoapUpdateRequest( subjective="S", objective="O", assessment="A", plan="P", clinical_reasoning="CR", client_communication="CC", problem_list="PL", discharge_instructions="DI", referral_letter="RL", history_gaps="HG", differential_diagnosis="DDX", confirm=False, ) assert req.subjective == "S" assert req.differential_diagnosis == "DDX" def test_large_content_ok(self): """大段内容应通过(无长度限制)""" large_text = "X" * 10000 req = SoapUpdateRequest(subjective=large_text) assert len(req.subjective) == 10000 # ============================================================ # GenerateDischargeInstructionsRequest # ============================================================ class TestDischargeInstructionsRequestValidation: """出院说明生成请求校验""" def test_valid_request(self): """有效请求应通过""" req = GenerateDischargeInstructionsRequest(consultation_id="abc123") assert req.consultation_id == "abc123" def test_missing_consultation_id(self): """缺少 consultation_id 应报错""" with pytest.raises(ValidationError): GenerateDischargeInstructionsRequest() # ============================================================ # GenerateReferralLetterRequest # ============================================================ class TestReferralLetterRequestValidation: """转诊信生成请求校验""" def test_valid_request(self): """有效请求应通过""" req = GenerateReferralLetterRequest(consultation_id="xyz789") assert req.consultation_id == "xyz789" def test_missing_consultation_id(self): """缺少 consultation_id 应报错""" with pytest.raises(ValidationError): GenerateReferralLetterRequest() # ============================================================ # TemplateTypeEnum # ============================================================ class TestTemplateTypeEnum: """模板类型枚举测试""" def test_valid_values(self): """有效枚举值应能创建""" assert TemplateTypeEnum.soap.value == "soap" assert TemplateTypeEnum.lab_result.value == "lab_result" assert TemplateTypeEnum.discharge_instructions.value == "discharge_instructions" assert TemplateTypeEnum.referral_letter.value == "referral_letter" def test_invalid_value_rejected(self): """无效枚举值应报错""" with pytest.raises(ValueError): TemplateTypeEnum("invalid_type") def test_enum_membership(self): """成员检查应正确""" assert TemplateTypeEnum.soap in TemplateTypeEnum # ============================================================ # SoapResponse # ============================================================ class TestSoapResponseValidation: """SOAP 响应模型校验""" def test_minimal_valid(self): """最简有效响应应通过""" resp = SoapResponse( id="soap_001", consultation_id="consult_001", ) assert resp.id == "soap_001" assert resp.consultation_id == "consult_001" assert resp.status == "draft" # 默认值 def test_all_defaults(self): """所有默认值应正确""" resp = SoapResponse(id="s1", consultation_id="c1") assert resp.subjective == "" assert resp.objective == "" assert resp.assessment == "" assert resp.plan == "" assert resp.clinical_reasoning == "" assert resp.client_communication == "" assert resp.problem_list == "" assert resp.discharge_instructions == "" assert resp.referral_letter == "" assert resp.history_gaps == "" assert resp.differential_diagnosis == "" def test_missing_required_fields(self): """缺少必填 id 应报错""" with pytest.raises(ValidationError): SoapResponse(consultation_id="c1") def test_missing_consultation_id(self): """缺少 consultation_id 应报错""" with pytest.raises(ValidationError): SoapResponse(id="s1")