using FluentAssertions; using Workflow.Application.Form.Schema; using Xunit; namespace Workflow.Tests.Form; public class FormDataValidatorTests { private const string Schema = """ { "type": "object", "properties": { "name": { "type": "string", "title": "姓名", "required": true, "x-component": "Input", "x-validator": [ { "type": "minLength", "value": 2, "message": "姓名至少 2 个字符" } ] }, "age": { "type": "number", "title": "年龄", "x-component": "InputNumber", "x-validator": [ { "type": "min", "value": 0, "message": "年龄不能小于 0" }, { "type": "max", "value": 150, "message": "年龄不能大于 150" } ] }, "gender": { "type": "string", "title": "性别", "required": true, "x-component": "Select", "x-data-source": { "type": "static", "options": [ { "label": "男", "value": "male" }, { "label": "女", "value": "female" } ] } } } } """; private static readonly HashSet AllowedComponents = ["Input", "InputNumber", "Select"]; [Fact] public void Validate_WithValidData_ReturnsValid() { var result = FormDataValidator.Validate(Schema, """{"name":"张三","age":25,"gender":"male"}""", AllowedComponents); result.IsValid.Should().BeTrue(string.Join("; ", result.Errors)); result.Errors.Should().BeEmpty(); } [Fact] public void Validate_WithMissingRequiredField_ReturnsFieldError() { var result = FormDataValidator.Validate(Schema, """{"age":25,"gender":"male"}""", AllowedComponents); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.Contains("姓名") && e.Contains("必填")); } [Fact] public void Validate_WithEnumMismatch_ReturnsFieldError() { var result = FormDataValidator.Validate(Schema, """{"name":"张三","age":25,"gender":"other"}""", AllowedComponents); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.Contains("gender") && e.Contains("允许范围")); } [Fact] public void Validate_WithLengthAndRangeViolations_ReturnsAllErrors() { var result = FormDataValidator.Validate(Schema, """{"name":"李","age":151,"gender":"male"}""", AllowedComponents); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.Contains("姓名至少 2 个字符")); result.Errors.Should().Contain(e => e.Contains("年龄不能大于 150")); } [Fact] public void Validate_HiddenFieldByReaction_SkipsRequiredCheck() { // leaveType != sick 时(visible 规则未命中),reason 被联动隐藏。 // reason 即使必填缺失也不应报错。 const string schema = """ { "type": "object", "properties": { "leaveType": { "type": "string", "title": "请假类型", "x-component": "Select", "x-reactions": [ { "type": "condition", "target": "reason", "when": { "source": "leaveType", "operator": "eq", "value": "sick" }, "action": "visible" } ] }, "reason": { "type": "string", "title": "原因", "required": true, "x-component": "Input" } } } """; // leaveType=annual 未命中 visible 规则 → reason 隐藏 → 跳过必填 var result = FormDataValidator.Validate( schema, """{"leaveType":"annual"}""", ["Input", "Select"]); result.IsValid.Should().BeTrue(string.Join("; ", result.Errors)); } [Fact] public void Validate_NestedFieldInContainer_ReadsValueByDottedPath() { // FormGrid/FormLayout 等容器下的嵌套字段:field.Path 为点分路径(dateRange.startDate), // 提交数据是嵌套对象 {dateRange:{startDate:...}}。校验器必须按点分路径取值, // 否则嵌套必填字段会被误判为缺失,导致带容器的表单无法通过校验。 const string schema = """ { "type": "object", "properties": { "leaveType": { "type": "string", "title": "请假类型", "required": true, "x-component": "Select" }, "dateRange": { "type": "void", "title": "日期范围", "x-component": "FormGrid", "properties": { "startDate": { "type": "string", "title": "开始日期", "required": true, "x-component": "DatePicker" }, "endDate": { "type": "string", "title": "结束日期", "required": true, "x-component": "DatePicker" } } } } } """; var result = FormDataValidator.Validate( schema, """{"leaveType":"annual","dateRange":{"startDate":"2026-06-15","endDate":"2026-06-16"}}""", ["Input", "Select", "DatePicker", "FormGrid"]); result.IsValid.Should().BeTrue(string.Join("; ", result.Errors)); } [Fact] public void Validate_NestedFieldMissing_ReportsRequiredError() { const string schema = """ { "type": "object", "properties": { "dateRange": { "type": "void", "x-component": "FormGrid", "properties": { "startDate": { "type": "string", "title": "开始日期", "required": true, "x-component": "DatePicker" } } } } } """; var result = FormDataValidator.Validate( schema, """{"dateRange":{}}""", ["DatePicker", "FormGrid"]); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.Contains("开始日期") && e.Contains("必填")); } [Fact] public void Validate_VisibleRequiredField_StillEnforced() { // leaveType=sick 时(visible 规则命中),reason 可见且必填,缺失应报错 const string schema = """ { "type": "object", "properties": { "leaveType": { "type": "string", "title": "请假类型", "x-component": "Select", "x-reactions": [ { "type": "condition", "target": "reason", "when": { "source": "leaveType", "operator": "eq", "value": "sick" }, "action": "visible" } ] }, "reason": { "type": "string", "title": "原因", "required": true, "x-component": "Input" } } } """; // leaveType=sick 命中 visible 规则 → reason 显示 → 必填校验生效 var result = FormDataValidator.Validate( schema, """{"leaveType":"sick"}""", ["Input", "Select"]); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.Contains("原因") && e.Contains("必填")); } [Fact] public void Validate_FieldHiddenByPermissionForCurrentNode_SkipsRequiredCheck() { // 字段 salary 配置了在"HR审批"节点 hidden。提交时若按 HR审批 节点校验,salary 缺失应跳过必填。 const string schema = """ { "type": "object", "properties": { "name": { "type": "string", "title": "姓名", "required": true, "x-component": "Input" }, "salary": { "type": "number", "title": "薪资", "required": true, "x-component": "InputNumber", "x-field-permission": { "HR审批": "hidden" } } } } """; // 不传节点:salary 是必填,缺失应报错 var noNode = FormDataValidator.Validate(schema, """{"name":"张三"}""", ["Input", "InputNumber"]); noNode.IsValid.Should().BeFalse("无节点上下文时不做权限过滤,salary 必填缺失应报错"); // 传 HR审批 节点:salary 在该节点 hidden,应跳过必填校验 → 通过 var hrNode = FormDataValidator.Validate( schema, """{"name":"张三"}""", ["Input", "InputNumber"], currentNodeKey: "HR审批"); hrNode.IsValid.Should().BeTrue(string.Join("; ", hrNode.Errors)); } [Fact] public void Validate_FieldVisibleForCurrentNode_StillEnforcesRequired() { // salary 在"直属主管审批"节点 visible(可见可编辑),缺失应报错 const string schema = """ { "type": "object", "properties": { "salary": { "type": "number", "title": "薪资", "required": true, "x-component": "InputNumber", "x-field-permission": { "直属主管审批": "visible", "HR审批": "hidden" } } } } """; var result = FormDataValidator.Validate( schema, """{}""", ["InputNumber"], currentNodeKey: "直属主管审批"); result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.Contains("薪资") && e.Contains("必填")); } [Fact] public void Validate_PermissionDefaultFallback_AppliesWhenNodeNotExplicitlyConfigured() { // salary 配了 __default__: hidden。当前节点"未知节点"未显式配置 → 走 default → 隐藏 → 跳过必填 const string schema = """ { "type": "object", "properties": { "salary": { "type": "number", "title": "薪资", "required": true, "x-component": "InputNumber", "x-field-permission": { "__default__": "hidden", "直属主管审批": "visible" } } } } """; // 直属主管审批:显式 visible → 必填生效 → 缺失报错 var manager = FormDataValidator.Validate( schema, """{}""", ["InputNumber"], currentNodeKey: "直属主管审批"); manager.IsValid.Should().BeFalse("直属主管审批节点 salary 可见,必填缺失应报错"); // 未知节点:走 __default__ = hidden → 跳过必填 → 通过 var unknown = FormDataValidator.Validate( schema, """{}""", ["InputNumber"], currentNodeKey: "未知节点"); unknown.IsValid.Should().BeTrue("未知节点走 __default__ hidden,应跳过必填"); } }