Backend/e-suite.API.Common/e-suite.API.Common.UnitTests/PatchMapImplementationTests.cs

293 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Type> _patchDtoTypes = null!;
[SetUp]
public void Setup()
{
_assembly = typeof(Patch<>).Assembly; // e_suite.API.Common
_patchDtoTypes = _assembly
.GetTypes()
.Where(t => t.GetCustomAttribute<PatchesAttribute>() != 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<PatchMapAttribute>();
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<PatchMapAttribute>() != 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<PatchMapAttribute>();
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> → 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<PatchMapAttribute>()
})
.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<PatchesAttribute>();
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<PatchMapAttribute>();
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<PatchesAttribute>();
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 nonpublic type '{targetType.Name}', which cannot be patched.");
}
}
[Test]
public void AllPatchMappedTargetPropertiesMustBeWritable()
{
foreach (var dtoType in _patchDtoTypes)
{
var patchesAttr = dtoType.GetCustomAttribute<PatchesAttribute>();
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<PatchMapAttribute>();
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}'.");
}
}
}
}