VetNet-LCPS / vetcopilot-backend /tests /test_validation.py
thomasm291678
VetCopilot-LCPS: LCPS 2.0 本地推理引擎替换 DeepSeek API
793e953
Raw History Blame Contribute Delete
15.6 kB
"""
输入验证测试(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")