using e_suite.Database.Core.Models; using eSuite.Core.Miscellaneous; using NUnit.Framework; using System.Reflection; namespace e_suite.API.Common.UnitTests; [TestFixture] public class PatchMapImplementationTests { private Assembly _assembly = null!; private List _patchDtoTypes = null!; [SetUp] public void Setup() { _assembly = typeof(Patch<>).Assembly; // e_suite.API.Common _patchDtoTypes = _assembly .GetTypes() .Where(t => t.GetCustomAttribute() != null) .ToList(); } [Test] public void CanDiscoverAllPatchMapAttributesInAssembly() { Assert.That(_patchDtoTypes, Is.Not.Empty, "No types with PatchMapAttribute were found."); } [Test] public void AllPatchMapTargetPropertyNamesMustResolveToRealProperties() { foreach (var dtoType in _patchDtoTypes) { var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var dtoProp in dtoProps) { var mapAttr = dtoProp.GetCustomAttribute(); if (mapAttr == null) continue; var targetPropName = mapAttr.TargetPropertyName; var exists = _assembly .GetTypes() .Any(t => t.GetProperty( targetPropName, BindingFlags.Public | BindingFlags.Instance) != null); Assert.That( exists, Is.True, $"PatchMap on {dtoType.Name}.{dtoProp.Name} refers to unknown property '{targetPropName}' in assembly {_assembly.GetName().Name}."); } } } [Test] public void AllPatchDtoPropertiesMustHavePatchMapAttribute() { foreach (var dtoType in _patchDtoTypes) { var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var dtoProp in dtoProps) { var hasPatchMap = dtoProp.GetCustomAttribute() != null; Assert.That( hasPatchMap, Is.True, $"Property '{dtoType.Name}.{dtoProp.Name}' is missing PatchMapAttribute."); } } } [Test] public void AllPatchMapAttributesMustMapToCompatibleTypes() { var allTypes = _assembly.GetTypes(); foreach (var dtoType in _patchDtoTypes) { var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var dtoProp in dtoProps) { var mapAttr = dtoProp.GetCustomAttribute(); if (mapAttr == null) continue; var targetPropName = mapAttr.TargetPropertyName; // Find the target property anywhere in the assembly var targetProp = allTypes .Select(t => t.GetProperty(targetPropName, BindingFlags.Public | BindingFlags.Instance)) .FirstOrDefault(p => p != null); Assert.That( targetProp, Is.Not.Null, $"PatchMap on {dtoType.Name}.{dtoProp.Name} refers to unknown property '{targetPropName}'."); var dtoPropType = dtoProp.PropertyType; var targetPropType = targetProp!.PropertyType; // Nullable → T compatibility if (dtoPropType.IsGenericType && dtoPropType.GetGenericTypeDefinition() == typeof(Nullable<>)) dtoPropType = Nullable.GetUnderlyingType(dtoPropType)!; if (targetPropType.IsGenericType && targetPropType.GetGenericTypeDefinition() == typeof(Nullable<>)) targetPropType = Nullable.GetUnderlyingType(targetPropType)!; // Navigation properties (GeneralIdRef → IGeneralId) var isDtoGeneralIdRef = dtoPropType == typeof(GeneralIdRef); var isTargetNavigation = typeof(IGeneralId).IsAssignableFrom(targetPropType); if (isDtoGeneralIdRef && isTargetNavigation) continue; // valid navigation mapping // Scalar → scalar compatibility Assert.That( targetPropType.IsAssignableFrom(dtoPropType), Is.True, $"PatchMap type mismatch: {dtoType.Name}.{dtoProp.Name} ({dtoProp.PropertyType.Name}) " + $"cannot be assigned to target property {targetProp.DeclaringType!.Name}.{targetProp.Name} ({targetProp.PropertyType.Name})."); } } } [Test] public void NoPatchDtoMayMapMultiplePropertiesToTheSameTargetProperty() { foreach (var dtoType in _patchDtoTypes) { var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance); // Collect all mappings: targetPropertyName → list of dto properties mapping to it var mapGroups = dtoProps .Select(p => new { Property = p, Attribute = p.GetCustomAttribute() }) .Where(x => x.Attribute != null) .GroupBy(x => x.Attribute!.TargetPropertyName) .ToList(); foreach (var group in mapGroups) { if (group.Count() > 1) { var offendingProps = string.Join( ", ", group.Select(g => $"{dtoType.Name}.{g.Property.Name}") ); Assert.Fail( $"Multiple properties in DTO '{dtoType.Name}' map to the same target property '{group.Key}': {offendingProps}" ); } } } } [Test] public void NavigationMappingsMustMapOnlyToIGeneralIdImplementations() { foreach (var dtoType in _patchDtoTypes) { var patchesAttr = dtoType.GetCustomAttribute(); Assert.That( patchesAttr, Is.Not.Null, $"DTO '{dtoType.Name}' is missing required [Patches] attribute."); var targetType = patchesAttr!.TargetType; var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var dtoProp in dtoProps) { var mapAttr = dtoProp.GetCustomAttribute(); if (mapAttr == null) continue; // Only enforce for navigation DTO properties if (dtoProp.PropertyType != typeof(GeneralIdRef)) continue; var targetPropName = mapAttr.TargetPropertyName; var targetProp = targetType.GetProperty( targetPropName, BindingFlags.Public | BindingFlags.Instance); Assert.That( targetProp, Is.Not.Null, $"PatchMap on {dtoType.Name}.{dtoProp.Name} refers to unknown property '{targetPropName}' on target type '{targetType.Name}'."); var targetPropType = targetProp!.PropertyType; // Unwrap nullable navigation types if needed if (targetPropType.IsGenericType && targetPropType.GetGenericTypeDefinition() == typeof(Nullable<>)) { targetPropType = Nullable.GetUnderlyingType(targetPropType)!; } Assert.That( typeof(IGeneralId).IsAssignableFrom(targetPropType), Is.True, $"Navigation mapping error: {dtoType.Name}.{dtoProp.Name} (GeneralIdRef) maps to " + $"{targetType.Name}.{targetProp.Name} ({targetProp.PropertyType.Name}), " + $"which does not implement IGeneralId."); } } } [Test] public void PatchesAttributeMustReferenceAConcretePublicClass() { foreach (var dtoType in _patchDtoTypes) { var patchesAttr = dtoType.GetCustomAttribute(); Assert.That( patchesAttr, Is.Not.Null, $"DTO '{dtoType.Name}' is missing required [Patches] attribute."); var targetType = patchesAttr!.TargetType; Assert.That( targetType.IsClass, Is.True, $"[Patches] on '{dtoType.Name}' must reference a class, but references '{targetType.Name}'."); Assert.That( !targetType.IsAbstract, Is.True, $"[Patches] on '{dtoType.Name}' references abstract type '{targetType.Name}', which cannot be patched."); Assert.That( targetType.IsPublic, Is.True, $"[Patches] on '{dtoType.Name}' references non‑public type '{targetType.Name}', which cannot be patched."); } } [Test] public void AllPatchMappedTargetPropertiesMustBeWritable() { foreach (var dtoType in _patchDtoTypes) { var patchesAttr = dtoType.GetCustomAttribute(); Assert.That( patchesAttr, Is.Not.Null, $"DTO '{dtoType.Name}' is missing required [Patches] attribute."); var targetType = patchesAttr!.TargetType; var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var dtoProp in dtoProps) { var mapAttr = dtoProp.GetCustomAttribute(); if (mapAttr == null) continue; var targetPropName = mapAttr.TargetPropertyName; var targetProp = targetType.GetProperty( targetPropName, BindingFlags.Public | BindingFlags.Instance); Assert.That( targetProp, Is.Not.Null, $"PatchMap on {dtoType.Name}.{dtoProp.Name} refers to unknown property '{targetPropName}' on target type '{targetType.Name}'."); Assert.That( targetProp!.CanWrite, Is.True, $"PatchMap on {dtoType.Name}.{dtoProp.Name} refers to non-writable property '{targetProp.Name}' on target type '{targetType.Name}'."); } } } }