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