work-flow/tests/Workflow.Tests/Form/FormDataValidatorTests.cs
向宁 9f878286e7 feat: form versioning, notification center, scheduler and webhooks
- Add FormDefinitionVersion with compare/versions endpoints and schema differ
- Add Notification entity, endpoints and application features
- Add Scheduler (timeout) and WebhookDispatcher services
- Add FormDataValidator/FieldPermissionEvaluator/ReactionEvaluator
- Add workflow task mark-read, CC support and SystemUserContext
- Add EF migrations for form versions and notifications
- Add unit tests for form schema, notifications, scheduler and serialization
2026-06-14 15:03:11 +08:00

325 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string> 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应跳过必填");
}
}