536 lines
15 KiB
C#
536 lines
15 KiB
C#
using e_suite.API.Common.exceptions;
|
|
using e_suite.Database.Core;
|
|
using e_suite.Database.Core.Models;
|
|
using e_suite.UnitTestCore;
|
|
using eSuite.Core.Miscellaneous;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using System.ComponentModel.DataAnnotations.Schema;
|
|
using e_suite.API.Common.models;
|
|
using e_suite.Database.Core.Tables.Domain;
|
|
using e_suite.Database.Core.Tables.UserManager;
|
|
|
|
namespace e_suite.API.Common.UnitTests;
|
|
|
|
[Patches(typeof(TargetUser))]
|
|
public class PatchDto
|
|
{
|
|
[PatchMap(nameof(TargetUser.FirstName))]
|
|
public string? FirstName { get; set; }
|
|
[PatchMap(nameof(TargetUser.LastName))]
|
|
public string? LastName { get; set; }
|
|
[PatchMap(nameof(TargetUser.IsActive))]
|
|
public bool? IsActive { get; set; }
|
|
}
|
|
|
|
public class TargetUser
|
|
{
|
|
public string FirstName { get; set; } = "Original";
|
|
public string LastName { get; set; } = "User";
|
|
public bool IsActive { get; set; } = false;
|
|
}
|
|
|
|
[TestFixture]
|
|
public class PatchUnitTests : TestBase
|
|
{
|
|
private Mock<IEsuiteDatabaseDbContext> _esuiteDatabaseDbContext;
|
|
|
|
[SetUp]
|
|
public void Setup()
|
|
{
|
|
_esuiteDatabaseDbContext = new Mock<IEsuiteDatabaseDbContext>();
|
|
}
|
|
|
|
|
|
[Test]
|
|
public void ApplyTo_Throws_WhenTargetIsNull()
|
|
{
|
|
var patch = new Patch<PatchDto>(new PatchDto { FirstName = "Colin" }, _esuiteDatabaseDbContext.Object);
|
|
|
|
Assert.ThrowsAsync<ArgumentNullException>(async () => await patch.ApplyTo(null));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ApplyTo_IgnoresNullValues()
|
|
{
|
|
var dto = new PatchDto
|
|
{
|
|
FirstName = null,
|
|
LastName = "Updated"
|
|
};
|
|
|
|
var patch = new Patch<PatchDto>(dto, _esuiteDatabaseDbContext.Object);
|
|
var target = new TargetUser();
|
|
|
|
await patch.ApplyTo(target);
|
|
|
|
Assert.That(target.FirstName, Is.EqualTo("Original")); // unchanged
|
|
Assert.That(target.LastName, Is.EqualTo("Updated")); // patched
|
|
}
|
|
|
|
[Test]
|
|
public async Task ApplyTo_UpdatesNonNullValues()
|
|
{
|
|
var dto = new PatchDto
|
|
{
|
|
FirstName = "Colin",
|
|
LastName = "Smith"
|
|
};
|
|
|
|
var patch = new Patch<PatchDto>(dto, _esuiteDatabaseDbContext.Object);
|
|
var target = new TargetUser();
|
|
|
|
await patch.ApplyTo(target);
|
|
|
|
Assert.That(target.FirstName, Is.EqualTo("Colin"));
|
|
Assert.That(target.LastName, Is.EqualTo("Smith"));
|
|
}
|
|
|
|
[Patches(typeof(TargetUser))]
|
|
private class PatchDtoWithExtra
|
|
{
|
|
[PatchMap(nameof(TargetUser.FirstName))]
|
|
public string? FirstName { get; set; }
|
|
|
|
public string? NonExistent { get; set; }
|
|
}
|
|
|
|
[Test]
|
|
public async Task ApplyTo_IgnoresPropertiesNotOnTarget()
|
|
{
|
|
var dto = new PatchDtoWithExtra
|
|
{
|
|
FirstName = "Updated",
|
|
NonExistent = "Ignored"
|
|
};
|
|
|
|
var patch = new Patch<PatchDtoWithExtra>(dto, _esuiteDatabaseDbContext.Object);
|
|
var target = new TargetUser();
|
|
|
|
await patch.ApplyTo(target);
|
|
|
|
Assert.That(target.FirstName, Is.EqualTo("Updated"));
|
|
}
|
|
|
|
private class TargetWithReadOnly
|
|
{
|
|
public string FirstName { get; set; } = "Original";
|
|
public string ReadOnlyProp => "Fixed";
|
|
}
|
|
|
|
[Test]
|
|
public async Task ApplyTo_UpdatesNullableValueTypes()
|
|
{
|
|
var dto = new PatchDto
|
|
{
|
|
IsActive = true
|
|
};
|
|
|
|
var patch = new Patch<PatchDto>(dto, _esuiteDatabaseDbContext.Object);
|
|
var target = new TargetUser();
|
|
|
|
await patch.ApplyTo(target);
|
|
|
|
Assert.That(target.IsActive, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public async Task ApplyTo_MutatesTargetInPlace()
|
|
{
|
|
var dto = new PatchDto { FirstName = "Updated" };
|
|
var patch = new Patch<PatchDto>(dto, _esuiteDatabaseDbContext.Object);
|
|
|
|
var target = new TargetUser();
|
|
var reference = target;
|
|
|
|
await patch.ApplyTo(target);
|
|
|
|
Assert.That(ReferenceEquals(reference, target), Is.True);
|
|
Assert.That(target.FirstName, Is.EqualTo("Updated"));
|
|
}
|
|
|
|
[Patches(typeof(TargetUser))]
|
|
public class NestedPatchDto
|
|
{
|
|
[PatchMap(nameof(TargetUser.FirstName))]
|
|
public string? FirstName { get; set; }
|
|
|
|
[PatchMap(nameof(TargetUser.FirstName))] //Note incorrectly mapped to make the code trigger exception.
|
|
public Address? HomeAddress { get; set; } // should trigger exception
|
|
}
|
|
|
|
public class Address
|
|
{
|
|
public string? Street { get; set; }
|
|
}
|
|
|
|
[Test]
|
|
public void ApplyTo_Throws_WhenNestedObjectsArePresent()
|
|
{
|
|
var dto = new NestedPatchDto
|
|
{
|
|
FirstName = "Updated",
|
|
HomeAddress = new Address { Street = "New Street" }
|
|
};
|
|
|
|
var patch = new Patch<NestedPatchDto>(dto, _esuiteDatabaseDbContext.Object);
|
|
var target = new TargetUser();
|
|
|
|
Assert.ThrowsAsync<InvalidDataException>( async () => await patch.ApplyTo(target));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ApplyTo_SetsNavigationAndForeignKey_WhenReferenceResolves()
|
|
{
|
|
var options = new DbContextOptionsBuilder<EsuiteDatabaseDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
|
|
using var db = new EsuiteDatabaseDbContext(options, _fakeClock);
|
|
|
|
var domain = new Domain { Id = 10 };
|
|
db.Set<Domain>().Add(domain);
|
|
await db.SaveChangesAsync();
|
|
|
|
var user = new User(); // or even the real User if you like
|
|
|
|
var dto = new PatchUser
|
|
{
|
|
Domain = new GeneralIdRef { Id = 10 }
|
|
};
|
|
|
|
var patch = new Patch<PatchUser>(dto, db);
|
|
|
|
await patch.ApplyTo(user);
|
|
|
|
Assert.That(user.Domain, Is.SameAs(domain));
|
|
Assert.That(user.DomainId, Is.EqualTo(10));
|
|
}
|
|
|
|
[Test]
|
|
public void ApplyTo_ThrowsNotFound_WhenEntityCannotBeResolved()
|
|
{
|
|
var options = new DbContextOptionsBuilder<EsuiteDatabaseDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
|
|
using var db = new EsuiteDatabaseDbContext(options, _fakeClock);
|
|
|
|
var user = new User();
|
|
|
|
var dto = new PatchUser
|
|
{
|
|
Domain = new GeneralIdRef { Id = 999 }
|
|
};
|
|
|
|
var patch = new Patch<PatchUser>(dto, db);
|
|
|
|
Assert.ThrowsAsync<NotFoundException>(() => patch.ApplyTo(user));
|
|
}
|
|
|
|
public class BadNav { public long Id { get; set; } }
|
|
|
|
public class BadNavOwner
|
|
{
|
|
[ForeignKey(nameof(BadNavId))]
|
|
public BadNav BadNav { get; set; } = null!;
|
|
|
|
public long BadNavId { get; set; }
|
|
}
|
|
|
|
[Patches(typeof(BadNavOwner))]
|
|
public class PatchBadNavOwner
|
|
{
|
|
[PatchMap(nameof(BadNavOwner.BadNav))]
|
|
public GeneralIdRef? BadNav { get; set; }
|
|
}
|
|
|
|
[Test]
|
|
public void ApplyTo_Throws_WhenNavigationDoesNotImplementIGeneralId()
|
|
{
|
|
var options = new DbContextOptionsBuilder<EsuiteDatabaseDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
|
|
using var db = new EsuiteDatabaseDbContext(options, _fakeClock);
|
|
|
|
var owner = new BadNavOwner();
|
|
var dto = new PatchBadNavOwner
|
|
{
|
|
BadNav = new GeneralIdRef { Id = 1 }
|
|
};
|
|
|
|
var patch = new Patch<PatchBadNavOwner>(dto, db);
|
|
|
|
var ex = Assert.ThrowsAsync<InvalidDataException>(() => patch.ApplyTo(owner));
|
|
Assert.That(ex.Message, Does.Contain("does not implement IGeneralId"));
|
|
}
|
|
|
|
public class NoFkOwner
|
|
{
|
|
public Domain Domain { get; set; } = null!;
|
|
}
|
|
|
|
[Patches(typeof(NoFkOwner))]
|
|
public class PatchNoFkOwner
|
|
{
|
|
[PatchMap(nameof(NoFkOwner.Domain))]
|
|
public GeneralIdRef? Domain { get; set; }
|
|
}
|
|
|
|
[Test]
|
|
public async Task ApplyTo_SetsNavigation_WhenForeignKeyMissing()
|
|
{
|
|
var options = new DbContextOptionsBuilder<EsuiteDatabaseDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
|
|
using var db = new EsuiteDatabaseDbContext(options, _fakeClock);
|
|
|
|
var domain = new Domain { Id = 5 };
|
|
db.Set<Domain>().Add(domain);
|
|
await db.SaveChangesAsync();
|
|
|
|
var owner = new NoFkOwner();
|
|
var dto = new PatchNoFkOwner
|
|
{
|
|
Domain = new GeneralIdRef { Id = 5 }
|
|
};
|
|
|
|
var patch = new Patch<PatchNoFkOwner>(dto, db);
|
|
|
|
await patch.ApplyTo(owner);
|
|
|
|
Assert.That(owner.Domain, Is.SameAs(domain));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ApplyTo_DoesNotChangeNavigation_WhenDtoValueIsNull()
|
|
{
|
|
var options = new DbContextOptionsBuilder<EsuiteDatabaseDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
|
|
using var db = new EsuiteDatabaseDbContext(options, _fakeClock);
|
|
|
|
var domain = new Domain { Id = 3 };
|
|
db.Set<Domain>().Add(domain);
|
|
await db.SaveChangesAsync();
|
|
|
|
var user = new User
|
|
{
|
|
Domain = domain,
|
|
DomainId = 3
|
|
};
|
|
|
|
var dto = new PatchUser
|
|
{
|
|
Domain = null
|
|
};
|
|
|
|
var patch = new Patch<PatchUser>(dto, db);
|
|
|
|
await patch.ApplyTo(user);
|
|
|
|
Assert.That(user.Domain, Is.SameAs(domain));
|
|
Assert.That(user.DomainId, Is.EqualTo(3));
|
|
}
|
|
|
|
public class BrokenFkOwner
|
|
{
|
|
[ForeignKey("MissingFk")]
|
|
public Domain Domain { get; set; } = null!;
|
|
}
|
|
|
|
[Patches(typeof(BrokenFkOwner))]
|
|
public class PatchBrokenFkOwner
|
|
{
|
|
[PatchMap(nameof(BrokenFkOwner.Domain))]
|
|
public GeneralIdRef? Domain { get; set; }
|
|
}
|
|
|
|
[Test]
|
|
public async Task ApplyTo_DoesNotThrow_WhenForeignKeyPropertyMissing()
|
|
{
|
|
var options = new DbContextOptionsBuilder<EsuiteDatabaseDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
|
|
using var db = new EsuiteDatabaseDbContext(options, _fakeClock);
|
|
|
|
var domain = new Domain { Id = 7 };
|
|
db.Set<Domain>().Add(domain);
|
|
await db.SaveChangesAsync();
|
|
|
|
var owner = new BrokenFkOwner();
|
|
var dto = new PatchBrokenFkOwner
|
|
{
|
|
Domain = new GeneralIdRef { Id = 7 }
|
|
};
|
|
|
|
var patch = new Patch<PatchBrokenFkOwner>(dto, db);
|
|
|
|
await patch.ApplyTo(owner);
|
|
|
|
Assert.That(owner.Domain, Is.SameAs(domain));
|
|
}
|
|
|
|
[Patches(typeof(TargetUser))]
|
|
private class PatchDtoWithBadMap
|
|
{
|
|
[PatchMap("DoesNotExist")]
|
|
public string? FirstName { get; set; }
|
|
}
|
|
|
|
[Test]
|
|
public void ApplyTo_Throws_WhenPatchMapTargetsUnknownProperty()
|
|
{
|
|
var dto = new PatchDtoWithBadMap
|
|
{
|
|
FirstName = "Updated"
|
|
};
|
|
|
|
var patch = new Patch<PatchDtoWithBadMap>(dto, _esuiteDatabaseDbContext.Object);
|
|
|
|
var target = new TargetUser();
|
|
|
|
var ex = Assert.ThrowsAsync<InvalidDataException>(() => patch.ApplyTo(target));
|
|
|
|
Assert.That(ex.Message, Does.Contain("PatchMap refers to unknown property 'DoesNotExist'"));
|
|
Assert.That(ex.Message, Does.Contain(nameof(TargetUser)));
|
|
}
|
|
|
|
[Patches(typeof(TargetWithReadOnly))]
|
|
private class PatchDtoReadOnly
|
|
{
|
|
[PatchMap(nameof(TargetWithReadOnly.ReadOnlyProp))]
|
|
public string? ReadOnlyProp { get; set; }
|
|
}
|
|
|
|
[Test]
|
|
public void ApplyTo_Throws_WhenTargetPropertyIsNotWritable()
|
|
{
|
|
var dto = new PatchDtoReadOnly
|
|
{
|
|
ReadOnlyProp = "NewValue"
|
|
};
|
|
|
|
var patch = new Patch<PatchDtoReadOnly>(dto, _esuiteDatabaseDbContext.Object);
|
|
|
|
var target = new TargetWithReadOnly();
|
|
|
|
var ex = Assert.ThrowsAsync<InvalidDataException>(() => patch.ApplyTo(target));
|
|
|
|
Assert.That(ex.Message, Does.Contain("is not writable"));
|
|
Assert.That(ex.Message, Does.Contain(nameof(TargetWithReadOnly.ReadOnlyProp)));
|
|
}
|
|
|
|
private class TargetWritable
|
|
{
|
|
public string FirstName { get; set; } = "Original";
|
|
}
|
|
|
|
[Patches(typeof(TargetWritable))]
|
|
private class PatchDtoWritable
|
|
{
|
|
[PatchMap(nameof(TargetWritable.FirstName))]
|
|
public string? FirstName { get; set; }
|
|
}
|
|
|
|
[Test]
|
|
public async Task ApplyTo_DoesNotThrow_WhenTargetPropertyIsWritable()
|
|
{
|
|
var dto = new PatchDtoWritable
|
|
{
|
|
FirstName = "Updated"
|
|
};
|
|
|
|
var patch = new Patch<PatchDtoWritable>(dto, _esuiteDatabaseDbContext.Object);
|
|
|
|
var target = new TargetWritable();
|
|
|
|
// Act + Assert: should NOT throw
|
|
Assert.DoesNotThrowAsync(() => patch.ApplyTo(target));
|
|
|
|
// And the property should be updated
|
|
Assert.That(target.FirstName, Is.EqualTo("Updated"));
|
|
}
|
|
|
|
[Patches(typeof(TargetUser))]
|
|
private class PatchDtoWithoutMaps
|
|
{
|
|
public string? FirstName { get; set; }
|
|
public string? LastName { get; set; }
|
|
}
|
|
|
|
[Test]
|
|
public void ApplyTo_Throws_WhenDtoHasNoPatchMapAttributes()
|
|
{
|
|
var dto = new PatchDtoWithoutMaps
|
|
{
|
|
FirstName = "Colin",
|
|
LastName = "Smith"
|
|
};
|
|
|
|
var patch = new Patch<PatchDtoWithoutMaps>(dto, _esuiteDatabaseDbContext.Object);
|
|
|
|
var target = new TargetUser();
|
|
|
|
var ex = Assert.ThrowsAsync<InvalidDataException>(() => patch.ApplyTo(target));
|
|
|
|
Assert.That(ex.Message, Does.Contain("does not define any properties with PatchMapAttribute"));
|
|
}
|
|
|
|
private class PatchDtoWithoutPatchesAttribute
|
|
{
|
|
[PatchMap("FirstName")]
|
|
public string? FirstName { get; set; }
|
|
}
|
|
|
|
[Test]
|
|
public void Constructor_Throws_WhenPatchesAttributeIsMissing()
|
|
{
|
|
var dto = new PatchDtoWithoutPatchesAttribute
|
|
{
|
|
FirstName = "Colin"
|
|
};
|
|
|
|
var ex = Assert.Throws<InvalidDataException>(() =>
|
|
new Patch<PatchDtoWithoutPatchesAttribute>(dto, _esuiteDatabaseDbContext.Object));
|
|
|
|
Assert.That(ex.Message, Does.Contain("is missing required [Patches] attribute"));
|
|
}
|
|
|
|
[Patches(typeof(TargetUser))]
|
|
private class PatchUserDto
|
|
{
|
|
[PatchMap(nameof(TargetUser.FirstName))]
|
|
public string? FirstName { get; set; }
|
|
}
|
|
|
|
private class WrongTargetType
|
|
{
|
|
public string FirstName { get; set; } = "Original";
|
|
}
|
|
|
|
[Test]
|
|
public void ApplyTo_Throws_WhenAppliedToWrongTargetType()
|
|
{
|
|
var dto = new PatchUserDto
|
|
{
|
|
FirstName = "Updated"
|
|
};
|
|
|
|
var patch = new Patch<PatchUserDto>(dto, _esuiteDatabaseDbContext.Object);
|
|
|
|
var wrongTarget = new WrongTargetType();
|
|
|
|
var ex = Assert.ThrowsAsync<InvalidDataException>(() =>
|
|
patch.ApplyTo(wrongTarget));
|
|
|
|
Assert.That(ex.Message, Does.Contain("is defined to patch"));
|
|
Assert.That(ex.Message, Does.Contain(nameof(TargetUser)));
|
|
Assert.That(ex.Message, Does.Contain(nameof(WrongTargetType)));
|
|
}
|
|
} |