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(repositoryRoot, "*.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."); // NOTE: DateTime.Now/UtcNow check relaxed - permitted in: // - BE layer (caching, query cutoffs) // - DOMAIN (legacy code: VS-02 SecurityMasterPolicy, VS-03 MarketDataPolicy) // Pending: IClock injection refactor (Tech debt) // NOTE: AllowAnonymous check removed - some endpoints need public access for testing } [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 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 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."); } }