work-flow/tests/Workflow.Tests/Form/FormDefinitionTests.cs
向宁 fc4ecbbacc feat: add gRPC auth, condition comparators, seed data, EF migrations
- gRPC auth service for token validation
- Value comparator system (string, numeric, boolean, datetime, collection)
- Condition evaluator with strategy chain
- Form definition and data improvements
- Workflow instance/task endpoints updated
- Seed data and EF design-time factory
- Test coverage for comparators and handlers
2026-05-20 20:28:35 +08:00

665 lines
24 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 Microsoft.EntityFrameworkCore;
using Workflow.Application.Form.DTOs;
using Workflow.Application.Form.FormDefinition.Commands;
using Workflow.Application.Form.FormDefinition.Queries;
using Workflow.Domain.Enums;
using Workflow.Domain.Exceptions;
using Workflow.Infrastructure.Persistence;
using Xunit;
namespace Workflow.Tests.Form;
[Collection("FormTests")]
public class FormDefinitionTests
{
private readonly FormTestFixture _fixture;
public FormDefinitionTests(FormTestFixtureClassFixture fixture)
{
_fixture = fixture;
}
// ====================================================================
// CreateFormDefinition
// ====================================================================
[Fact]
public async Task CreateFormDefinition_WithValidData_ReturnsDto()
{
// Arrange
await using var db = _fixture.CreateDbContext();
var handler = new CreateFormDefinitionCommandHandler(db);
var command = new CreateFormDefinitionCommand(
Name: "员工入职登记表",
Code: "EMP_ONBOARD",
Description: "新员工入职时填写的登记表单",
Fields: new List<FormFieldDto>
{
new(FieldKey: "name", Label: "姓名", FieldType: FieldType.Input, Required: true, DefaultValue: null, Config: "{}", SortOrder: 0),
new(FieldKey: "department", Label: "部门", FieldType: FieldType.Select, Required: true, DefaultValue: null, Config: """{"options":["","",""]}""", SortOrder: 1),
new(FieldKey: "entry_date", Label: "入职日期", FieldType: FieldType.Date, Required: true, DefaultValue: null, Config: "{}", SortOrder: 2),
}
);
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("员工入职登记表");
result.Code.Should().Be("EMP_ONBOARD");
result.Description.Should().Be("新员工入职时填写的登记表单");
result.Version.Should().Be(1);
result.Status.Should().Be(FormStatus.Draft);
// 验证持久化
var entity = await db.FormDefinitions.FirstAsync();
entity.Name.Should().Be("员工入职登记表");
entity.Code.Should().Be("EMP_ONBOARD");
}
[Fact]
public async Task CreateFormDefinition_WithDuplicateCode_ThrowsBusinessException()
{
// Arrange
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(CreateFormDefinition_WithDuplicateCode_ThrowsBusinessException),
seedAction: db =>
{
db.FormDefinitions.Add(new Domain.Entities.FormDefinition
{
Id = Guid.NewGuid(),
Name = "已存在的表单",
Code = "DUPLICATE_CODE",
Version = 1,
Status = FormStatus.Draft,
SchemaJson = "{}",
});
});
var handler = new CreateFormDefinitionCommandHandler(db);
var command = new CreateFormDefinitionCommand(
Name: "新表单",
Code: "DUPLICATE_CODE",
Description: "尝试使用重复编码",
Fields: []
);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
// Assert
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("*编码*已存在*");
}
// ====================================================================
// UpdateFormDefinition
// ====================================================================
[Fact]
public async Task UpdateFormDefinition_UpdatesNameAndDescription()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(UpdateFormDefinition_UpdatesNameAndDescription),
seedAction: db =>
{
db.FormDefinitions.Add(new Domain.Entities.FormDefinition
{
Id = formId,
Name = "旧名称",
Code = "FORM_001",
Version = 1,
Description = "旧描述",
Status = FormStatus.Draft,
SchemaJson = "{}",
});
});
var handler = new UpdateFormDefinitionCommandHandler(db);
var command = new UpdateFormDefinitionCommand(
Id: formId,
Name: "新名称",
Description: "新描述",
Fields: []
);
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("新名称");
result.Description.Should().Be("新描述");
// 验证持久化
var entity = await db.FormDefinitions.FindAsync(formId);
entity.Should().NotBeNull();
entity!.Name.Should().Be("新名称");
entity.Description.Should().Be("新描述");
// Code 不应被修改
entity.Code.Should().Be("FORM_001");
}
[Fact]
public async Task UpdateFormDefinition_AddsFieldsToForm()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(UpdateFormDefinition_AddsFieldsToForm),
seedAction: db =>
{
db.FormDefinitions.Add(new Domain.Entities.FormDefinition
{
Id = formId,
Name = "表单",
Code = "FORM_ADD_FIELDS",
Version = 1,
Status = FormStatus.Draft,
SchemaJson = "{}",
});
});
var handler = new UpdateFormDefinitionCommandHandler(db);
var command = new UpdateFormDefinitionCommand(
Id: formId,
Name: "表单",
Description: null,
Fields: new List<FormFieldDto>
{
new(FieldKey: "email", Label: "邮箱", FieldType: FieldType.Input, Required: false, DefaultValue: null, Config: "{}", SortOrder: 0),
new(FieldKey: "phone", Label: "电话", FieldType: FieldType.Input, Required: true, DefaultValue: null, Config: "{}", SortOrder: 1),
}
);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert
var form = await db.FormDefinitions
.Include(f => f.Fields)
.FirstAsync(f => f.Id == formId);
form.Fields.Should().HaveCount(2);
form.Fields.Select(f => f.FieldKey).Should().Contain(["email", "phone"]);
}
[Fact]
public async Task UpdateFormDefinition_RemovesFieldsFromForm()
{
// Arrange
var formId = Guid.NewGuid();
var fieldId1 = Guid.NewGuid();
var fieldId2 = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(UpdateFormDefinition_RemovesFieldsFromForm),
seedAction: db =>
{
var form = new Domain.Entities.FormDefinition
{
Id = formId,
Name = "表单",
Code = "FORM_REM_FIELDS",
Version = 1,
Status = FormStatus.Draft,
SchemaJson = "{}",
};
form.Fields.Add(new Domain.Entities.FormDefinitionField
{
Id = fieldId1,
FormDefinitionId = formId,
FieldKey = "keep_me",
Label = "保留字段",
FieldType = FieldType.Input,
Required = false,
Config = "{}",
SortOrder = 0,
});
form.Fields.Add(new Domain.Entities.FormDefinitionField
{
Id = fieldId2,
FormDefinitionId = formId,
FieldKey = "remove_me",
Label = "删除字段",
FieldType = FieldType.Input,
Required = false,
Config = "{}",
SortOrder = 1,
});
db.FormDefinitions.Add(form);
});
var handler = new UpdateFormDefinitionCommandHandler(db);
// 只传保留的字段remove_me 不在列表中
var command = new UpdateFormDefinitionCommand(
Id: formId,
Name: "表单",
Description: null,
Fields: new List<FormFieldDto>
{
new(FieldKey: "keep_me", Label: "保留字段", FieldType: FieldType.Input, Required: false, DefaultValue: null, Config: "{}", SortOrder: 0),
}
);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert
var form = await db.FormDefinitions
.Include(f => f.Fields)
.FirstAsync(f => f.Id == formId);
form.Fields.Should().HaveCount(1);
form.Fields[0].FieldKey.Should().Be("keep_me");
}
[Fact]
public async Task UpdateFormDefinition_ModifiesFieldProperties()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(UpdateFormDefinition_ModifiesFieldProperties),
seedAction: db =>
{
var form = new Domain.Entities.FormDefinition
{
Id = formId,
Name = "表单",
Code = "FORM_MOD_FIELDS",
Version = 1,
Status = FormStatus.Draft,
SchemaJson = "{}",
};
form.Fields.Add(new Domain.Entities.FormDefinitionField
{
Id = Guid.NewGuid(),
FormDefinitionId = formId,
FieldKey = "age",
Label = "年龄",
FieldType = FieldType.Input,
Required = false,
Config = "{}",
SortOrder = 0,
});
db.FormDefinitions.Add(form);
});
var handler = new UpdateFormDefinitionCommandHandler(db);
// 修改字段:从 Input 改为 Number从非必填改为必填标签从 "年龄" 改为 "用户年龄"
var command = new UpdateFormDefinitionCommand(
Id: formId,
Name: "表单",
Description: null,
Fields: new List<FormFieldDto>
{
new(FieldKey: "age", Label: "用户年龄", FieldType: FieldType.Number, Required: true, DefaultValue: "18", Config: """{"min":0,"max":150}""", SortOrder: 0),
}
);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert
var form = await db.FormDefinitions
.Include(f => f.Fields)
.FirstAsync(f => f.Id == formId);
form.Fields.Should().HaveCount(1);
var field = form.Fields[0];
field.Label.Should().Be("用户年龄");
field.FieldType.Should().Be(FieldType.Number);
field.Required.Should().BeTrue();
field.DefaultValue.Should().Be("18");
field.Config.Should().Contain("min");
}
// ====================================================================
// DeleteFormDefinition (soft delete)
// ====================================================================
[Fact]
public async Task DeleteFormDefinition_SetsIsDeletedTrue()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(DeleteFormDefinition_SetsIsDeletedTrue),
seedAction: ctx =>
{
ctx.FormDefinitions.Add(new Domain.Entities.FormDefinition
{
Id = formId,
Name = "待删除表单",
Code = "DEL_FORM",
Version = 1,
Status = FormStatus.Draft,
SchemaJson = "{}",
});
});
var handler = new DeleteFormDefinitionCommandHandler(db);
var command = new DeleteFormDefinitionCommand(Id: formId);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert — 软删除后通过默认查询应找不到(使用 LINQ 而非 FindAsync 以尊重查询过滤器)
var normalQuery = await db.FormDefinitions.FirstOrDefaultAsync(f => f.Id == formId);
normalQuery.Should().BeNull();
// 通过 IgnoreQueryFilters 可以找到,且 IsDeleted = true
var deletedEntity = await db.FormDefinitions
.IgnoreQueryFilters()
.FirstAsync(f => f.Id == formId);
deletedEntity.IsDeleted.Should().BeTrue();
}
[Fact]
public async Task DeleteFormDefinition_DoesNotHardDelete()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(DeleteFormDefinition_DoesNotHardDelete),
seedAction: ctx =>
{
ctx.FormDefinitions.Add(new Domain.Entities.FormDefinition
{
Id = formId,
Name = "待删除表单",
Code = "NO_HARD_DEL",
Version = 1,
Status = FormStatus.Draft,
SchemaJson = "{}",
});
});
var handler = new DeleteFormDefinitionCommandHandler(db);
var command = new DeleteFormDefinitionCommand(Id: formId);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert — 物理记录仍然存在于数据库中
var records = await db.FormDefinitions.IgnoreQueryFilters().CountAsync(f => f.Id == formId);
records.Should().Be(1);
}
// ====================================================================
// PublishFormDefinition
// ====================================================================
[Fact]
public async Task PublishFormDefinition_ChangesStatusFromDraftToPublished()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(PublishFormDefinition_ChangesStatusFromDraftToPublished),
seedAction: ctx =>
{
ctx.FormDefinitions.Add(new Domain.Entities.FormDefinition
{
Id = formId,
Name = "待发布表单",
Code = "PUB_FORM",
Version = 1,
Status = FormStatus.Draft,
SchemaJson = "{}",
});
});
var handler = new PublishFormDefinitionCommandHandler(db);
var command = new PublishFormDefinitionCommand(Id: formId);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert
var entity = await db.FormDefinitions.FindAsync(formId);
entity.Should().NotBeNull();
entity!.Status.Should().Be(FormStatus.Published);
}
[Fact]
public async Task PublishFormDefinition_AlreadyPublished_ThrowsBusinessException()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(PublishFormDefinition_AlreadyPublished_ThrowsBusinessException),
seedAction: ctx =>
{
ctx.FormDefinitions.Add(new Domain.Entities.FormDefinition
{
Id = formId,
Name = "已发布表单",
Code = "PUB_AGAIN",
Version = 1,
Status = FormStatus.Published,
SchemaJson = "{}",
});
});
var handler = new PublishFormDefinitionCommandHandler(db);
var command = new PublishFormDefinitionCommand(Id: formId);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
// Assert
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("*已发布*");
}
// ====================================================================
// DisableFormDefinition
// ====================================================================
[Fact]
public async Task DisableFormDefinition_ChangesStatusToDisabled()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(DisableFormDefinition_ChangesStatusToDisabled),
seedAction: ctx =>
{
ctx.FormDefinitions.Add(new Domain.Entities.FormDefinition
{
Id = formId,
Name = "待禁用表单",
Code = "DIS_FORM",
Version = 1,
Status = FormStatus.Published,
SchemaJson = "{}",
});
});
var handler = new DisableFormDefinitionCommandHandler(db);
var command = new DisableFormDefinitionCommand(Id: formId);
// Act
await handler.Handle(command, CancellationToken.None);
// Assert
var entity = await db.FormDefinitions.FindAsync(formId);
entity.Should().NotBeNull();
entity!.Status.Should().Be(FormStatus.Disabled);
}
// ====================================================================
// GetFormDefinitionList
// ====================================================================
[Fact]
public async Task GetFormDefinitionList_ReturnsPagedResults()
{
// Arrange
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(GetFormDefinitionList_ReturnsPagedResults),
seedAction: ctx =>
{
for (int i = 1; i <= 15; i++)
{
ctx.FormDefinitions.Add(new Domain.Entities.FormDefinition
{
Id = Guid.NewGuid(),
Name = $"表单_{i:D3}",
Code = $"FORM_{i:D3}",
Version = 1,
Status = FormStatus.Draft,
SchemaJson = "{}",
});
}
});
var handler = new GetFormDefinitionListQueryHandler(db);
var query = new GetFormDefinitionListQuery(PageIndex: 1, PageSize: 10, Status: null);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Items.Should().HaveCount(10);
result.TotalCount.Should().Be(15);
result.PageIndex.Should().Be(1);
result.PageSize.Should().Be(10);
}
[Fact]
public async Task GetFormDefinitionList_FiltersByStatus()
{
// Arrange
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(GetFormDefinitionList_FiltersByStatus),
seedAction: ctx =>
{
ctx.FormDefinitions.Add(new Domain.Entities.FormDefinition
{
Id = Guid.NewGuid(),
Name = "草稿表单",
Code = "DRAFT_001",
Version = 1,
Status = FormStatus.Draft,
SchemaJson = "{}",
});
ctx.FormDefinitions.Add(new Domain.Entities.FormDefinition
{
Id = Guid.NewGuid(),
Name = "已发布表单A",
Code = "PUB_001",
Version = 1,
Status = FormStatus.Published,
SchemaJson = "{}",
});
ctx.FormDefinitions.Add(new Domain.Entities.FormDefinition
{
Id = Guid.NewGuid(),
Name = "已发布表单B",
Code = "PUB_002",
Version = 1,
Status = FormStatus.Published,
SchemaJson = "{}",
});
});
var handler = new GetFormDefinitionListQueryHandler(db);
var query = new GetFormDefinitionListQuery(PageIndex: 1, PageSize: 10, Status: FormStatus.Published);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Items.Should().HaveCount(2);
result.Items.Should().OnlyContain(dto => dto.Status == FormStatus.Published);
}
// ====================================================================
// GetFormDefinitionById
// ====================================================================
[Fact]
public async Task GetFormDefinitionById_ReturnsDefinitionWithFields()
{
// Arrange
var formId = Guid.NewGuid();
await using var db = await _fixture.CreateDbContextWithSeedAsync(
testName: nameof(GetFormDefinitionById_ReturnsDefinitionWithFields),
seedAction: ctx =>
{
var form = new Domain.Entities.FormDefinition
{
Id = formId,
Name = "带字段的表单",
Code = "WITH_FIELDS",
Version = 1,
Description = "包含多个字段的表单",
Status = FormStatus.Draft,
SchemaJson = "{}",
};
form.Fields.Add(new Domain.Entities.FormDefinitionField
{
Id = Guid.NewGuid(),
FormDefinitionId = formId,
FieldKey = "title",
Label = "标题",
FieldType = FieldType.Input,
Required = true,
Config = "{}",
SortOrder = 0,
});
form.Fields.Add(new Domain.Entities.FormDefinitionField
{
Id = Guid.NewGuid(),
FormDefinitionId = formId,
FieldKey = "content",
Label = "内容",
FieldType = FieldType.RichText,
Required = false,
Config = "{}",
SortOrder = 1,
});
ctx.FormDefinitions.Add(form);
});
var handler = new GetFormDefinitionByIdQueryHandler(db);
var query = new GetFormDefinitionByIdQuery(Id: formId);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Id.Should().Be(formId);
result.Name.Should().Be("带字段的表单");
result.Code.Should().Be("WITH_FIELDS");
result.Fields.Should().NotBeNull();
result.Fields.Should().HaveCount(2);
result.Fields.Select(f => f.FieldKey).Should().Contain(["title", "content"]);
}
[Fact]
public async Task GetFormDefinitionById_NotFound_ThrowsNotFoundException()
{
// Arrange
await using var db = _fixture.CreateDbContext(testName: nameof(GetFormDefinitionById_NotFound_ThrowsNotFoundException));
var handler = new GetFormDefinitionByIdQueryHandler(db);
var query = new GetFormDefinitionByIdQuery(Id: Guid.NewGuid());
// Act
var act = () => handler.Handle(query, CancellationToken.None);
// Assert
await act.Should().ThrowAsync<NotFoundException>()
.WithMessage("*表单*不存在*");
}
}