using BuildFlow.Shared.Domain.Exceptions;
using BuildFlow.Workflow.Tests;
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 Workflow.Infrastructure.Persistence;
using Xunit;
namespace BuildFlow.Workflow.Tests.BuildFlow.Shares;
///
/// [B-补测试] BuildFlow/Shares 子域全 Handler 覆盖。
/// 覆盖:CreateFormShare / DeleteFormShare / GetFormShareList / GetFormShareByToken / SubmitSharedForm。
/// 飞书矩阵原标记 workflow/BuildFlow/Shares 零覆盖 → 本次全补。
///
public class FormShareHandlerTests
{
private static async Task 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 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_ThrowsBusiness()
{
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);
await act.Should().ThrowAsync("表单定义不存在应拒绝");
}
[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_ThrowsBusiness()
{
await using var db = TestDbFactory.Create();
var handler = new DeleteFormShareCommandHandler(db);
var act = async () => await handler.Handle(
new DeleteFormShareCommand(Guid.NewGuid()), CancellationToken.None);
await act.Should().ThrowAsync();
}
[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_ThrowsBusiness()
{
await using var db = TestDbFactory.Create();
var act = async () => await new GetFormShareByTokenQueryHandler(db).Handle(
new GetFormShareByTokenQuery("nope", null), CancellationToken.None);
await act.Should().ThrowAsync();
}
[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("过期应拒绝");
}
[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("达到查看上限应拒绝");
}
[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("密码错误应拒绝");
}
[Fact]
public async Task Submit_TokenNotFound_ThrowsBusiness()
{
await using var db = TestDbFactory.Create();
var act = async () => await new SubmitSharedFormCommandHandler(db).Handle(
new SubmitSharedFormCommand("nope", "{}"), CancellationToken.None);
await act.Should().ThrowAsync();
}
[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("过期应拒绝");
}
[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("只读分享不允许提交");
}
// 有效的 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);
}
}