namespace HrynCo.Common.Tests; using FluentAssertions; using Xunit; public sealed class TimeApiUsageGuardTests { private static readonly string[] ForbiddenPatterns = [ "DateTime.Now", "DateTimeOffset.Now" ]; [Fact] public void ProductionCode_ShouldNotUseLocalNowApis() { string solutionRoot = GetSolutionRoot(); var violations = Directory .EnumerateFiles(solutionRoot, "*.cs", SearchOption.AllDirectories) .Where(IsProductionSourceFile) .SelectMany(file => FindViolations(solutionRoot, file)) .ToArray(); violations.Should().BeEmpty( "production code should use UTC-based APIs for persisted and serialized timestamps.{0}{1}", Environment.NewLine, string.Join(Environment.NewLine, violations)); } private static string GetSolutionRoot() { string? current = AppContext.BaseDirectory; while (current is not null) { if (File.Exists(Path.Combine(current, "HrynCo.Common.sln"))) { return current; } current = Directory.GetParent(current)?.FullName; } throw new DirectoryNotFoundException("Could not locate the solution root from the test assembly output."); } private static bool IsProductionSourceFile(string filePath) { string normalizedPath = filePath.Replace('\\', '/'); return !normalizedPath.Contains("/bin/", StringComparison.OrdinalIgnoreCase) && !normalizedPath.Contains("/obj/", StringComparison.OrdinalIgnoreCase) && !normalizedPath.Contains("/Migrations/", StringComparison.OrdinalIgnoreCase) && !normalizedPath.Contains(".Tests/", StringComparison.OrdinalIgnoreCase) && !normalizedPath.Contains(".IntegrationTests/", StringComparison.OrdinalIgnoreCase); } private static IEnumerable FindViolations(string solutionRoot, string filePath) { string relativePath = Path.GetRelativePath(solutionRoot, filePath).Replace('\\', '/'); string[] lines = File.ReadAllLines(filePath); for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++) { string line = lines[lineIndex]; foreach (string forbiddenPattern in ForbiddenPatterns) { if (line.Contains(forbiddenPattern, StringComparison.Ordinal)) { yield return $"{relativePath}:{lineIndex + 1} contains forbidden API `{forbiddenPattern}`"; } } } } }