xiangning 5239e59b4e fix(security): 修复端点权限缺失问题并加强系统安全防护
- 移除所有 RAG 相关端点的 AllowAnonymous,添加细粒度权限控制
- 配置全局默认授权策略,防止新端点意外公开
- 为 AI 模型、文档、对话、知识库等资源分配读写权限
- 强化 JWT 令牌验证,开启 Issuer/Audience 校验
- 修复脚本执行服务沙箱配置,增强 API 黑名单过滤
- 添加飞书通知凭证环境变量配置,移除硬编码敏感信息
- 完善验证行为逻辑,优化性能和异常处理
- 新增监控模块告警规则实体及相关配置
- 实现外部系统连接器契约和凭证解析服务
2026-07-17 10:06:22 +08:00

286 lines
11 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.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;
/// <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);
}
}