- 将菜单实体的 ID 类型从 Guid 改为 long,并配置为自增主键 - 添加菜单创建、删除、树形结构构建等完整的 CRUD 操作支持 - 为文件上传相关端点添加 file:upload 权限验证 - 为文件操作端点添加 file:read 和 file:write 权限控制 - 为通知功能添加 notification:read 权限限制 - 实现 BuildFlow 按钮执行的权限检查机制 - 在 IM 模块中集成 Redis 未读计数器优化性能 - 更新用户主页路径从 /workspace 到 /dashboard/workspace - 添加 SignalR Hub 授权和 WebSocket 连接安全保护 - 新增平台测试项目并完善按钮权限测试用例
112 lines
4.2 KiB
C#
112 lines
4.2 KiB
C#
using System.Text.RegularExpressions;
|
||
using Xunit;
|
||
|
||
namespace BuildFlow.Architecture.Tests;
|
||
|
||
/// <summary>
|
||
/// 权限码注册约束测试。保证 Host 端点声明的权限码都能进入平台权限表,
|
||
/// 避免端点要求了权限但角色管理页无法分配,或登录 token 永远拿不到对应权限码。
|
||
/// </summary>
|
||
public partial class PermissionRegistrationTests
|
||
{
|
||
[Fact]
|
||
public void EndpointPermissionCodesShouldBeSeeded()
|
||
{
|
||
var repoRoot = FindRepoRoot();
|
||
var endpointCodes = ReadEndpointPermissionCodes(repoRoot).ToArray();
|
||
var seededCodes = ReadSeedPermissionCodes(repoRoot).ToHashSet(StringComparer.Ordinal);
|
||
|
||
var missing = endpointCodes
|
||
.Where(code => !seededCodes.Contains(code))
|
||
.ToArray();
|
||
|
||
Assert.True(
|
||
missing.Length == 0,
|
||
"端点声明的权限码必须同步写入 SeedData.SeedPermissionsAsync,否则角色无法分配这些权限:" +
|
||
Environment.NewLine +
|
||
string.Join(Environment.NewLine, missing));
|
||
}
|
||
|
||
[Fact]
|
||
public void SeededPlatformPermissionCodesShouldBeEnforcedByEndpoints()
|
||
{
|
||
var repoRoot = FindRepoRoot();
|
||
var endpointCodes = ReadEndpointPermissionCodes(repoRoot).ToHashSet(StringComparer.Ordinal);
|
||
var seededCodes = ReadSeedPermissionCodes(repoRoot)
|
||
// button PermissionCode 允许配置为业务动态权限,不一定对应固定 endpoint。
|
||
.Where(code => !code.StartsWith("dynamic:", StringComparison.Ordinal))
|
||
.Order(StringComparer.Ordinal)
|
||
.ToArray();
|
||
|
||
var unused = seededCodes
|
||
.Where(code => !endpointCodes.Contains(code))
|
||
.ToArray();
|
||
|
||
Assert.True(
|
||
unused.Length == 0,
|
||
"SeedData 中的平台权限码必须有对应后端端点强校验,否则角色管理页可分配但后端不生效:" +
|
||
Environment.NewLine +
|
||
string.Join(Environment.NewLine, unused));
|
||
}
|
||
|
||
private static DirectoryInfo FindRepoRoot()
|
||
{
|
||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||
while (current is not null)
|
||
{
|
||
if (File.Exists(Path.Combine(current.FullName, "BuildFlow.slnx")))
|
||
{
|
||
return current;
|
||
}
|
||
|
||
current = current.Parent;
|
||
}
|
||
|
||
throw new InvalidOperationException("无法定位 BuildFlow.slnx,权限码注册测试无法确定仓库边界。");
|
||
}
|
||
|
||
private static bool ContainsPathSegment(string path, string segment)
|
||
{
|
||
return path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||
.Any(part => string.Equals(part, segment, StringComparison.Ordinal));
|
||
}
|
||
|
||
private static IEnumerable<string> ReadEndpointPermissionCodes(DirectoryInfo repoRoot)
|
||
{
|
||
var endpointRoot = Path.Combine(repoRoot.FullName, "src", "BuildFlow.Host", "Endpoints");
|
||
|
||
return Directory
|
||
.EnumerateFiles(endpointRoot, "*.cs", SearchOption.AllDirectories)
|
||
.Where(path => !ContainsPathSegment(path, "bin") && !ContainsPathSegment(path, "obj"))
|
||
.SelectMany(path => PermissionCallRegex()
|
||
.Matches(File.ReadAllText(path))
|
||
.Select(match => match.Groups["code"].Value))
|
||
.Distinct(StringComparer.Ordinal)
|
||
.Order(StringComparer.Ordinal);
|
||
}
|
||
|
||
private static IEnumerable<string> ReadSeedPermissionCodes(DirectoryInfo repoRoot)
|
||
{
|
||
var seedFile = Path.Combine(
|
||
repoRoot.FullName,
|
||
"src",
|
||
"modules",
|
||
"platform",
|
||
"Infrastructure",
|
||
"Persistence",
|
||
"SeedData.cs");
|
||
|
||
return SeedPermissionRegex()
|
||
.Matches(File.ReadAllText(seedFile))
|
||
.Select(match => match.Groups["code"].Value)
|
||
.Distinct(StringComparer.Ordinal)
|
||
.Order(StringComparer.Ordinal);
|
||
}
|
||
|
||
[GeneratedRegex("Permissions\\(\"(?<code>[^\"]+)\"\\)")]
|
||
private static partial Regex PermissionCallRegex();
|
||
|
||
[GeneratedRegex("new\\s+SeedPermission\\([^,]+,\\s*\"[^\"]+\",\\s*\"(?<code>[^\"]+)\"")]
|
||
private static partial Regex SeedPermissionRegex();
|
||
}
|