294 lines
9.9 KiB
C#
294 lines
9.9 KiB
C#
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 辅助方法:创建一个已发布的表单定义并返回其 Id,
|
||
/// 用于 FormData 测试的前置条件。
|
||
/// </summary>
|
||
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.FormDatas.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.FormDatas.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.FormDatas.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<BusinessException>()
|
||
.WithMessage("*必填*name*");
|
||
|
||
// 验证数据未被写入
|
||
var count = await db.FormDatas.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<BusinessException>()
|
||
.WithMessage("*age*类型*");
|
||
|
||
// 验证数据未被写入
|
||
var count = await db.FormDatas.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.FormDatas.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();
|
||
}
|
||
}
|