using System.Reflection; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; using Tiku.Api.Controllers; namespace Tiku.IntegrationTests; public sealed class ArchitectureBoundaryTests { [Fact] public void Api_controllers_depend_on_application_contracts_not_infrastructure() { var root = FindRepositoryRoot(); var violations = Directory.EnumerateFiles( Path.Combine(root, "Tiku.Api", "Controllers"), "*.cs", SearchOption.AllDirectories) .SelectMany(path => File.ReadLines(path) .Select((line, index) => new { path, line, lineNumber = index + 1 })) .Where(candidate => candidate.line.Contains("using Tiku.Infrastructure", StringComparison.Ordinal) || candidate.line.Contains("TikuDbContext", StringComparison.Ordinal)) .Select(candidate => $"{Path.GetRelativePath(root, candidate.path)}:{candidate.lineNumber}") .ToArray(); Assert.True( violations.Length == 0, $"API controllers crossed the Application boundary:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}"); } [Fact] public void Service_classes_do_not_exceed_the_hard_size_limit_or_grow_legacy_debt() { var root = FindRepositoryRoot(); var legacyLineBudgets = new Dictionary(StringComparer.Ordinal) { ["TenantAdminDirectService"] = 3190, ["DirectContentService"] = 2015, ["CommerceAdminService"] = 1842, ["LearningActivityService"] = 1781, ["PlatformAdminService"] = 1719, ["ContentManagementService"] = 1096, ["PlatformQuestionBankService"] = 1033, ["CommerceService"] = 983, ["AssetManagementService"] = 948, ["AuthService"] = 876, ["ReferralService"] = 849, ["PlatformTenantCapabilitiesService"] = 805 }; var serviceGroups = Directory.EnumerateFiles( Path.Combine(root, "Tiku.Infrastructure"), "*Service*.cs", SearchOption.AllDirectories) .Where(path => !path.Contains( $"{Path.DirectorySeparatorChar}Migrations{Path.DirectorySeparatorChar}", StringComparison.Ordinal)) .GroupBy(path => Path.GetFileNameWithoutExtension(path).Split('.')[0], StringComparer.Ordinal) .Select(group => new { Service = group.Key, Lines = group.Sum(path => File.ReadLines(path).Count()), Files = group.Count() }) .ToArray(); var violations = serviceGroups .Where(item => legacyLineBudgets.TryGetValue(item.Service, out var budget) ? item.Lines > budget : item.Lines > 800) .Select(item => legacyLineBudgets.TryGetValue(item.Service, out var budget) ? $"{item.Service} ({item.Lines} lines across {item.Files} files; legacy budget {budget})" : $"{item.Service} ({item.Lines} lines across {item.Files} files; hard limit 800)") .ToArray(); Assert.True( violations.Length == 0, $"Service classes exceeded the hard limit or grew legacy debt:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}"); } [Fact] public void Background_jobs_are_dispatched_by_module_registered_handlers() { var root = FindRepositoryRoot(); var infrastructureRoot = Path.Combine(root, "Tiku.Infrastructure"); var sourceFiles = Directory.EnumerateFiles(infrastructureRoot, "*.cs", SearchOption.AllDirectories) .Where(path => !path.Contains( $"{Path.DirectorySeparatorChar}Persistence{Path.DirectorySeparatorChar}Migrations{Path.DirectorySeparatorChar}", StringComparison.Ordinal)) .ToArray(); var handlerFiles = sourceFiles .Where(path => File.ReadAllText(path).Contains(": IBackgroundJobHandler", StringComparison.Ordinal)) .ToArray(); var expectedJobTypes = new[] { "content_export", "content_import", "asset_security_scan", "tenant_export", "statistics_aggregation", "commerce_reconciliation", "tenant_domain_recheck" }; Assert.Equal(expectedJobTypes.Length, handlerFiles.Length); foreach (var jobType in expectedJobTypes) Assert.Single(handlerFiles, path => File.ReadAllText(path).Contains($"JobType => \"{jobType}\"", StringComparison.Ordinal)); var moduleRegistrations = Directory .EnumerateFiles(Path.Combine(infrastructureRoot, "Modules"), "*.cs", SearchOption.AllDirectories) .Sum(path => File.ReadLines(path).Count(line => line.Contains("AddScoped>>", jobsSource, StringComparison.Ordinal); } [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.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) && !Path.GetFileName(path).StartsWith("TikuDbContext.", StringComparison.Ordinal)); AssertNoForbiddenSymbols( root, files, new[] { ".AuthSessions", "Set" }, "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(AuthController).Assembly; var violations = controllerAssembly .GetTypes() .Where(type => !type.IsAbstract && typeof(ControllerBase).IsAssignableFrom(type)) .SelectMany(type => type .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly) .Where(method => method .GetCustomAttributes(true) .OfType() .Any()) .Select(method => new { Controller = type, Action = method, Metadata = type.GetCustomAttributes(true) .Concat(method.GetCustomAttributes(true)) })) .Where(candidate => !candidate.Metadata.Any(attribute => attribute is IAuthorizeData or 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 files, IReadOnlyCollection 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)}"); } }