using System.Text.RegularExpressions; using Xunit; namespace BuildFlow.Architecture.Tests; /// /// 权限码注册约束测试。保证 Host 端点声明的权限码都能进入平台权限表, /// 避免端点要求了权限但角色管理页无法分配,或登录 token 永远拿不到对应权限码。 /// 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 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 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\\(\"(?[^\"]+)\"\\)")] private static partial Regex PermissionCallRegex(); [GeneratedRegex("new\\s+SeedPermission\\([^,]+,\\s*\"[^\"]+\",\\s*\"(?[^\"]+)\"")] private static partial Regex SeedPermissionRegex(); }