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 §10'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 - see §10. 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) ?? [])}";
}