""" Tests for Slot Validation Service. 槽位校验服务单元测试 """ import pytest from app.services.mid.slot_validation_service import ( SlotValidationService, SlotValidationErrorCode, ValidationResult, SlotValidationError, BatchValidationResult, ) class TestSlotValidationService: """槽位校验服务测试类""" @pytest.fixture def service(self): """创建校验服务实例""" return SlotValidationService() @pytest.fixture def string_slot_def(self): """字符串类型槽位定义""" return { "slot_key": "name", "type": "string", "required": False, "validation_rule": None, "ask_back_prompt": "请输入您的姓名", } @pytest.fixture def required_string_slot_def(self): """必填字符串类型槽位定义""" return { "slot_key": "phone", "type": "string", "required": True, "validation_rule": r"^1[3-9]\d{9}$", "ask_back_prompt": "请输入正确的手机号码", } @pytest.fixture def number_slot_def(self): """数字类型槽位定义""" return { "slot_key": "age", "type": "number", "required": False, "validation_rule": None, "ask_back_prompt": "请输入年龄", } @pytest.fixture def boolean_slot_def(self): """布尔类型槽位定义""" return { "slot_key": "is_student", "type": "boolean", "required": False, "validation_rule": None, "ask_back_prompt": "是否是学生?", } @pytest.fixture def enum_slot_def(self): """枚举类型槽位定义""" return { "slot_key": "grade", "type": "enum", "required": False, "options": ["初一", "初二", "初三", "高一", "高二", "高三"], "validation_rule": None, "ask_back_prompt": "请选择年级", } @pytest.fixture def array_enum_slot_def(self): """数组枚举类型槽位定义""" return { "slot_key": "subjects", "type": "array_enum", "required": False, "options": ["语文", "数学", "英语", "物理", "化学"], "validation_rule": None, "ask_back_prompt": "请选择学科", } @pytest.fixture def json_schema_slot_def(self): """JSON Schema 校验槽位定义""" return { "slot_key": "email", "type": "string", "required": True, "validation_rule": '{"type": "string", "format": "email"}', "ask_back_prompt": "请输入有效的邮箱地址", } class TestBasicValidation: """基础校验测试""" def test_empty_validation_rule(self, service, string_slot_def): """测试空校验规则(应通过)""" string_slot_def["validation_rule"] = None result = service.validate_slot_value(string_slot_def, "test") assert result.ok is True assert result.normalized_value == "test" def test_whitespace_validation_rule(self, service, string_slot_def): """测试空白校验规则(应通过)""" string_slot_def["validation_rule"] = " " result = service.validate_slot_value(string_slot_def, "test") assert result.ok is True def test_no_slot_definition(self, service): """测试无槽位定义(动态槽位)""" # 使用最小定义 minimal_def = {"slot_key": "dynamic_field"} result = service.validate_slot_value(minimal_def, "any_value") assert result.ok is True class TestRegexValidation: """正则表达式校验测试""" def test_regex_match(self, service, required_string_slot_def): """测试正则匹配成功""" result = service.validate_slot_value( required_string_slot_def, "13800138000" ) assert result.ok is True assert result.normalized_value == "13800138000" def test_regex_mismatch(self, service, required_string_slot_def): """测试正则匹配失败""" result = service.validate_slot_value( required_string_slot_def, "invalid_phone" ) assert result.ok is False assert result.error_code == SlotValidationErrorCode.SLOT_REGEX_MISMATCH assert result.ask_back_prompt == "请输入正确的手机号码" def test_regex_invalid_pattern(self, service, string_slot_def): """测试非法正则表达式""" string_slot_def["validation_rule"] = "[invalid(" result = service.validate_slot_value(string_slot_def, "test") assert result.ok is False assert ( result.error_code == SlotValidationErrorCode.SLOT_VALIDATION_RULE_INVALID ) def test_regex_with_chinese(self, service, string_slot_def): """测试包含中文的正则""" string_slot_def["validation_rule"] = r"^[\u4e00-\u9fa5]{2,4}$" result = service.validate_slot_value(string_slot_def, "张三") assert result.ok is True result = service.validate_slot_value(string_slot_def, "John") assert result.ok is False class TestJsonSchemaValidation: """JSON Schema 校验测试""" def test_json_schema_match(self, service): """测试 JSON Schema 匹配成功""" slot_def = { "slot_key": "config", "type": "object", "validation_rule": '{"type": "object", "properties": {"name": {"type": "string"}}}', } result = service.validate_slot_value(slot_def, {"name": "test"}) assert result.ok is True def test_json_schema_mismatch(self, service): """测试 JSON Schema 匹配失败""" slot_def = { "slot_key": "count", "type": "number", "validation_rule": '{"type": "integer", "minimum": 0, "maximum": 100}', "ask_back_prompt": "请输入0-100之间的整数", } result = service.validate_slot_value(slot_def, 150) assert result.ok is False assert ( result.error_code == SlotValidationErrorCode.SLOT_JSON_SCHEMA_MISMATCH ) assert result.ask_back_prompt == "请输入0-100之间的整数" def test_json_schema_invalid_json(self, service, string_slot_def): """测试非法 JSON Schema""" string_slot_def["validation_rule"] = "{invalid json}" result = service.validate_slot_value(string_slot_def, "test") assert result.ok is False assert ( result.error_code == SlotValidationErrorCode.SLOT_VALIDATION_RULE_INVALID ) def test_json_schema_array(self, service): """测试数组类型的 JSON Schema""" slot_def = { "slot_key": "items", "type": "array", "validation_rule": '{"type": "array", "items": {"type": "string"}}', } result = service.validate_slot_value(slot_def, ["a", "b", "c"]) assert result.ok is True result = service.validate_slot_value(slot_def, [1, 2, 3]) assert result.ok is False class TestRequiredValidation: """必填校验测试""" def test_required_missing_none(self, service, required_string_slot_def): """测试必填字段为 None""" result = service.validate_slot_value( required_string_slot_def, None ) assert result.ok is False assert result.error_code == SlotValidationErrorCode.SLOT_REQUIRED_MISSING def test_required_missing_empty_string(self, service, required_string_slot_def): """测试必填字段为空字符串""" result = service.validate_slot_value(required_string_slot_def, "") assert result.ok is False assert result.error_code == SlotValidationErrorCode.SLOT_REQUIRED_MISSING def test_required_missing_whitespace(self, service, required_string_slot_def): """测试必填字段为空白字符""" result = service.validate_slot_value(required_string_slot_def, " ") assert result.ok is False assert result.error_code == SlotValidationErrorCode.SLOT_REQUIRED_MISSING def test_required_present(self, service, required_string_slot_def): """测试必填字段有值""" result = service.validate_slot_value( required_string_slot_def, "13800138000" ) assert result.ok is True def test_not_required_empty(self, service, string_slot_def): """测试非必填字段为空""" result = service.validate_slot_value(string_slot_def, "") assert result.ok is True class TestTypeValidation: """类型校验测试""" def test_string_type(self, service, string_slot_def): """测试字符串类型""" result = service.validate_slot_value(string_slot_def, "hello") assert result.ok is True assert result.normalized_value == "hello" def test_string_type_conversion(self, service, string_slot_def): """测试字符串类型自动转换""" result = service.validate_slot_value(string_slot_def, 123) assert result.ok is True assert result.normalized_value == "123" def test_number_type_integer(self, service, number_slot_def): """测试数字类型 - 整数""" result = service.validate_slot_value(number_slot_def, 25) assert result.ok is True assert result.normalized_value == 25 def test_number_type_float(self, service, number_slot_def): """测试数字类型 - 浮点数""" result = service.validate_slot_value(number_slot_def, 25.5) assert result.ok is True assert result.normalized_value == 25.5 def test_number_type_string_conversion(self, service, number_slot_def): """测试数字类型 - 字符串转换""" result = service.validate_slot_value(number_slot_def, "30") assert result.ok is True assert result.normalized_value == 30 def test_number_type_invalid(self, service, number_slot_def): """测试数字类型 - 无效值""" result = service.validate_slot_value(number_slot_def, "not_a_number") assert result.ok is False assert result.error_code == SlotValidationErrorCode.SLOT_TYPE_INVALID def test_number_type_reject_boolean(self, service, number_slot_def): """测试数字类型 - 拒绝布尔值""" result = service.validate_slot_value(number_slot_def, True) assert result.ok is False assert result.error_code == SlotValidationErrorCode.SLOT_TYPE_INVALID def test_boolean_type_true(self, service, boolean_slot_def): """测试布尔类型 - True""" result = service.validate_slot_value(boolean_slot_def, True) assert result.ok is True assert result.normalized_value is True def test_boolean_type_false(self, service, boolean_slot_def): """测试布尔类型 - False""" result = service.validate_slot_value(boolean_slot_def, False) assert result.ok is True assert result.normalized_value is False def test_boolean_type_string_true(self, service, boolean_slot_def): """测试布尔类型 - 字符串 true""" result = service.validate_slot_value(boolean_slot_def, "true") assert result.ok is True assert result.normalized_value is True def test_boolean_type_string_yes(self, service, boolean_slot_def): """测试布尔类型 - 字符串 yes/是""" result = service.validate_slot_value(boolean_slot_def, "是") assert result.ok is True assert result.normalized_value is True def test_boolean_type_string_false(self, service, boolean_slot_def): """测试布尔类型 - 字符串 false""" result = service.validate_slot_value(boolean_slot_def, "false") assert result.ok is True assert result.normalized_value is False def test_boolean_type_invalid(self, service, boolean_slot_def): """测试布尔类型 - 无效值""" result = service.validate_slot_value(boolean_slot_def, "maybe") assert result.ok is False assert result.error_code == SlotValidationErrorCode.SLOT_TYPE_INVALID def test_enum_type_valid(self, service, enum_slot_def): """测试枚举类型 - 有效值""" result = service.validate_slot_value(enum_slot_def, "高一") assert result.ok is True assert result.normalized_value == "高一" def test_enum_type_invalid(self, service, enum_slot_def): """测试枚举类型 - 无效值""" result = service.validate_slot_value(enum_slot_def, "大一") assert result.ok is False assert result.error_code == SlotValidationErrorCode.SLOT_ENUM_INVALID def test_enum_type_not_string(self, service, enum_slot_def): """测试枚举类型 - 非字符串""" result = service.validate_slot_value(enum_slot_def, 123) assert result.ok is False assert result.error_code == SlotValidationErrorCode.SLOT_TYPE_INVALID def test_array_enum_type_valid(self, service, array_enum_slot_def): """测试数组枚举类型 - 有效值""" result = service.validate_slot_value( array_enum_slot_def, ["语文", "数学"] ) assert result.ok is True def test_array_enum_type_invalid_item(self, service, array_enum_slot_def): """测试数组枚举类型 - 无效元素""" result = service.validate_slot_value( array_enum_slot_def, ["语文", "生物"] ) assert result.ok is False assert ( result.error_code == SlotValidationErrorCode.SLOT_ARRAY_ENUM_INVALID ) def test_array_enum_type_not_array(self, service, array_enum_slot_def): """测试数组枚举类型 - 非数组""" result = service.validate_slot_value(array_enum_slot_def, "语文") assert result.ok is False assert result.error_code == SlotValidationErrorCode.SLOT_TYPE_INVALID def test_array_enum_type_non_string_item(self, service, array_enum_slot_def): """测试数组枚举类型 - 非字符串元素""" result = service.validate_slot_value(array_enum_slot_def, ["语文", 123]) assert result.ok is False assert ( result.error_code == SlotValidationErrorCode.SLOT_ARRAY_ENUM_INVALID ) class TestBatchValidation: """批量校验测试""" def test_batch_all_valid(self, service, string_slot_def, number_slot_def): """测试批量校验 - 全部通过""" slot_defs = [string_slot_def, number_slot_def] values = {"name": "张三", "age": 25} result = service.validate_slots(slot_defs, values) assert result.ok is True assert len(result.errors) == 0 assert result.validated_values["name"] == "张三" assert result.validated_values["age"] == 25 def test_batch_some_invalid(self, service, string_slot_def, number_slot_def): """测试批量校验 - 部分失败""" slot_defs = [string_slot_def, number_slot_def] values = {"name": "张三", "age": "not_a_number"} result = service.validate_slots(slot_defs, values) assert result.ok is False assert len(result.errors) == 1 assert result.errors[0].slot_key == "age" def test_batch_missing_required( self, service, required_string_slot_def, string_slot_def ): """测试批量校验 - 缺失必填字段""" slot_defs = [required_string_slot_def, string_slot_def] values = {"name": "张三"} # 缺少 phone result = service.validate_slots(slot_defs, values) assert result.ok is False assert len(result.errors) == 1 assert result.errors[0].slot_key == "phone" assert ( result.errors[0].error_code == SlotValidationErrorCode.SLOT_REQUIRED_MISSING ) def test_batch_undefined_slot(self, service, string_slot_def): """测试批量校验 - 未定义槽位""" slot_defs = [string_slot_def] values = {"name": "张三", "undefined_field": "value"} result = service.validate_slots(slot_defs, values) assert result.ok is True # 未定义槽位应允许通过 assert "undefined_field" in result.validated_values class TestCombinedValidation: """组合校验测试(类型 + 正则/JSON Schema)""" def test_type_and_regex_both_pass(self, service): """测试类型和正则都通过""" slot_def = { "slot_key": "code", "type": "string", "required": True, "validation_rule": r"^[A-Z]{2}\d{4}$", } result = service.validate_slot_value(slot_def, "AB1234") assert result.ok is True def test_type_pass_regex_fail(self, service): """测试类型通过但正则失败""" slot_def = { "slot_key": "code", "type": "string", "required": True, "validation_rule": r"^[A-Z]{2}\d{4}$", } result = service.validate_slot_value(slot_def, "ab1234") assert result.ok is False assert result.error_code == SlotValidationErrorCode.SLOT_REGEX_MISMATCH def test_type_fail_no_regex_check(self, service): """测试类型失败时不执行正则校验""" slot_def = { "slot_key": "code", "type": "number", "required": True, "validation_rule": r"^\d+$", } result = service.validate_slot_value(slot_def, "not_a_number") assert result.ok is False assert result.error_code == SlotValidationErrorCode.SLOT_TYPE_INVALID class TestAskBackPrompt: """追问提示语测试""" def test_ask_back_prompt_on_validation_fail(self, service): """测试校验失败时返回 ask_back_prompt""" slot_def = { "slot_key": "email", "type": "string", "required": True, "validation_rule": r"^[\w\.-]+@[\w\.-]+\.\w+$", "ask_back_prompt": "请输入有效的邮箱地址,如 example@domain.com", } result = service.validate_slot_value(slot_def, "invalid_email") assert result.ok is False assert result.ask_back_prompt == "请输入有效的邮箱地址,如 example@domain.com" def test_no_ask_back_prompt_on_success(self, service, string_slot_def): """测试校验通过时不返回 ask_back_prompt""" result = service.validate_slot_value(string_slot_def, "valid") assert result.ok is True assert result.ask_back_prompt is None def test_ask_back_prompt_on_required_missing(self, service): """测试必填缺失时返回 ask_back_prompt""" slot_def = { "slot_key": "name", "type": "string", "required": True, "ask_back_prompt": "请告诉我们您的姓名", } result = service.validate_slot_value(slot_def, "") assert result.ok is False assert result.ask_back_prompt == "请告诉我们您的姓名" class TestSlotValidationErrorCode: """错误码测试""" def test_error_code_values(self): """测试错误码值""" assert SlotValidationErrorCode.SLOT_REQUIRED_MISSING == "SLOT_REQUIRED_MISSING" assert SlotValidationErrorCode.SLOT_TYPE_INVALID == "SLOT_TYPE_INVALID" assert SlotValidationErrorCode.SLOT_REGEX_MISMATCH == "SLOT_REGEX_MISMATCH" assert ( SlotValidationErrorCode.SLOT_JSON_SCHEMA_MISMATCH == "SLOT_JSON_SCHEMA_MISMATCH" ) assert ( SlotValidationErrorCode.SLOT_VALIDATION_RULE_INVALID == "SLOT_VALIDATION_RULE_INVALID" ) class TestValidationResult: """ValidationResult 测试""" def test_success_result(self): """测试成功结果""" result = ValidationResult(ok=True, normalized_value="test") assert result.ok is True assert result.normalized_value == "test" assert result.error_code is None assert result.error_message is None def test_failure_result(self): """测试失败结果""" result = ValidationResult( ok=False, error_code="SLOT_REGEX_MISMATCH", error_message="格式不正确", ask_back_prompt="请重新输入", ) assert result.ok is False assert result.error_code == "SLOT_REGEX_MISMATCH" assert result.error_message == "格式不正确" assert result.ask_back_prompt == "请重新输入"