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; } private const string ValidSchema = """ { "type": "object", "properties": { "name": { "type": "string", "title": "姓名", "required": true, "x-decorator": "FormItem", "x-component": "Input" } } } """; // ==================================================================== // CreateFormDefinition // ==================================================================== [Fact] public async Task CreateFormDefinition_WithValidSchema_ReturnsDto() { await using var db = _fixture.CreateDbContext(); var handler = new CreateFormDefinitionCommandHandler(db); var command = new CreateFormDefinitionCommand( Name: "员工入职登记表", Code: "EMP_ONBOARD", Description: "新员工入职时填写的登记表单", SchemaJson: ValidSchema ); var result = await handler.Handle(command, CancellationToken.None); result.Should().NotBeNull(); result.Name.Should().Be("员工入职登记表"); result.Code.Should().Be("EMP_ONBOARD"); result.Version.Should().Be(1); result.Status.Should().Be(FormStatus.Draft); var entity = await db.FormDefinitions.FirstAsync(); entity.SchemaJson.Should().Contain("name"); } [Fact] public async Task CreateFormDefinition_WithDuplicateCode_ThrowsBusinessException() { 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 = ValidSchema, }); }); var handler = new CreateFormDefinitionCommandHandler(db); var command = new CreateFormDefinitionCommand( Name: "新表单", Code: "DUPLICATE_CODE", Description: null, SchemaJson: ValidSchema ); var act = () => handler.Handle(command, CancellationToken.None); await act.Should().ThrowAsync() .WithMessage("*编码*已存在*"); } [Fact] public async Task CreateFormDefinition_WithInvalidSchema_ThrowsBusinessException() { await using var db = _fixture.CreateDbContext(); var handler = new CreateFormDefinitionCommandHandler(db); var command = new CreateFormDefinitionCommand( Name: "无效表单", Code: "INVALID_SCHEMA", Description: null, SchemaJson: """{ "type": "string" }""" ); var act = () => handler.Handle(command, CancellationToken.None); await act.Should().ThrowAsync() .WithMessage("*Schema 校验失败*"); } // ==================================================================== // UpdateFormDefinition // ==================================================================== [Fact] public async Task UpdateFormDefinition_UpdatesNameAndIncrementsVersion() { var formId = Guid.NewGuid(); await using var db = await _fixture.CreateDbContextWithSeedAsync( testName: nameof(UpdateFormDefinition_UpdatesNameAndIncrementsVersion), seedAction: db => { db.FormDefinitions.Add(new Domain.Entities.FormDefinition { Id = formId, Name = "旧名称", Code = "FORM_001", Version = 1, Description = "旧描述", Status = FormStatus.Draft, SchemaJson = ValidSchema, }); }); var handler = new UpdateFormDefinitionCommandHandler(db); var command = new UpdateFormDefinitionCommand( Id: formId, Name: "新名称", Description: "新描述", SchemaJson: ValidSchema ); var result = await handler.Handle(command, CancellationToken.None); result.Should().NotBeNull(); result.Name.Should().Be("新名称"); result.Version.Should().Be(2); var entity = await db.FormDefinitions.FindAsync(formId); entity.Should().NotBeNull(); entity!.Version.Should().Be(2); entity.Code.Should().Be("FORM_001"); } // ==================================================================== // DeleteFormDefinition (soft delete) // ==================================================================== [Fact] public async Task DeleteFormDefinition_SetsIsDeletedTrue() { 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 = ValidSchema, }); }); var handler = new DeleteFormDefinitionCommandHandler(db); await handler.Handle(new DeleteFormDefinitionCommand(Id: formId), CancellationToken.None); var normalQuery = await db.FormDefinitions.FirstOrDefaultAsync(f => f.Id == formId); normalQuery.Should().BeNull(); var deletedEntity = await db.FormDefinitions .IgnoreQueryFilters() .FirstAsync(f => f.Id == formId); deletedEntity.IsDeleted.Should().BeTrue(); } // ==================================================================== // PublishFormDefinition // ==================================================================== [Fact] public async Task PublishFormDefinition_ChangesStatusFromDraftToPublished() { 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 = ValidSchema, }); }); var handler = new PublishFormDefinitionCommandHandler(db); await handler.Handle(new PublishFormDefinitionCommand(Id: formId), CancellationToken.None); var entity = await db.FormDefinitions.FindAsync(formId); entity.Should().NotBeNull(); entity!.Status.Should().Be(FormStatus.Published); } [Fact] public async Task PublishFormDefinition_AlreadyPublished_ThrowsBusinessException() { 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 = ValidSchema, }); }); var handler = new PublishFormDefinitionCommandHandler(db); var act = () => handler.Handle(new PublishFormDefinitionCommand(Id: formId), CancellationToken.None); await act.Should().ThrowAsync() .WithMessage("*已发布*"); } // ==================================================================== // DisableFormDefinition // ==================================================================== [Fact] public async Task DisableFormDefinition_ChangesStatusToDisabled() { 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 = ValidSchema, }); }); var handler = new DisableFormDefinitionCommandHandler(db); await handler.Handle(new DisableFormDefinitionCommand(Id: formId), CancellationToken.None); var entity = await db.FormDefinitions.FindAsync(formId); entity.Should().NotBeNull(); entity!.Status.Should().Be(FormStatus.Disabled); } // ==================================================================== // GetFormDefinitionList // ==================================================================== [Fact] public async Task GetFormDefinitionList_ReturnsPagedResults() { 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 = ValidSchema, }); } }); var handler = new GetFormDefinitionListQueryHandler(db); var result = await handler.Handle( new GetFormDefinitionListQuery(PageIndex: 1, PageSize: 10, Status: null), CancellationToken.None); result.Items.Should().HaveCount(10); result.TotalCount.Should().Be(15); } // ==================================================================== // GetFormDefinitionById // ==================================================================== [Fact] public async Task GetFormDefinitionById_ReturnsSchemaJson() { var formId = Guid.NewGuid(); await using var db = await _fixture.CreateDbContextWithSeedAsync( testName: nameof(GetFormDefinitionById_ReturnsSchemaJson), seedAction: ctx => { ctx.FormDefinitions.Add(new Domain.Entities.FormDefinition { Id = formId, Name = "带 Schema 的表单", Code = "WITH_SCHEMA", Version = 1, Description = "包含 Formily Schema 的表单", Status = FormStatus.Draft, SchemaJson = ValidSchema, }); }); var handler = new GetFormDefinitionByIdQueryHandler(db); var result = await handler.Handle(new GetFormDefinitionByIdQuery(Id: formId), CancellationToken.None); result.Should().NotBeNull(); result.SchemaJson.Should().NotBeNullOrEmpty(); result.SchemaJson.Should().Contain("name"); } [Fact] public async Task GetFormDefinitionById_NotFound_ThrowsNotFoundException() { await using var db = _fixture.CreateDbContext(testName: nameof(GetFormDefinitionById_NotFound_ThrowsNotFoundException)); var handler = new GetFormDefinitionByIdQueryHandler(db); var act = () => handler.Handle(new GetFormDefinitionByIdQuery(Id: Guid.NewGuid()), CancellationToken.None); await act.Should().ThrowAsync() .WithMessage("*表单*不存在*"); } }