using FluentAssertions; using Microsoft.EntityFrameworkCore; using Workflow.Application.Form.DTOs; using Workflow.Application.Form.FormData.Commands; using Workflow.Application.Form.FormData.Queries; using Workflow.Domain.Enums; using Workflow.Domain.Exceptions; using Workflow.Infrastructure.Persistence; using Xunit; namespace Workflow.Tests.Form; [Collection("FormTests")] public class FormDataTests { private readonly FormTestFixture _fixture; public FormDataTests(FormTestFixtureClassFixture fixture) { _fixture = fixture; } /// /// 辅助方法:创建一个已发布的表单定义并返回其 Id, /// 用于 FormData 测试的前置条件。 /// private static Domain.Entities.FormDefinition CreatePublishedFormDefinition(Guid formId) { var form = new Domain.Entities.FormDefinition { Id = formId, Name = "测试表单", Code = $"TEST_FORM_{formId:N}", Version = 1, Status = FormStatus.Published, SchemaJson = "{}", }; form.Fields.Add(new Domain.Entities.FormDefinitionField { Id = Guid.NewGuid(), FormDefinitionId = formId, FieldKey = "name", Label = "姓名", FieldType = FieldType.Input, Required = true, Config = "{}", SortOrder = 0, }); form.Fields.Add(new Domain.Entities.FormDefinitionField { Id = Guid.NewGuid(), FormDefinitionId = formId, FieldKey = "age", Label = "年龄", FieldType = FieldType.Number, Required = false, DefaultValue = "18", Config = """{"min":0,"max":150}""", SortOrder = 1, }); form.Fields.Add(new Domain.Entities.FormDefinitionField { Id = Guid.NewGuid(), FormDefinitionId = formId, FieldKey = "gender", Label = "性别", FieldType = FieldType.Radio, Required = true, Config = """{"options":["男","女"]}""", SortOrder = 2, }); form.Fields.Add(new Domain.Entities.FormDefinitionField { Id = Guid.NewGuid(), FormDefinitionId = formId, FieldKey = "resume", Label = "简历附件", FieldType = FieldType.FileUpload, Required = false, Config = """{"maxSize":10485760,"extensions":[".pdf",".doc",".docx"]}""", SortOrder = 3, }); return form; } // ==================================================================== // SubmitFormData // ==================================================================== [Fact] public async Task SubmitFormData_StoresDataJson() { // Arrange var formId = Guid.NewGuid(); var instanceId = Guid.NewGuid(); var dataJson = """{"name":"张三","age":25,"gender":"男"}"""; await using var db = await _fixture.CreateDbContextWithSeedAsync( testName: nameof(SubmitFormData_StoresDataJson), seedAction: ctx => ctx.FormDefinitions.Add(CreatePublishedFormDefinition(formId))); var handler = new SubmitFormDataCommandHandler(db); var command = new SubmitFormDataCommand( FormDefinitionId: formId, InstanceId: instanceId, DataJson: dataJson ); // Act var formDataId = await handler.Handle(command, CancellationToken.None); // Assert formDataId.Should().NotBe(Guid.Empty); var savedData = await db.FormData.FindAsync(formDataId); savedData.Should().NotBeNull(); savedData!.DataJson.Should().Be(dataJson); } [Fact] public async Task SubmitFormData_AssociatesWithFormDefinition() { // Arrange var formId = Guid.NewGuid(); var instanceId = Guid.NewGuid(); await using var db = await _fixture.CreateDbContextWithSeedAsync( testName: nameof(SubmitFormData_AssociatesWithFormDefinition), seedAction: ctx => ctx.FormDefinitions.Add(CreatePublishedFormDefinition(formId))); var handler = new SubmitFormDataCommandHandler(db); var command = new SubmitFormDataCommand( FormDefinitionId: formId, InstanceId: instanceId, DataJson: """{"name":"李四","age":30,"gender":"女"}""" ); // Act var formDataId = await handler.Handle(command, CancellationToken.None); // Assert var savedData = await db.FormData.FindAsync(formDataId); savedData.Should().NotBeNull(); savedData!.FormDefinitionId.Should().Be(formId); } [Fact] public async Task SubmitFormData_AssociatesWithWorkflowInstance() { // Arrange var formId = Guid.NewGuid(); var instanceId = Guid.NewGuid(); await using var db = await _fixture.CreateDbContextWithSeedAsync( testName: nameof(SubmitFormData_AssociatesWithWorkflowInstance), seedAction: ctx => ctx.FormDefinitions.Add(CreatePublishedFormDefinition(formId))); var handler = new SubmitFormDataCommandHandler(db); var command = new SubmitFormDataCommand( FormDefinitionId: formId, InstanceId: instanceId, DataJson: """{"name":"王五","age":28,"gender":"男"}""" ); // Act var formDataId = await handler.Handle(command, CancellationToken.None); // Assert var savedData = await db.FormData.FindAsync(formDataId); savedData.Should().NotBeNull(); savedData!.InstanceId.Should().Be(instanceId); } [Fact] public async Task SubmitFormData_RequiredFieldMissing_ThrowsBusinessException() { // Arrange var formId = Guid.NewGuid(); var instanceId = Guid.NewGuid(); // name 是必填字段,但 DataJson 中没有 name var dataJson = """{"age":25,"gender":"男"}"""; await using var db = await _fixture.CreateDbContextWithSeedAsync( testName: nameof(SubmitFormData_RequiredFieldMissing_ThrowsBusinessException), seedAction: ctx => ctx.FormDefinitions.Add(CreatePublishedFormDefinition(formId))); var handler = new SubmitFormDataCommandHandler(db); var command = new SubmitFormDataCommand( FormDefinitionId: formId, InstanceId: instanceId, DataJson: dataJson ); // Act var act = () => handler.Handle(command, CancellationToken.None); // Assert await act.Should().ThrowAsync() .WithMessage("*必填*name*"); // 验证数据未被写入 var count = await db.FormData.CountAsync(); count.Should().Be(0); } [Fact] public async Task SubmitFormData_InvalidFieldType_ThrowsBusinessException() { // Arrange var formId = Guid.NewGuid(); var instanceId = Guid.NewGuid(); // age 字段类型为 Number,但提交了非数字值 "abc" var dataJson = """{"name":"赵六","age":"abc","gender":"男"}"""; await using var db = await _fixture.CreateDbContextWithSeedAsync( testName: nameof(SubmitFormData_InvalidFieldType_ThrowsBusinessException), seedAction: ctx => ctx.FormDefinitions.Add(CreatePublishedFormDefinition(formId))); var handler = new SubmitFormDataCommandHandler(db); var command = new SubmitFormDataCommand( FormDefinitionId: formId, InstanceId: instanceId, DataJson: dataJson ); // Act var act = () => handler.Handle(command, CancellationToken.None); // Assert await act.Should().ThrowAsync() .WithMessage("*age*类型*"); // 验证数据未被写入 var count = await db.FormData.CountAsync(); count.Should().Be(0); } // ==================================================================== // GetFormDataByInstance // ==================================================================== [Fact] public async Task GetFormDataByInstance_ReturnsFormData() { // Arrange var formId = Guid.NewGuid(); var instanceId = Guid.NewGuid(); var formDataId = Guid.NewGuid(); var dataJson = """{"name":"钱七","age":35,"gender":"男"}"""; await using var db = await _fixture.CreateDbContextWithSeedAsync( testName: nameof(GetFormDataByInstance_ReturnsFormData), seedAction: ctx => { ctx.FormDefinitions.Add(CreatePublishedFormDefinition(formId)); ctx.FormData.Add(new Domain.Entities.FormData { Id = formDataId, FormDefinitionId = formId, InstanceId = instanceId, DataJson = dataJson, }); }); var handler = new GetFormDataByInstanceQueryHandler(db); var query = new GetFormDataByInstanceQuery(InstanceId: instanceId); // Act var result = await handler.Handle(query, CancellationToken.None); // Assert result.Should().NotBeNull(); result!.Id.Should().Be(formDataId); result.FormDefinitionId.Should().Be(formId); result.InstanceId.Should().Be(instanceId); result.DataJson.Should().Be(dataJson); } [Fact] public async Task GetFormDataByInstance_NotFound_ReturnsNull() { // Arrange await using var db = _fixture.CreateDbContext(testName: nameof(GetFormDataByInstance_NotFound_ReturnsNull)); var handler = new GetFormDataByInstanceQueryHandler(db); var query = new GetFormDataByInstanceQuery(InstanceId: Guid.NewGuid()); // Act var result = await handler.Handle(query, CancellationToken.None); // Assert result.Should().BeNull(); } }