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 { 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() .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 { 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 { 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 { 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() .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() .WithMessage("*表单*不存在*"); } }