- fields_registry: FORM_TO_BUSINESS_KEY bidirectional mapping between the graph's six form keys (has_injury...) and the acd business keys (ywrysw...); bjrjs/ywmtc have no form source (set_info only); business_patch_from_form maps graph diffs to whitelisted business keys and filters internal/unknown keys - app_service: chat injection now maps repository business keys back to form keys; both complete() and stream() write back field diffs (FormUpdate events captured in stream, closing the skeleton gap); form keys never leak into the business repository - docs/assignment6-compat.md: mapping table documented, stream write-back backlog item resolved - tests: mapping coverage, write-back mapping end-to-end, reverse mapping injection (bjrjs/ywmtc excluded), stream FormUpdate write-back Verified: 205 unit tests
122 lines
4.8 KiB
Python
122 lines
4.8 KiB
Python
"""作业 6:字段注册表测试(分组完整性、类型/敏感级别、外部表示)。"""
|
||
|
||
import pytest
|
||
|
||
from src.services.fields_registry import (
|
||
FIELD_REGISTRY,
|
||
GROUP_KEYS,
|
||
INTERNAL_FIELD_KEYS,
|
||
FORM_TO_BUSINESS_KEY,
|
||
BUSINESS_TO_FORM_KEY,
|
||
FieldGroup,
|
||
SensitiveLevel,
|
||
business_patch_from_form,
|
||
parse_field_value,
|
||
to_external_string,
|
||
)
|
||
|
||
|
||
def test_registry_covers_all_three_groups():
|
||
"""注册表完整覆盖 acd/human1/human2 三个分组(作业 6-3)。"""
|
||
assert set(GROUP_KEYS) == set(FieldGroup)
|
||
assert len(FIELD_REGISTRY) == 24
|
||
assert len(GROUP_KEYS[FieldGroup.ACD]) == 8
|
||
assert len(GROUP_KEYS[FieldGroup.HUMAN1]) == 8
|
||
assert len(GROUP_KEYS[FieldGroup.HUMAN2]) == 8
|
||
|
||
|
||
def test_person_fields_include_full_set():
|
||
"""人员字段必须包含 sfzmwh/sjwh/csbw,不能只迁姓名和号码。"""
|
||
for index in (1, 2):
|
||
group_keys = set(GROUP_KEYS[FieldGroup.HUMAN1 if index == 1 else FieldGroup.HUMAN2])
|
||
for suffix in ("xm", "hpzl", "hphm", "sfzmhm", "sfzmwh", "sjhm", "sjwh", "csbw"):
|
||
assert f"{suffix}{index}" in group_keys
|
||
|
||
|
||
def test_registry_metadata_recorded():
|
||
"""每个字段都记录类型、可写性、敏感级别(作业 6-4)。"""
|
||
for spec in FIELD_REGISTRY.values():
|
||
assert spec.value_type in (str, bool, int)
|
||
assert spec.writable is True
|
||
assert isinstance(spec.sensitive, SensitiveLevel)
|
||
assert spec.description
|
||
|
||
|
||
def test_sensitive_levels_assigned():
|
||
"""证件号/手机号 sensitive,姓名 personal,其余 public。"""
|
||
assert FIELD_REGISTRY["sfzmhm1"].sensitive is SensitiveLevel.SENSITIVE
|
||
assert FIELD_REGISTRY["sjhm2"].sensitive is SensitiveLevel.SENSITIVE
|
||
assert FIELD_REGISTRY["xm1"].sensitive is SensitiveLevel.PERSONAL
|
||
assert FIELD_REGISTRY["hphm1"].sensitive is SensitiveLevel.PUBLIC
|
||
|
||
|
||
@pytest.mark.parametrize("raw,expected", [
|
||
("1", True), ("true", True), ("是", True),
|
||
("0", False), ("false", False), ("否", False),
|
||
])
|
||
def test_parse_bool_explicit(raw, expected):
|
||
assert parse_field_value(FIELD_REGISTRY["ywrysw"], raw) is expected
|
||
|
||
|
||
@pytest.mark.parametrize("bad", ["", "maybe", "2", "有吧"])
|
||
def test_parse_bool_rejects_ambiguous(bad):
|
||
"""模糊值必须拒绝——保留 空/False/0/未知 的区分能力。"""
|
||
with pytest.raises(ValueError):
|
||
parse_field_value(FIELD_REGISTRY["ywrysw"], bad)
|
||
|
||
|
||
def test_parse_int_and_str():
|
||
assert parse_field_value(FIELD_REGISTRY["jdcsl"], "2") == 2
|
||
assert parse_field_value(FIELD_REGISTRY["jdcsl"], "0") == 0
|
||
with pytest.raises(ValueError):
|
||
parse_field_value(FIELD_REGISTRY["jdcsl"], "两辆")
|
||
assert parse_field_value(FIELD_REGISTRY["hphm1"], "苏B12345") == "苏B12345"
|
||
|
||
|
||
def test_external_string_representation():
|
||
"""对外序列化:True->1,False->0,None->空串;内部仍区分 False 与 0。"""
|
||
assert to_external_string(True) == "1"
|
||
assert to_external_string(False) == "0"
|
||
assert to_external_string(0) == "0"
|
||
assert to_external_string(None) == ""
|
||
assert to_external_string("苏B12345") == "苏B12345"
|
||
# 内部存储不混淆:False 是 bool、0 是 int
|
||
assert parse_field_value(FIELD_REGISTRY["ywrysw"], "0") is False
|
||
assert parse_field_value(FIELD_REGISTRY["jdcsl"], "0") == 0
|
||
assert isinstance(parse_field_value(FIELD_REGISTRY["jdcsl"], "0"), int)
|
||
|
||
|
||
def test_internal_field_protection_listed():
|
||
"""图内部键登记在保护清单中,set_info 白名单不会收录它们。"""
|
||
for key in INTERNAL_FIELD_KEYS:
|
||
assert key not in FIELD_REGISTRY
|
||
|
||
|
||
def test_form_business_key_mapping_covers_form_fields():
|
||
"""图表单六键全部映射到 acd 业务键(作业 6 建立的映射)。"""
|
||
assert set(FORM_TO_BUSINESS_KEY) == {
|
||
"has_injury", "has_non_motor_vehicle_hit", "is_on_scene",
|
||
"vehicle_count", "accident_time", "accident_cause",
|
||
}
|
||
for business_key in FORM_TO_BUSINESS_KEY.values():
|
||
assert business_key in FIELD_REGISTRY
|
||
assert FIELD_REGISTRY[business_key].group is FieldGroup.ACD
|
||
|
||
|
||
def test_business_patch_from_form_maps_and_filters():
|
||
"""表单键映射、人员键原样、未知键过滤。"""
|
||
patch = business_patch_from_form({
|
||
"has_injury": False, # -> ywrysw
|
||
"vehicle_count": 2, # -> jdcsl
|
||
"xm2": "李四", # 人员键原样
|
||
"stage_code": "1002", # 内部键过滤
|
||
"no_such_form_key": "x", # 未知键过滤
|
||
})
|
||
assert patch == {"ywrysw": False, "jdcsl": 2, "xm2": "李四"}
|
||
|
||
|
||
def test_business_keys_without_form_source_not_injected():
|
||
"""bjrjs/ywmtc 无表单来源:不进入注入映射(只能 set_info 直写)。"""
|
||
assert "bjrjs" not in BUSINESS_TO_FORM_KEY
|
||
assert "ywmtc" not in BUSINESS_TO_FORM_KEY
|