293 lines
11 KiB
C#
293 lines
11 KiB
C#
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 non‑public 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}'.");
|
||
}
|
||
}
|
||
}
|
||
} |