Files
tiku-backend.net/Tiku.IntegrationTests/ArchitectureBoundaryTests.cs

319 lines
13 KiB
C#

namespace Tiku.IntegrationTests;
public sealed class ArchitectureBoundaryTests
{
[Fact]
public void Production_authorization_does_not_depend_on_legacy_role_claims()
{
var root = FindRepositoryRoot();
var authorizationFiles = Directory
.EnumerateFiles(Path.Combine(root, "Tiku.Api", "Security"), "*.cs", SearchOption.AllDirectories)
.Append(Path.Combine(root, "Tiku.Api", "Configuration", "AuthenticationExtensions.cs"));
var forbidden = new[]
{
"TikuClaimTypes.TenantRole",
"TenantRoleAuthorization",
"PrimaryRole"
};
AssertNoForbiddenSymbols(
root,
authorizationFiles,
forbidden,
"Production authorization still depends on a legacy role claim or primary role");
}
[Fact]
public void Backoffice_controllers_do_not_construct_platform_access_flags()
{
var root = FindRepositoryRoot();
var controllerFiles = Directory.EnumerateFiles(
Path.Combine(root, "Tiku.Api", "Controllers"),
"*Backoffice*Controller.cs",
SearchOption.AllDirectories);
var forbidden = new[]
{
"new BackofficeActor(",
"IsPlatformAdmin(",
"IsPlatform ="
};
AssertNoForbiddenSymbols(
root,
controllerFiles,
forbidden,
"Backoffice controllers must use the resolved access context instead of constructing platform access flags");
}
[Fact]
public void BackendAuthorizationDoesNotUseMembershipBusinessRoles()
{
var root = FindRepositoryRoot();
var files = new[]
{
Path.Combine(root, "Tiku.Infrastructure", "Growth", "CommissionService.cs"),
Path.Combine(root, "Tiku.Infrastructure", "Growth", "ReferralService.cs"),
Path.Combine(root, "Tiku.Application", "TenantAdmin", "TenantAdminDirectModels.cs"),
Path.Combine(root, "Tiku.Api", "Controllers", "TenantAdminDirectController.cs")
};
AssertNoForbiddenSymbols(
root,
files,
new[]
{
"item.Role == TenantRole.TenantOwner",
"item.Role == TenantRole.TenantAdmin",
"TenantRole Role = TenantRole.TenantAdmin",
"new TenantAdminActor("
},
"Backend authorization must use database role permissions rather than membership business roles or fabricated admin actors");
}
[Fact]
public void Auth_sessions_are_accessed_only_through_the_session_store()
{
var root = FindRepositoryRoot();
var sourceRoots = new[] { "Tiku.Api", "Tiku.Application", "Tiku.Infrastructure" };
var allowedFiles = new[]
{
"TikuDbContext.cs",
"AuthSessionStore.cs",
"SessionStore.cs"
};
var files = sourceRoots
.SelectMany(directory => Directory.EnumerateFiles(
Path.Combine(root, directory),
"*.cs",
SearchOption.AllDirectories))
.Where(path => !path.Contains(
$"{Path.DirectorySeparatorChar}Persistence{Path.DirectorySeparatorChar}Migrations{Path.DirectorySeparatorChar}",
StringComparison.Ordinal))
.Where(path => !path.Contains(
$"{Path.DirectorySeparatorChar}Persistence{Path.DirectorySeparatorChar}Configurations{Path.DirectorySeparatorChar}",
StringComparison.Ordinal))
.Where(path => !allowedFiles.Contains(Path.GetFileName(path), StringComparer.Ordinal));
AssertNoForbiddenSymbols(
root,
files,
new[] { ".AuthSessions", "Set<AuthSession>" },
"AuthSession DbSet access must be encapsulated by IAuthSessionStore");
}
[Fact]
public void Api_uses_an_authenticated_fallback_policy()
{
var root = FindRepositoryRoot();
var authenticationRegistration = File.ReadAllText(
Path.Combine(root, "Tiku.Api", "Configuration", "AuthenticationExtensions.cs"));
Assert.Contains("FallbackPolicy", authenticationRegistration, StringComparison.Ordinal);
Assert.Contains("RequireAuthenticatedUser()", authenticationRegistration, StringComparison.Ordinal);
}
[Fact]
public void Every_controller_action_declares_authorization_or_anonymous_access()
{
var controllerAssembly = typeof(Tiku.Api.Controllers.AuthController).Assembly;
var violations = controllerAssembly
.GetTypes()
.Where(type => !type.IsAbstract && typeof(Microsoft.AspNetCore.Mvc.ControllerBase).IsAssignableFrom(type))
.SelectMany(type => type
.GetMethods(System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.DeclaredOnly)
.Where(method => method
.GetCustomAttributes(inherit: true)
.OfType<Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute>()
.Any())
.Select(method => new
{
Controller = type,
Action = method,
Metadata = type.GetCustomAttributes(inherit: true)
.Concat(method.GetCustomAttributes(inherit: true))
}))
.Where(candidate => !candidate.Metadata.Any(attribute =>
attribute is Microsoft.AspNetCore.Authorization.IAuthorizeData or
Microsoft.AspNetCore.Authorization.IAllowAnonymous))
.Select(candidate => $"{candidate.Controller.FullName}.{candidate.Action.Name}")
.Order(StringComparer.Ordinal)
.ToArray();
Assert.True(
violations.Length == 0,
$"Controller actions without explicit authorization metadata were found:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
}
[Fact]
public void Business_code_does_not_bypass_tenant_query_boundaries()
{
var root = FindRepositoryRoot();
var sourceRoots = new[] { "Tiku.Api", "Tiku.Application", "Tiku.Infrastructure" };
var forbidden = new[]
{
"IgnoreQueryFilters(",
"FromSql(",
"FromSqlRaw(",
"FromSqlInterpolated(",
"ExecuteSql(",
"ExecuteSqlRaw(",
"ExecuteSqlInterpolated(",
"new NpgsqlCommand("
};
var violations = sourceRoots
.SelectMany(directory => Directory.EnumerateFiles(
Path.Combine(root, directory),
"*.cs",
SearchOption.AllDirectories))
.Where(path => !path.Contains(
$"{Path.DirectorySeparatorChar}Persistence{Path.DirectorySeparatorChar}Migrations{Path.DirectorySeparatorChar}",
StringComparison.Ordinal))
.SelectMany(path => File.ReadLines(path)
.Select((line, index) => new { path, line, lineNumber = index + 1 }))
.Where(candidate => forbidden.Any(symbol =>
candidate.line.Contains(symbol, StringComparison.Ordinal)))
.Select(candidate => $"{Path.GetRelativePath(root, candidate.path)}:{candidate.lineNumber}")
.ToArray();
Assert.True(
violations.Length == 0,
$"Forbidden tenant-boundary bypasses were found:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
}
[Fact]
public void Business_code_does_not_depend_on_supabase_or_removed_provider_models()
{
var root = FindRepositoryRoot();
var sourceRoots = new[] { "Tiku.Api", "Tiku.Application", "Tiku.Domain", "Tiku.Infrastructure" };
var forbidden = new[]
{
"Supabase",
"supabase_storage",
"SUPABASE_",
"TenantAuthProvider",
"TenantPaymentAccount",
"TenantPaymentMode",
"TenantPaymentAccountStatus",
"TenantAuthProviderStatus"
};
var allowedPathFragments = new[]
{
$"{Path.DirectorySeparatorChar}Persistence{Path.DirectorySeparatorChar}Migrations{Path.DirectorySeparatorChar}"
};
var violations = sourceRoots
.SelectMany(directory => Directory.EnumerateFiles(
Path.Combine(root, directory),
"*.cs",
SearchOption.AllDirectories))
.Where(path => !allowedPathFragments.Any(fragment => path.Contains(fragment, StringComparison.Ordinal)))
.SelectMany(path => File.ReadLines(path)
.Select((line, index) => new { path, line, lineNumber = index + 1 }))
.Where(candidate => forbidden.Any(symbol =>
candidate.line.Contains(symbol, StringComparison.Ordinal)))
.Select(candidate => $"{Path.GetRelativePath(root, candidate.path)}:{candidate.lineNumber}")
.ToArray();
Assert.True(
violations.Length == 0,
$"Forbidden external-provider coupling was found:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
}
[Fact]
public void Production_code_does_not_reference_removed_wechat_payment_sdk()
{
var root = FindRepositoryRoot();
var sourceRoots = new[] { "Tiku.Api", "Tiku.Application", "Tiku.Domain", "Tiku.Infrastructure" };
var forbidden = new[]
{
"SKIT.FlurlHttpClient.Wechat"
};
var violations = sourceRoots
.SelectMany(directory => Directory.EnumerateFiles(
Path.Combine(root, directory),
"*.cs",
SearchOption.AllDirectories))
.Where(path => !path.Contains(
$"{Path.DirectorySeparatorChar}Persistence{Path.DirectorySeparatorChar}Migrations{Path.DirectorySeparatorChar}",
StringComparison.Ordinal))
.SelectMany(path => File.ReadLines(path)
.Select((line, index) => new { path, line, lineNumber = index + 1 }))
.Where(candidate => forbidden.Any(symbol =>
candidate.line.Contains(symbol, StringComparison.Ordinal)))
.Select(candidate => $"{Path.GetRelativePath(root, candidate.path)}:{candidate.lineNumber}")
.ToArray();
Assert.True(
violations.Length == 0,
$"Removed WeChat payment SDK references were found:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
}
[Fact]
public void Business_layers_do_not_reference_third_party_provider_sdks()
{
var root = FindRepositoryRoot();
var sourceRoots = new[] { "Tiku.Api", "Tiku.Application", "Tiku.Domain" };
var forbidden = new[]
{
"AlibabaCloud.",
"Aliyun.OSS",
"Senparc.Weixin",
"AlipaySDKNet",
"Aop.Api",
"Microsoft.SemanticKernel"
};
var violations = sourceRoots
.SelectMany(directory => Directory.EnumerateFiles(
Path.Combine(root, directory),
"*.cs",
SearchOption.AllDirectories))
.SelectMany(path => File.ReadLines(path)
.Select((line, index) => new { path, line, lineNumber = index + 1 }))
.Where(candidate => forbidden.Any(symbol =>
candidate.line.Contains(symbol, StringComparison.Ordinal)))
.Select(candidate => $"{Path.GetRelativePath(root, candidate.path)}:{candidate.lineNumber}")
.ToArray();
Assert.True(
violations.Length == 0,
$"Business-layer third-party provider SDK references were found:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
}
private static string FindRepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "TIKU-BACKEND.slnx")))
{
directory = directory.Parent;
}
return directory?.FullName ?? throw new DirectoryNotFoundException("Repository root was not found.");
}
private static void AssertNoForbiddenSymbols(
string root,
IEnumerable<string> files,
IReadOnlyCollection<string> forbidden,
string failureMessage)
{
var violations = files
.SelectMany(path => File.ReadLines(path)
.Select((line, index) => new { path, line, lineNumber = index + 1 }))
.Where(candidate => forbidden.Any(symbol =>
candidate.line.Contains(symbol, StringComparison.Ordinal)))
.Select(candidate => $"{Path.GetRelativePath(root, candidate.path)}:{candidate.lineNumber}")
.ToArray();
Assert.True(
violations.Length == 0,
$"{failureMessage}:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
}
}