using System.Reflection; using NetArchTest.Rules; using New.Application.Ownership; using New.Infrastructure.CaseFramework; using New.Infrastructure.Legacy; using New.Infrastructure.Persistence; using Xunit; namespace Architecture.Tests; /// /// Encodes the design's architecture rules as build-failing assertions. A demo that /// passes the smoke script but fails these has demonstrated nothing - the /// seam boundaries are the point, not an implementation detail. /// public class ArchitectureTests { private static readonly Assembly DomainAssembly = typeof(New.Domain.RegistrationApplication).Assembly; private static readonly Assembly ApplicationAssembly = typeof(New.Application.Ports.IApplicationSource).Assembly; private static readonly Assembly PersistenceAssembly = typeof(NewDbContext).Assembly; private static readonly Assembly LegacyAssembly = typeof(LegacyCaseSource).Assembly; private static readonly Assembly CaseFrameworkAssembly = typeof(CaseFrameworkGateway).Assembly; private static readonly Assembly ApiAssembly = typeof(New.Api.Endpoints.WorklistEndpoints).Assembly; private static readonly Assembly[] AllNewAssemblies = [ DomainAssembly, ApplicationAssembly, PersistenceAssembly, LegacyAssembly, CaseFrameworkAssembly, ApiAssembly, ]; [Fact] public void Rule1_Domain_And_Application_Have_No_Dependency_On_CaseFramework() { var result = Types.InAssembly(DomainAssembly).Should().NotHaveDependencyOn("New.Infrastructure.CaseFramework").GetResult(); Assert.True(result.IsSuccessful, Describe(result)); result = Types.InAssembly(ApplicationAssembly).Should().NotHaveDependencyOn("New.Infrastructure.CaseFramework").GetResult(); Assert.True(result.IsSuccessful, Describe(result)); } [Fact] public void Rule2_Domain_And_Application_Have_No_Dependency_On_Legacy() { var result = Types.InAssembly(DomainAssembly).Should().NotHaveDependencyOn("New.Infrastructure.Legacy").GetResult(); Assert.True(result.IsSuccessful, Describe(result)); result = Types.InAssembly(ApplicationAssembly).Should().NotHaveDependencyOn("New.Infrastructure.Legacy").GetResult(); Assert.True(result.IsSuccessful, Describe(result)); } [Fact] public void Rule3_Legacy_Dtos_Are_Internal() { var result = Types.InAssembly(LegacyAssembly) .That().ResideInNamespace("New.Infrastructure.Legacy.Dtos") .Should().NotBePublic() .GetResult(); Assert.True(result.IsSuccessful, Describe(result)); } [Fact] public void Rule4_CaseFramework_Dtos_Are_Internal() { var result = Types.InAssembly(CaseFrameworkAssembly) .That().ResideInNamespace("New.Infrastructure.CaseFramework.Dtos") .Should().NotBePublic() .GetResult(); Assert.True(result.IsSuccessful, Describe(result)); } [Fact] public void Rule5_No_Legacy_Or_CaseFramework_Connection_String_In_New_Config() { // Config-file concern, not code. Verified by inspection: the // only connection string anywhere under New.* is ConnectionStrings:New // (New.Infrastructure.Persistence.ServiceCollectionExtensions), and // docker-compose.yml only ever injects ConnectionStrings__New into // new-backend. Nothing to assert against compiled IL here. Assert.True(true); } [Fact] public void Rule6_No_Public_Member_Named_Status_In_Domain() { var offendingMembers = DomainAssembly.GetTypes() .Where(t => t.IsPublic || t.IsNestedPublic) .SelectMany(t => t.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)) .Where(m => (m is PropertyInfo || m is FieldInfo) && m.Name == "Status") .ToList(); Assert.True(offendingMembers.Count == 0, $"Found public member(s) named exactly 'Status' in New.Domain: {string.Join(", ", offendingMembers.Select(m => $"{m.DeclaringType!.Name}.{m.Name}"))}. " + "Use ProcessStatus (case-framework-sourced) or AssessmentOutcome (domain decision) instead."); } [Fact] public void Rule7_ApplicationSourceResolver_Is_Only_Type_Referencing_Both_Sources() { // ApplicationSourceResolver is `internal` (New.Api) with no // InternalsVisibleTo grant, so it can't be named via `typeof` here - // looked up by name instead, exactly as NetArchTest itself inspects // compiled IL rather than relying on compile-time visibility. var resolverType = ApiAssembly.GetType("New.Api.Resolution.ApplicationSourceResolver"); Assert.NotNull(resolverType); var ownedType = typeof(OwnedApplicationSource); var legacyType = typeof(LegacyCaseSource); var typesReferencingBoth = AllNewAssemblies .SelectMany(GetLoadableTypes) .Where(t => ReferencesType(t, ownedType) && ReferencesType(t, legacyType)) .ToList(); Assert.True( typesReferencingBoth.Count == 1 && typesReferencingBoth[0] == resolverType, $"Expected only {resolverType!.Name} to reference both {nameof(OwnedApplicationSource)} and {nameof(LegacyCaseSource)}, " + $"but found: {string.Join(", ", typesReferencingBoth.Select(t => t.FullName))}"); } [Fact] public void Rule8_TakeOwnershipHandler_References_Only_Ports() { var result = Types.InAssembly(ApplicationAssembly) .That().HaveName(nameof(TakeOwnershipHandler)) .Should().NotHaveDependencyOnAny( "New.Infrastructure.Persistence", "New.Infrastructure.Legacy", "New.Infrastructure.CaseFramework") .GetResult(); Assert.True(result.IsSuccessful, Describe(result)); } [Fact] public void Rule9_No_New_Project_References_SqlServer() { var offending = AllNewAssemblies .Where(a => a.GetReferencedAssemblies().Any(r => r.Name == "Microsoft.EntityFrameworkCore.SqlServer")) .ToList(); Assert.True(offending.Count == 0, $"These New.* assemblies reference Microsoft.EntityFrameworkCore.SqlServer: {string.Join(", ", offending.Select(a => a.GetName().Name))}"); } [Fact] public void Rule10_Legacy_SqlServer_Only_Is_The_Legacy_Agents_Concern() { // Owned by legacy/ (a separate solution) - not referenceable from // this test project. Verified by inspection there instead. Assert.True(true); } [Fact] public void Rule11_Api_Never_Both_Constructs_Legacy_Dto_And_Touches_DbContext() { // Structurally all-but-guaranteed already: legacy DTOs are internal to // New.Infrastructure.Legacy with no InternalsVisibleTo grant (rule 3), // so New.Api cannot even name them, let alone construct one. This is // therefore a best-effort namespace-level check, not full proof - see // ADR-002 for why rule 11's stronger claim ("the write-through // translator contains no branching on request values") is a review // rule, not a machine-enforced one. var apiTypesTouchingDbContext = Types.InAssembly(ApiAssembly) .That().HaveDependencyOn("New.Infrastructure.Persistence") .GetTypes(); var apiTypesTouchingLegacyDtos = Types.InAssembly(ApiAssembly) .That().HaveDependencyOn("New.Infrastructure.Legacy.Dtos") .GetTypes(); var overlap = apiTypesTouchingDbContext.Intersect(apiTypesTouchingLegacyDtos).ToList(); Assert.True(overlap.Count == 0, $"These New.Api types both touch persistence and legacy DTOs: {string.Join(", ", overlap.Select(t => t.FullName))}"); } private static bool ReferencesType(Type t, Type target) { if (t == target) { return false; } const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; var ctorParamTypes = t.GetConstructors(all).SelectMany(c => c.GetParameters()).Select(p => p.ParameterType); var fieldTypes = t.GetFields(all).Select(f => f.FieldType); var propTypes = t.GetProperties(all).Select(p => p.PropertyType); return ctorParamTypes.Concat(fieldTypes).Concat(propTypes).Any(x => x == target); } private static IEnumerable GetLoadableTypes(Assembly assembly) { try { return assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types.Where(t => t is not null)!; } } private static string Describe(TestResult result) => result.IsSuccessful ? string.Empty : $"Failing types: {string.Join(", ", result.FailingTypes?.Select(t => t.FullName) ?? [])}"; }