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();
}
}