- 将 AlertRule 和 ExternalSystem 实体改为使用 JsonData 存储业务字段 - 移除独立的 ExternalSystemCredential 表,凭证信息合并到 ExternalSystem.JsonData - 添加强类型视图 Data 属性用于访问 JsonData 中的业务数据 - 更新数据库配置将字段存储改为 jsonb 类型并调整索引策略 - 重构告警评估器使用 JsonData 强类型视图读取规则数据 - 更新连接器凭证解析器从 JsonData 中获取凭证信息 - 在 appsettings.json 中添加 monitoring 模块配置 - 将 monitoring 模块集成到主应用程序中并注册服务 - 更新相关处理器使用新的数据模型结构
283 lines
11 KiB
C#
283 lines
11 KiB
C#
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_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<BusinessException>("表单定义不存在应拒绝");
|
||
}
|
||
|
||
[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<BusinessException>();
|
||
}
|
||
|
||
[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<BusinessException>();
|
||
}
|
||
|
||
[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_ThrowsBusiness()
|
||
{
|
||
await using var db = TestDbFactory.Create();
|
||
|
||
var act = async () => await new SubmitSharedFormCommandHandler(db).Handle(
|
||
new SubmitSharedFormCommand("nope", "{}"), CancellationToken.None);
|
||
|
||
await act.Should().ThrowAsync<BusinessException>();
|
||
}
|
||
|
||
[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);
|
||
}
|
||
}
|