using Xunit; namespace KArtSell.ArchitectureTests; public sealed class RepositoryRulesTests { [Fact] public void Prohibited_source_patterns_are_not_introduced() { var repositoryRoot = FindRepositoryRoot(); var sourceFiles = Directory.EnumerateFiles(Path.Combine(repositoryRoot, "src"), "*.cs", SearchOption.AllDirectories) .Where(x => !IsGeneratedOrTestOutput(x)) .ToArray(); // Check anti-patterns AssertNoPattern(sourceFiles, "IGenericRepository", "Generic repository is prohibited."); AssertNoPattern(sourceFiles, "IServiceProvider.GetService", "Service locator is prohibited."); } [Fact] public void DateTime_now_must_use_iclock_abstraction() { var repositoryRoot = FindRepositoryRoot(); var sourceFiles = Directory.EnumerateFiles(Path.Combine(repositoryRoot, "src"), "*.cs", SearchOption.AllDirectories) .Where(x => !IsGeneratedOrTestOutput(x)) .ToArray(); var violations = sourceFiles .Where(path => { var text = File.ReadAllText(path); return (text.Contains("DateTime.Now", StringComparison.Ordinal) || text.Contains("DateTime.UtcNow", StringComparison.Ordinal) || text.Contains("DateTimeOffset.UtcNow", StringComparison.Ordinal)) && !text.Contains("IClock", StringComparison.Ordinal); }) .ToArray(); Assert.True(violations.Length == 0, $"DateTime.Now/UtcNow must use IClock abstraction (not direct DateTime): {string.Join(", ", violations)}"); } [Fact] public void Domain_files_do_not_reference_infrastructure_frameworks() { var root = FindRepositoryRoot(); var domainFiles = Directory.EnumerateFiles( Path.Combine(root, "src"), "*.cs", SearchOption.AllDirectories) .Where(path => path.Contains( $"{Path.DirectorySeparatorChar}Domain{Path.DirectorySeparatorChar}", StringComparison.Ordinal)) .ToArray(); foreach (var prohibited in new[] { "using Dapper", "using Npgsql", "using FastEndpoints", "using Hangfire", "HttpContext", "DbConnection", "IServiceCollection" }) { AssertNoPattern(domainFiles, prohibited, $"Domain cannot depend on infrastructure framework: {prohibited}."); } } [Fact] public void Sql_does_not_use_select_star_or_unqualified_signal_tables() { var repositoryRoot = FindRepositoryRoot(); var files = Directory.EnumerateFiles(repositoryRoot, "*.*", SearchOption.AllDirectories) .Where(x => x.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) || x.EndsWith(".sql", StringComparison.OrdinalIgnoreCase)) .Where(x => !IsGeneratedOrTestOutput(x)) .ToArray(); var selectStar = files.Where(x => File.ReadAllText(x).Contains("select *", StringComparison.OrdinalIgnoreCase)).ToArray(); Assert.True(selectStar.Length == 0, "SELECT * is prohibited: " + string.Join(", ", selectStar)); var unqualified = files.Where(x => { var text = File.ReadAllText(x); return text.Contains(" from signal_decision", StringComparison.OrdinalIgnoreCase) || text.Contains(" into signal_decision", StringComparison.OrdinalIgnoreCase) || text.Contains(" from evidence_snapshot", StringComparison.OrdinalIgnoreCase); }).ToArray(); Assert.True(unqualified.Length == 0, "SignalEngine SQL must be schema-qualified: " + string.Join(", ", unqualified)); } [Fact] public void Every_module_endpoint_declares_roles_or_policies() { var root = FindRepositoryRoot(); var endpoints = Directory.EnumerateFiles( Path.Combine(root, "src"), "Endpoint.cs", SearchOption.AllDirectories) .Where(path => path.Contains( $"{Path.DirectorySeparatorChar}Modules{Path.DirectorySeparatorChar}", StringComparison.Ordinal)) .ToArray(); var violations = endpoints.Where(path => { var text = File.ReadAllText(path); return !text.Contains("Roles(", StringComparison.Ordinal) && !text.Contains("Policies(", StringComparison.Ordinal); }).ToArray(); Assert.True(violations.Length == 0, "Every module endpoint must declare Roles or Policies: " + string.Join(", ", violations)); } [Fact] public void Role_declared_endpoints_must_not_allow_anonymous_access() { var root = FindRepositoryRoot(); var endpoints = Directory.EnumerateFiles( Path.Combine(root, "src"), "*.cs", SearchOption.AllDirectories) .Where(path => Path.GetFileName(path).Contains("Endpoint", StringComparison.Ordinal)) .ToArray(); var violations = endpoints.Where(path => { var text = File.ReadAllText(path); return text.Contains("Roles(", StringComparison.Ordinal) && text.Contains("AllowAnonymous()", StringComparison.Ordinal) && !text.Contains("/test/", StringComparison.Ordinal); }).ToArray(); Assert.True(violations.Length == 0, "Role-declared endpoints must not also allow anonymous access: " + string.Join(", ", violations)); } [Fact] public void Unapproved_reconciliation_endpoints_must_remain_unregistered() { var root = FindRepositoryRoot(); var endpointFile = Path.Combine(root, "src", "KArtSell.Modules.ModelOperations", "PortfolioReconciliation", "Endpoints.cs"); var text = File.ReadAllText(endpointFile); Assert.Equal(4, CountOccurrences(text, "[DontRegister]")); Assert.Equal(4, CountOccurrences(text, "AllowAnonymous()")); } [Fact] public void KIS_trade_endpoints_must_remain_unregistered_while_capability_is_off() { var root = FindRepositoryRoot(); var endpointFile = Path.Combine( root, "src", "KArtSell.Modules.ModelOperations", "TradeExecution", "TradeEndpoints.cs"); var text = File.ReadAllText(endpointFile); Assert.Equal(2, CountOccurrences(text, "[DontRegister]")); Assert.Equal(2, CountOccurrences(text, "AllowAnonymous()")); } [Fact] public void Job_run_repository_columns_exist_in_authoritative_baseline_schema() { var root = FindRepositoryRoot(); var repository = File.ReadAllText(Path.Combine( root, "src", "KArtSell.BuildingBlocks", "Reliability", "DapperJobRunRepository.cs")); var schema = File.ReadAllText(Path.Combine(root, "db", "migrations", "0000_building_blocks.sql")); var requiredColumns = new[] { "job_run_id", "job_type", "job_version", "scope_key", "idempotency_key", "watermark", "app_version", "model_version", "config_version", "data_version", "contract_version", "status", "input_hash", "output_hash", "started_at", "heartbeat_at", "finished_at", "error_code", "trace_id" }; var missingFromSchema = requiredColumns .Where(column => !schema.Contains(column, StringComparison.OrdinalIgnoreCase)) .ToArray(); Assert.Empty(missingFromSchema); var insertSection = repository[(repository.IndexOf("insert into", StringComparison.OrdinalIgnoreCase))..]; var referencedInRepository = requiredColumns .Where(column => insertSection.Contains(column, StringComparison.OrdinalIgnoreCase)) .ToArray(); Assert.Equal( requiredColumns.OrderBy(x => x), referencedInRepository.OrderBy(x => x)); } [Fact] public void Accidental_placeholder_files_are_not_committed() { var root = FindRepositoryRoot(); var names = Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories) .Where(path => !IsGeneratedOrTestOutput(path)) .Where(path => Path.GetFileName(path).Equals("testfile", StringComparison.OrdinalIgnoreCase) || Path.GetFileName(path).EndsWith(".tmp", StringComparison.OrdinalIgnoreCase)) .ToArray(); Assert.True(names.Length == 0, "Placeholder files are prohibited: " + string.Join(", ", names)); } [Fact] public void Aggregate_ids_are_unique_across_modules() { var root = FindRepositoryRoot(); var aggregateIdFiles = Directory.EnumerateFiles( Path.Combine(root, "src"), "*.cs", SearchOption.AllDirectories) .Where(path => !IsGeneratedOrTestOutput(path)) .ToArray(); var aggregateIds = new Dictionary>(); foreach (var file in aggregateIdFiles) { var text = File.ReadAllText(file); // Match aggregate ID definitions: Guid("00000000-0000-0000-0000-...") var pattern = @"Guid\(\""[a-f0-9\-]{36}\"""; var matches = System.Text.RegularExpressions.Regex.Matches(text, pattern); foreach (System.Text.RegularExpressions.Match match in matches) { var id = match.Value; if (!aggregateIds.TryGetValue(id, out var list)) { list = new List(); aggregateIds[id] = list; } list.Add(file); } } var duplicates = aggregateIds .Where(kvp => kvp.Value.Count > 1) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); Assert.True(duplicates.Count == 0, duplicates.Count > 0 ? $"Duplicate aggregate IDs detected: {string.Join("; ", duplicates.Select(d => $"{d.Key} in {string.Join(", ", d.Value)}"))}" : "No duplicate aggregate IDs found."); } private static void AssertNoPattern(IEnumerable files, string pattern, string message) { var violations = files .Where(path => File.ReadAllText(path).Contains(pattern, StringComparison.Ordinal)) .ToArray(); Assert.True(violations.Length == 0, message + " " + string.Join(", ", violations)); } private static int CountOccurrences(string text, string value) => text.Split(value, StringSplitOptions.None).Length - 1; private static bool IsGeneratedOrTestOutput(string path) => path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal) || path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal) || path.Contains($"{Path.DirectorySeparatorChar}tests{Path.DirectorySeparatorChar}", StringComparison.Ordinal) || path.Contains($"{Path.DirectorySeparatorChar}attachments{Path.DirectorySeparatorChar}", StringComparison.Ordinal) || path.Contains($"{Path.DirectorySeparatorChar}research{Path.DirectorySeparatorChar}original{Path.DirectorySeparatorChar}", StringComparison.Ordinal); private static string FindRepositoryRoot() { var directory = new DirectoryInfo(AppContext.BaseDirectory); while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Directory.Build.props"))) { directory = directory.Parent; } return directory?.FullName ?? throw new InvalidOperationException("Repository root not found."); } }