2026-07-24 12:47:09 +08:00

305 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using BuildFlow.Shared.Domain.Errors;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Workflow.Application.BuildFlow.Shares.Commands;
using Workflow.Application.BuildFlow.Shares.DTOs;
using Workflow.Application.BuildFlow.Shares.Handlers;
using Workflow.Application.BuildFlow.Shares.Queries;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Xunit;
namespace BuildFlow.Workflow.Tests.BuildFlow.Shares;
/// <summary>
/// [B-补测试] BuildFlow/Shares 子域全 Handler 覆盖。
/// 覆盖:CreateFormShare / DeleteFormShare / GetFormShareList / GetFormShareByToken / SubmitSharedForm。
/// 飞书矩阵原标记 workflow/BuildFlow/Shares 零覆盖 → 本次全补。
/// </summary>
public class FormShareHandlerTests
{
private static async Task<Guid> SeedFormDefinition(WorkflowDbContext db, string schema = "{}")
{
var form = new FormDefinition
{
Id = Guid.NewGuid(),
Name = "测试表单",
Code = "test-form",
SchemaJson = schema,
};
db.FormDefinitions.Add(form);
await db.SaveChangesAsync();
return form.Id;
}
private static async Task<FormShareDto> SeedShare(
WorkflowDbContext db, Guid formId, BuildFlowSharePermission permission = BuildFlowSharePermission.View,
string? password = null, DateTime? expiresAt = null, int? maxViews = null)
{
var handler = new CreateFormShareCommandHandler(db);
return await handler.Handle(
new CreateFormShareCommand(formId, permission, true, password, expiresAt, maxViews, "备注"),
CancellationToken.None);
}
[Fact]
public async Task Create_Valid_PersistsAndReturnsToken()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db);
var result = await new CreateFormShareCommandHandler(db).Handle(
new CreateFormShareCommand(formId, BuildFlowSharePermission.Edit, true, null, null, null, "对外收集"),
CancellationToken.None);
result.Token.Should().MatchRegex("^[0-9a-f]{32}$", "token 应为 32 位 hex");
result.Permission.Should().Be(BuildFlowSharePermission.Edit);
result.Remark.Should().Be("对外收集");
(await db.BuildFlowFormShares.CountAsync()).Should().Be(1);
}
[Fact]
public async Task Create_FormNotFound_ThrowsNotFound()
{
await using var db = TestDbFactory.Create();
var handler = new CreateFormShareCommandHandler(db);
var act = async () => await handler.Handle(
new CreateFormShareCommand(Guid.NewGuid(), BuildFlowSharePermission.View, true, null, null, null, null),
CancellationToken.None);
var exception = await act.Should().ThrowAsync<NotFoundException>("表单定义不存在应返回未找到语义");
exception.Which.Code.Should().Be(ErrorCode.NotFound);
}
[Fact]
public async Task Create_WithPassword_HashesPassword()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db);
var result = await new CreateFormShareCommandHandler(db).Handle(
new CreateFormShareCommand(formId, BuildFlowSharePermission.View, true, "secret123", null, null, null),
CancellationToken.None);
var entity = await db.BuildFlowFormShares.SingleAsync(s => s.Id == result.Id);
entity.Password.Should().NotBe("secret123", "明文密码不得入库");
entity.Password.Should().StartWith("$2", "应为 bcrypt 哈希");
}
[Fact]
public async Task Delete_Existing_Removes()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db);
var share = await SeedShare(db, formId);
await new DeleteFormShareCommandHandler(db).Handle(
new DeleteFormShareCommand(share.Id), CancellationToken.None);
(await db.BuildFlowFormShares.IgnoreQueryFilters().CountAsync(s => s.Id == share.Id))
.Should().Be(0, "物理删除后不存在");
}
[Fact]
public async Task Delete_NotFound_ThrowsNotFound()
{
await using var db = TestDbFactory.Create();
var handler = new DeleteFormShareCommandHandler(db);
var act = async () => await handler.Handle(
new DeleteFormShareCommand(Guid.NewGuid()), CancellationToken.None);
var exception = await act.Should().ThrowAsync<NotFoundException>();
exception.Which.Code.Should().Be(ErrorCode.NotFound);
}
[Fact]
public async Task GetList_ReturnsAll()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db);
await SeedShare(db, formId);
await SeedShare(db, formId);
var result = await new GetFormShareListQueryHandler(db).Handle(
new GetFormShareListQuery(null, null, 1, 20), CancellationToken.None);
result.Total.Should().Be(2);
result.Items.Should().HaveCount(2);
}
[Fact]
public async Task GetList_FiltersByFormDefinitionId()
{
await using var db = TestDbFactory.Create();
var formA = await SeedFormDefinition(db);
var formB = await SeedFormDefinition(db);
await SeedShare(db, formA);
await SeedShare(db, formB);
var result = await new GetFormShareListQueryHandler(db).Handle(
new GetFormShareListQuery(formA, null, 1, 20), CancellationToken.None);
result.Total.Should().Be(1);
result.Items.Should().ContainSingle(x => x.FormDefinitionId == formA);
}
[Fact]
public async Task GetByToken_Valid_IncrementsViewCountAndReturnsForm()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db, """{"type":"form"}""");
var share = await SeedShare(db, formId, BuildFlowSharePermission.View);
var result = await new GetFormShareByTokenQueryHandler(db).Handle(
new GetFormShareByTokenQuery(share.Token, null), CancellationToken.None);
result.FormDefinitionId.Should().Be(formId);
result.FormName.Should().Be("测试表单");
result.SchemaJson.Should().Be("""{"type":"form"}""");
var entity = await db.BuildFlowFormShares.SingleAsync(s => s.Id == share.Id);
entity.ViewCount.Should().Be(1, "查看次数应自增");
}
[Fact]
public async Task GetByToken_NotFound_ThrowsNotFound()
{
await using var db = TestDbFactory.Create();
var act = async () => await new GetFormShareByTokenQueryHandler(db).Handle(
new GetFormShareByTokenQuery("nope", null), CancellationToken.None);
var exception = await act.Should().ThrowAsync<NotFoundException>();
exception.Which.Code.Should().Be(ErrorCode.NotFound);
}
[Fact]
public async Task GetByToken_AssociatedFormNotFound_ThrowsNotFound()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db);
var share = await SeedShare(db, formId);
var form = await db.FormDefinitions.SingleAsync(x => x.Id == formId);
form.IsDeleted = true;
await db.SaveChangesAsync();
var act = async () => await new GetFormShareByTokenQueryHandler(db).Handle(
new GetFormShareByTokenQuery(share.Token, null), CancellationToken.None);
var exception = await act.Should().ThrowAsync<NotFoundException>();
exception.Which.Code.Should().Be(ErrorCode.NotFound);
}
[Fact]
public async Task GetByToken_Expired_ThrowsBusiness()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db);
var share = await SeedShare(db, formId, expiresAt: DateTime.UtcNow.AddMinutes(-1));
var act = async () => await new GetFormShareByTokenQueryHandler(db).Handle(
new GetFormShareByTokenQuery(share.Token, null), CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>("过期应拒绝");
}
[Fact]
public async Task GetByToken_MaxViewsReached_ThrowsBusiness()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db);
var share = await SeedShare(db, formId, maxViews: 1);
// 先消费一次额度
await new GetFormShareByTokenQueryHandler(db).Handle(
new GetFormShareByTokenQuery(share.Token, null), CancellationToken.None);
var act = async () => await new GetFormShareByTokenQueryHandler(db).Handle(
new GetFormShareByTokenQuery(share.Token, null), CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>("达到查看上限应拒绝");
}
[Fact]
public async Task GetByToken_PasswordProtected_CorrectPassword_Returns()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db);
var share = await SeedShare(db, formId, password: "secret123");
var result = await new GetFormShareByTokenQueryHandler(db).Handle(
new GetFormShareByTokenQuery(share.Token, "secret123"), CancellationToken.None);
result.FormDefinitionId.Should().Be(formId);
}
[Fact]
public async Task GetByToken_PasswordProtected_WrongPassword_ThrowsBusiness()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db);
var share = await SeedShare(db, formId, password: "secret123");
var act = async () => await new GetFormShareByTokenQueryHandler(db).Handle(
new GetFormShareByTokenQuery(share.Token, "wrong"), CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>("密码错误应拒绝");
}
[Fact]
public async Task Submit_TokenNotFound_ThrowsNotFound()
{
await using var db = TestDbFactory.Create();
var act = async () => await new SubmitSharedFormCommandHandler(db).Handle(
new SubmitSharedFormCommand("nope", "{}"), CancellationToken.None);
var exception = await act.Should().ThrowAsync<NotFoundException>();
exception.Which.Code.Should().Be(ErrorCode.NotFound);
}
[Fact]
public async Task Submit_Expired_ThrowsBusiness()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db);
var share = await SeedShare(db, formId, BuildFlowSharePermission.Edit,
expiresAt: DateTime.UtcNow.AddMinutes(-1));
var act = async () => await new SubmitSharedFormCommandHandler(db).Handle(
new SubmitSharedFormCommand(share.Token, "{}"), CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>("过期应拒绝");
}
[Fact]
public async Task Submit_ReadOnlyPermission_ThrowsBusiness()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db);
var share = await SeedShare(db, formId, BuildFlowSharePermission.View);
var act = async () => await new SubmitSharedFormCommandHandler(db).Handle(
new SubmitSharedFormCommand(share.Token, "{}"), CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>("只读分享不允许提交");
}
// 有效的 Formily Schema:顶层 object + properties,足以通过 SchemaValidator 与 FormDataValidator。
private const string ValidFormSchema = "{\"type\":\"object\",\"properties\":{}}";
[Fact]
public async Task Submit_EditPermission_CreatesFormData()
{
await using var db = TestDbFactory.Create();
var formId = await SeedFormDefinition(db, ValidFormSchema);
var share = await SeedShare(db, formId, BuildFlowSharePermission.Edit);
var instanceId = await new SubmitSharedFormCommandHandler(db).Handle(
new SubmitSharedFormCommand(share.Token, "{}"), CancellationToken.None);
instanceId.Should().NotBeEmpty();
(await db.FormData.CountAsync()).Should().Be(1);
}
}