diff --git a/e-suite.API.Common/e-suite.API.Common.UnitTests/PatchFactoryUnitTests.cs b/e-suite.API.Common/e-suite.API.Common.UnitTests/PatchFactoryUnitTests.cs new file mode 100644 index 0000000..249ee93 --- /dev/null +++ b/e-suite.API.Common/e-suite.API.Common.UnitTests/PatchFactoryUnitTests.cs @@ -0,0 +1,50 @@ +using e_suite.API.Common.models; +using e_suite.Database.Core; +using Moq; +using NUnit.Framework; + +namespace e_suite.API.Common.UnitTests; + +[TestFixture] +public class PatchFactoryUnitTests +{ + [Test] + public void Create_ReturnsPatchInstanceWithCorrectValue() + { + // Arrange + var esuiteDatabaseDbContext = new Mock(); + var factory = new PatchFactory(esuiteDatabaseDbContext.Object); + var dto = new PatchUserProfile + { + FirstName = "Colin" + }; + + // Act + var patch = factory.Create(dto); + + // Assert + Assert.That(patch, Is.Not.Null); + Assert.That(patch, Is.InstanceOf>()); + + // And verify the internal value is the same object + var concrete = patch as Patch; + Assert.That(concrete!.Value, Is.EqualTo(dto)); + } + + [Test] + public void Create_ReturnsNewInstanceEachTime() + { + //Arrange + var esuiteDatabaseDbContext = new Mock(); + var factory = new PatchFactory(esuiteDatabaseDbContext.Object); + var dto = new PatchUserProfile(); + + //Act + var p1 = factory.Create(dto); + var p2 = factory.Create(dto); + + //Assert + Assert.That(p1, Is.Not.SameAs(p2)); + } + +} \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common.UnitTests/PatchMapImplementationTests.cs b/e-suite.API.Common/e-suite.API.Common.UnitTests/PatchMapImplementationTests.cs new file mode 100644 index 0000000..86bd240 --- /dev/null +++ b/e-suite.API.Common/e-suite.API.Common.UnitTests/PatchMapImplementationTests.cs @@ -0,0 +1,293 @@ +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}'."); + } + } + } +} \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common.UnitTests/PatchUnitTests.cs b/e-suite.API.Common/e-suite.API.Common.UnitTests/PatchUnitTests.cs new file mode 100644 index 0000000..e19bef7 --- /dev/null +++ b/e-suite.API.Common/e-suite.API.Common.UnitTests/PatchUnitTests.cs @@ -0,0 +1,536 @@ +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 _esuiteDatabaseDbContext; + + [SetUp] + public void Setup() + { + _esuiteDatabaseDbContext = new Mock(); + } + + + [Test] + public void ApplyTo_Throws_WhenTargetIsNull() + { + var patch = new Patch(new PatchDto { FirstName = "Colin" }, _esuiteDatabaseDbContext.Object); + + Assert.ThrowsAsync(async () => await patch.ApplyTo(null)); + } + + [Test] + public async Task ApplyTo_IgnoresNullValues() + { + var dto = new PatchDto + { + FirstName = null, + LastName = "Updated" + }; + + var patch = new Patch(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(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(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(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(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(dto, _esuiteDatabaseDbContext.Object); + var target = new TargetUser(); + + Assert.ThrowsAsync( async () => await patch.ApplyTo(target)); + } + + [Test] + public async Task ApplyTo_SetsNavigationAndForeignKey_WhenReferenceResolves() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + using var db = new EsuiteDatabaseDbContext(options, _fakeClock); + + var domain = new Domain { Id = 10 }; + db.Set().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(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() + .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(dto, db); + + Assert.ThrowsAsync(() => 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() + .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(dto, db); + + var ex = Assert.ThrowsAsync(() => 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() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + using var db = new EsuiteDatabaseDbContext(options, _fakeClock); + + var domain = new Domain { Id = 5 }; + db.Set().Add(domain); + await db.SaveChangesAsync(); + + var owner = new NoFkOwner(); + var dto = new PatchNoFkOwner + { + Domain = new GeneralIdRef { Id = 5 } + }; + + var patch = new Patch(dto, db); + + await patch.ApplyTo(owner); + + Assert.That(owner.Domain, Is.SameAs(domain)); + } + + [Test] + public async Task ApplyTo_DoesNotChangeNavigation_WhenDtoValueIsNull() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + using var db = new EsuiteDatabaseDbContext(options, _fakeClock); + + var domain = new Domain { Id = 3 }; + db.Set().Add(domain); + await db.SaveChangesAsync(); + + var user = new User + { + Domain = domain, + DomainId = 3 + }; + + var dto = new PatchUser + { + Domain = null + }; + + var patch = new Patch(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() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + using var db = new EsuiteDatabaseDbContext(options, _fakeClock); + + var domain = new Domain { Id = 7 }; + db.Set().Add(domain); + await db.SaveChangesAsync(); + + var owner = new BrokenFkOwner(); + var dto = new PatchBrokenFkOwner + { + Domain = new GeneralIdRef { Id = 7 } + }; + + var patch = new Patch(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(dto, _esuiteDatabaseDbContext.Object); + + var target = new TargetUser(); + + var ex = Assert.ThrowsAsync(() => 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(dto, _esuiteDatabaseDbContext.Object); + + var target = new TargetWithReadOnly(); + + var ex = Assert.ThrowsAsync(() => 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(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(dto, _esuiteDatabaseDbContext.Object); + + var target = new TargetUser(); + + var ex = Assert.ThrowsAsync(() => 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(() => + new Patch(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(dto, _esuiteDatabaseDbContext.Object); + + var wrongTarget = new WrongTargetType(); + + var ex = Assert.ThrowsAsync(() => + 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))); + } +} \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common.UnitTests/e-suite.API.Common.UnitTests.csproj b/e-suite.API.Common/e-suite.API.Common.UnitTests/e-suite.API.Common.UnitTests.csproj index 376715e..42f61d0 100644 --- a/e-suite.API.Common/e-suite.API.Common.UnitTests/e-suite.API.Common.UnitTests.csproj +++ b/e-suite.API.Common/e-suite.API.Common.UnitTests/e-suite.API.Common.UnitTests.csproj @@ -8,12 +8,16 @@ + + + + diff --git a/e-suite.API.Common/e-suite.API.Common/IPatch.cs b/e-suite.API.Common/e-suite.API.Common/IPatch.cs new file mode 100644 index 0000000..9f5e710 --- /dev/null +++ b/e-suite.API.Common/e-suite.API.Common/IPatch.cs @@ -0,0 +1,7 @@ +namespace e_suite.API.Common; + +public interface IPatch +{ + T Value { get; } + Task ApplyTo(object target); +} \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common/IPatchFactory.cs b/e-suite.API.Common/e-suite.API.Common/IPatchFactory.cs new file mode 100644 index 0000000..44213d5 --- /dev/null +++ b/e-suite.API.Common/e-suite.API.Common/IPatchFactory.cs @@ -0,0 +1,8 @@ +using e_suite.Database.Core; + +namespace e_suite.API.Common; + +public interface IPatchFactory +{ + IPatch Create(T value); +} \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common/IUserManager.cs b/e-suite.API.Common/e-suite.API.Common/IUserManager.cs index e5a2b2d..6aa4d82 100644 --- a/e-suite.API.Common/e-suite.API.Common/IUserManager.cs +++ b/e-suite.API.Common/e-suite.API.Common/IUserManager.cs @@ -20,7 +20,8 @@ public interface IUserManager Task DeactivateUser(AuditUserDetails auditUserDetails, GeneralIdRef generalIdRef, CancellationToken cancellationToken = default!); Task GetProfile(string email, CancellationToken cancellationToken = default!); Task UpdateProfile(AuditUserDetails auditUserDetails, string email, UpdatedUserProfile userProfile, CancellationToken cancellationToken = default!); - public Task> GetUsersAsync(Paging paging, CancellationToken cancellationToken); + Task PatchProfile( AuditUserDetails auditUserDetails, string email, PatchUserProfile patchUserProfile, CancellationToken cancellationToken ); + Task> GetUsersAsync(Paging paging, CancellationToken cancellationToken); Task GetUserAsync(GeneralIdRef generalIdRef, CancellationToken cancellationToken); Task GetUserByEmailAsync(string email, CancellationToken cancellationToken); @@ -34,4 +35,5 @@ public interface IUserManager Task CreateSingleUseGuid(AuditUserDetails auditUserDetails, GeneralIdRef generalIdRef, CancellationToken cancellationToken); Task GetUserWithSingleUseGuid(Guid guid, CancellationToken cancellationToken); Task SetAuthentication( AuditUserDetails auditUserDetails, UserAuthenticationDetails userAuthenticationDetails, bool setEmailConfirmed, CancellationToken cancellationToken ); + Task PatchUser(AuditUserDetails auditUserDetails, IGeneralIdRef userId, PatchUser patchUser, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common/IocRegistration.cs b/e-suite.API.Common/e-suite.API.Common/IocRegistration.cs new file mode 100644 index 0000000..c36ecda --- /dev/null +++ b/e-suite.API.Common/e-suite.API.Common/IocRegistration.cs @@ -0,0 +1,12 @@ +using Autofac; +using e_suite.DependencyInjection; + +namespace e_suite.API.Common; + +public class IocRegistration : IIocRegistration +{ + public void RegisterTypes(ContainerBuilder builder) + { + builder.RegisterType().As().InstancePerLifetimeScope(); + } +} \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common/Patch.cs b/e-suite.API.Common/e-suite.API.Common/Patch.cs new file mode 100644 index 0000000..5815848 --- /dev/null +++ b/e-suite.API.Common/e-suite.API.Common/Patch.cs @@ -0,0 +1,165 @@ +using e_suite.API.Common.exceptions; +using e_suite.Database.Core; +using e_suite.Database.Core.Extensions; +using e_suite.Database.Core.Models; +using eSuite.Core.Miscellaneous; +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations.Schema; +using System.Reflection; + +namespace e_suite.API.Common; + +public class Patch : IPatch +{ + private readonly IEsuiteDatabaseDbContext _dbContext; + private readonly Type _targetType; + public T Value { get; } + + public Patch(T value, IEsuiteDatabaseDbContext dbContext) + { + Value = value; + _dbContext = dbContext; + + var patchesAttr = typeof(T).GetCustomAttribute(); + if (patchesAttr == null) + throw new InvalidDataException( + $"Patch DTO '{typeof(T).Name}' is missing required [Patches] attribute."); + + _targetType = patchesAttr.TargetType; + } + + public async Task ApplyTo(object target) + { + if (target == null) + throw new ArgumentNullException(nameof(target)); + + if (!_targetType.IsAssignableFrom(target.GetType())) + { + throw new InvalidDataException( + $"Patch DTO '{typeof(T).Name}' is defined to patch '{_targetType.Name}', " + + $"but was applied to instance of '{target.GetType().Name}'."); + } + + + var targetType = target.GetType(); + var targetProps = targetType + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .ToDictionary(p => p.Name); + + var patchProps = typeof(T) + .GetProperties(BindingFlags.Public | BindingFlags.Instance); + + var mappedProps = patchProps + .Where(p => p.GetCustomAttribute() != null) + .ToList(); + + if (mappedProps.Count == 0) + { + throw new InvalidDataException( + $"Patch DTO '{typeof(T).Name}' does not define any properties with PatchMapAttribute."); + } + + foreach (var patchProp in patchProps) + { + // Only process properties with PatchMap + var mapAttr = patchProp.GetCustomAttribute(); + if (mapAttr == null) + continue; + + var targetPropName = mapAttr.TargetPropertyName; + + if (!targetProps.TryGetValue(targetPropName, out var targetProp)) + throw new InvalidDataException( + $"PatchMap refers to unknown property '{targetPropName}' on '{targetType.Name}'"); + + if (!targetProp.CanWrite) + throw new InvalidDataException( + $"Target property '{targetPropName}' is not writable"); + + var patchValue = patchProp.GetValue(Value); + + // Null means "not included in the patch" + if (patchValue == null) + continue; + + bool isGeneralIdRef = typeof(IGeneralIdRef).IsAssignableFrom(patchProp.PropertyType); + + // Disallow nested objects (except string and IGeneralIdRef) + if (patchProp.PropertyType.IsClass && + patchProp.PropertyType != typeof(string) && + !isGeneralIdRef) + { + throw new InvalidDataException( + $"Nested objects are not allowed in PATCH DTOs. Property: {patchProp.Name}"); + } + + // Handle navigation properties via IGeneralIdRef + if (isGeneralIdRef) + { + // Ensure the target navigation type implements IGeneralId + if (!typeof(IGeneralId).IsAssignableFrom(targetProp.PropertyType)) + { + throw new InvalidDataException( + $"PatchMap for '{patchProp.Name}' refers to navigation property '{targetPropName}', " + + $"but '{targetProp.PropertyType.Name}' does not implement IGeneralId. " + + $"Navigation properties patched via IGeneralIdRef must implement IGeneralId."); + } + + var refValue = (IGeneralIdRef)patchValue; + + var targetEntityType = targetProp.PropertyType; + + // Get DbSet + var setMethod = typeof(DbContext) + .GetMethod(nameof(DbContext.Set), Type.EmptyTypes)! + .MakeGenericMethod(targetEntityType); + + var dbSet = setMethod.Invoke(_dbContext, null); + + // Call FindByGeneralIdRefAsync + var findMethod = typeof(QueryableExtensions) + .GetMethod(nameof(QueryableExtensions.FindByGeneralIdRefAsync))! + .MakeGenericMethod(targetEntityType); + + var task = (Task)findMethod.Invoke( + null, + [dbSet, refValue, CancellationToken.None] + )!; + + await task.ConfigureAwait(false); + + var resultProperty = task.GetType().GetProperty("Result"); + var resolvedEntity = resultProperty?.GetValue(task); + + if (resolvedEntity == null) + throw new NotFoundException( + $"Unable to resolve reference for '{patchProp.Name}'"); + + // Assign navigation property + targetProp.SetValue(target, resolvedEntity); + + // Look for ForeignKeyAttribute on the navigation property + var fkAttr = targetProp.GetCustomAttribute(); + if (fkAttr != null) + { + var fkName = fkAttr.Name; + + if (targetProps.TryGetValue(fkName, out var fkProp)) + { + var idValue = resolvedEntity + .GetType() + .GetProperty("Id") + ?.GetValue(resolvedEntity); + + fkProp.SetValue(target, idValue); + } + } + + continue; + } + + // Default scalar assignment + targetProp.SetValue(target, patchValue); + } + } +} \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common/PatchFactory.cs b/e-suite.API.Common/e-suite.API.Common/PatchFactory.cs new file mode 100644 index 0000000..a643683 --- /dev/null +++ b/e-suite.API.Common/e-suite.API.Common/PatchFactory.cs @@ -0,0 +1,17 @@ +using e_suite.Database.Core; + +namespace e_suite.API.Common; + +public class PatchFactory : IPatchFactory +{ + private readonly IEsuiteDatabaseDbContext _dbContext; + public PatchFactory(IEsuiteDatabaseDbContext dbContext) + { + _dbContext = dbContext; + } + + public IPatch Create(T value) + { + return new Patch(value, _dbContext); + } +} \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common/PatchMapAttribute.cs b/e-suite.API.Common/e-suite.API.Common/PatchMapAttribute.cs new file mode 100644 index 0000000..7cc9a97 --- /dev/null +++ b/e-suite.API.Common/e-suite.API.Common/PatchMapAttribute.cs @@ -0,0 +1,12 @@ +namespace e_suite.API.Common; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class PatchMapAttribute : Attribute +{ + public string TargetPropertyName { get; } + + public PatchMapAttribute(string targetPropertyName) + { + TargetPropertyName = targetPropertyName; + } +} \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common/PatchesAttribute.cs b/e-suite.API.Common/e-suite.API.Common/PatchesAttribute.cs new file mode 100644 index 0000000..6cb4a38 --- /dev/null +++ b/e-suite.API.Common/e-suite.API.Common/PatchesAttribute.cs @@ -0,0 +1,12 @@ +namespace e_suite.API.Common; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +public sealed class PatchesAttribute : Attribute +{ + public Type TargetType { get; } + + public PatchesAttribute(Type targetType) + { + TargetType = targetType; + } +} \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common/e-suite.API.Common.csproj b/e-suite.API.Common/e-suite.API.Common/e-suite.API.Common.csproj index 6a99cae..98a8d0f 100644 --- a/e-suite.API.Common/e-suite.API.Common/e-suite.API.Common.csproj +++ b/e-suite.API.Common/e-suite.API.Common/e-suite.API.Common.csproj @@ -13,6 +13,7 @@ + diff --git a/e-suite.API.Common/e-suite.API.Common/models/EditUser.cs b/e-suite.API.Common/e-suite.API.Common/models/EditUser.cs index bb17d39..d02ea8d 100644 --- a/e-suite.API.Common/e-suite.API.Common/models/EditUser.cs +++ b/e-suite.API.Common/e-suite.API.Common/models/EditUser.cs @@ -1,15 +1,35 @@ -using System.ComponentModel; +using e_suite.API.Common.models.@base; +using e_suite.Database.Core.Tables.UserManager; +using eSuite.Core.Miscellaneous; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -using e_suite.API.Common.models.@base; -using eSuite.Core.Miscellaneous; namespace e_suite.API.Common.models; public class EditUser : UserBase -{ +{ [JsonPropertyName("id")] [Required] [DefaultValue(null)] - public GeneralIdRef? Id { get; set; } + public GeneralIdRef Id { get; set; } = null!; +} + +[Patches(typeof(User))] +public class PatchUser +{ + [PatchMap(nameof(User.FirstName))] + public string? FirstName { get; set; } + + [PatchMap(nameof(User.LastName))] + public string? LastName { get; set; } + + [PatchMap(nameof(User.MiddleNames))] + public string? MiddleNames { get; set; } + + [PatchMap(nameof(User.Email))] + public string? Email { get; set; } + + [PatchMap(nameof(User.Domain))] + public GeneralIdRef? Domain { get; set; } } \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common/models/UserProfile.cs b/e-suite.API.Common/e-suite.API.Common/models/UserProfile.cs index 991421f..efac785 100644 --- a/e-suite.API.Common/e-suite.API.Common/models/UserProfile.cs +++ b/e-suite.API.Common/e-suite.API.Common/models/UserProfile.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; +using e_suite.Database.Core.Tables.UserManager; namespace e_suite.API.Common.models; @@ -52,6 +53,9 @@ public class UserProfile public string SsoSubject { get; set; } = string.Empty; public Dictionary SsoProviders { get; set; } = null!; + + [JsonPropertyName("preferredLocale")] + public string PreferredLocale { get; set; } = string.Empty; } @@ -79,4 +83,27 @@ public class UpdatedUserProfile [JsonPropertyName("securityCode")] public string SecurityCode { get; set; } = string.Empty; + + [JsonPropertyName("preferredLocale")] + public string PreferredLocale { get; set; } = string.Empty; +} + +[Patches(typeof(User))] +public class PatchUserProfile +{ + [JsonPropertyName("firstName")] + [PatchMap(nameof(User.FirstName))] + public string? FirstName { get; set; } + + [JsonPropertyName("middleNames")] + [PatchMap(nameof(User.MiddleNames))] + public string? MiddleNames { get; set; } + + [JsonPropertyName("lastName")] + [PatchMap(nameof(User.LastName))] + public string? LastName { get; set; } + + [JsonPropertyName("preferredLocale")] + [PatchMap(nameof(User.PreferredLocale))] + public string? PreferredLocale { get; set; } } \ No newline at end of file diff --git a/e-suite.API.Common/e-suite.API.Common/models/base/UserBase.cs b/e-suite.API.Common/e-suite.API.Common/models/base/UserBase.cs index f26b1e5..b4040ea 100644 --- a/e-suite.API.Common/e-suite.API.Common/models/base/UserBase.cs +++ b/e-suite.API.Common/e-suite.API.Common/models/base/UserBase.cs @@ -2,7 +2,7 @@ namespace e_suite.API.Common.models.@base; -public abstract class UserBase +public class UserBase { public string FirstName { get; set; } = string.Empty; diff --git a/e-suite.API/eSuite.API.UnitTests/Controllers/ProfileControllerUnitTests/ProfileControllerTestBase.cs b/e-suite.API/eSuite.API.UnitTests/Controllers/ProfileControllerUnitTests/ProfileControllerTestBase.cs index f17b224..09d4489 100644 --- a/e-suite.API/eSuite.API.UnitTests/Controllers/ProfileControllerUnitTests/ProfileControllerTestBase.cs +++ b/e-suite.API/eSuite.API.UnitTests/Controllers/ProfileControllerUnitTests/ProfileControllerTestBase.cs @@ -2,6 +2,7 @@ using e_suite.API.Common; using e_suite.UnitTestCore; using eSuite.API.Controllers; +using eSuite.API.SingleSignOn; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Moq; @@ -12,6 +13,7 @@ public abstract class ProfileControllerTestBase : TestBase { protected ProfileController _profileController = null!; protected Mock _userManagerMock = null!; + protected Mock _cookieManagerMock = null!; protected const string AccessDeniedText = "Access denied"; protected const string BadRequestText = "Bad request"; @@ -23,8 +25,9 @@ public abstract class ProfileControllerTestBase : TestBase await base.Setup(); _userManagerMock = new Mock(); + _cookieManagerMock = new Mock(); - _profileController = new ProfileController(_userManagerMock.Object); + _profileController = new ProfileController(_userManagerMock.Object, _cookieManagerMock.Object); } protected void AddAuthorisedUserToController(long id, string email, string displayName) diff --git a/e-suite.API/eSuite.API/Controllers/AccountController.cs b/e-suite.API/eSuite.API/Controllers/AccountController.cs index 8c91f4a..9337c42 100644 --- a/e-suite.API/eSuite.API/Controllers/AccountController.cs +++ b/e-suite.API/eSuite.API/Controllers/AccountController.cs @@ -159,10 +159,7 @@ public class AccountController : ESuiteController [Route("auth/{ssoId}")] [HttpGet] [AllowAnonymous] -#pragma warning disable IDE0060 // Remove unused parameter - // ReSharper disable once IdentifierTypo public async Task Auth([FromRoute] long ssoId, [FromQuery] string code, [FromQuery] string scope, [FromQuery] string authuser, [FromQuery] string prompt, CancellationToken cancellationToken) -#pragma warning restore IDE0060 // Remove unused parameter { var ssoUserId = await _singleSignOn.ExchangeAuthorisationToken(ssoId, code, cancellationToken); diff --git a/e-suite.API/eSuite.API/Controllers/ProfileController.cs b/e-suite.API/eSuite.API/Controllers/ProfileController.cs index e922261..440e2bf 100644 --- a/e-suite.API/eSuite.API/Controllers/ProfileController.cs +++ b/e-suite.API/eSuite.API/Controllers/ProfileController.cs @@ -1,9 +1,11 @@ using e_suite.API.Common; using e_suite.API.Common.models; using eSuite.API.security; +using eSuite.API.SingleSignOn; using eSuite.API.Utilities; using eSuite.Core.Security; using Microsoft.AspNetCore.Mvc; +using Moq; namespace eSuite.API.Controllers; @@ -15,14 +17,16 @@ namespace eSuite.API.Controllers; public class ProfileController : ESuiteControllerBase { private readonly IUserManager _userManager; + private readonly ICookieManager _cookieManager; /// /// /// /// - public ProfileController(IUserManager userManager) + public ProfileController(IUserManager userManager, ICookieManager cookieManager) { _userManager = userManager; + _cookieManager = cookieManager; } /// @@ -60,4 +64,26 @@ public class ProfileController : ESuiteControllerBase await _userManager.UpdateProfile(AuditUserDetails, User.Email(), userProfile, cancellationToken); return Ok(); } + + /// + /// /// Patching is useful when you only want to update a few fields of the user rather than the whole object. + /// + /// + /// + /// + [Route("myProfile")] + [HttpPatch] + [AccessKey(SecurityAccess.Everyone)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task PatchMyProfile( + [FromBody] PatchUserProfile patchUserProfile, + CancellationToken cancellationToken = default! + ) + { + var loginResponse = await _userManager.PatchProfile(AuditUserDetails, User.Email(), patchUserProfile, cancellationToken); + + await _cookieManager.CreateSessionCookie(Response, loginResponse); + return Ok(); + } } \ No newline at end of file diff --git a/e-suite.API/eSuite.API/Controllers/UserController.cs b/e-suite.API/eSuite.API/Controllers/UserController.cs index 6c2dadb..f6567e2 100644 --- a/e-suite.API/eSuite.API/Controllers/UserController.cs +++ b/e-suite.API/eSuite.API/Controllers/UserController.cs @@ -1,5 +1,6 @@ using e_suite.API.Common; using e_suite.API.Common.models; +using e_suite.API.Common.models.@base; using e_suite.Database.Core.Models; using e_suite.Utilities.Pagination; using eSuite.API.Models; @@ -8,6 +9,8 @@ using eSuite.API.Utilities; using eSuite.Core.Miscellaneous; using eSuite.Core.Security; using Microsoft.AspNetCore.Mvc; +using Moq; +using static System.Runtime.InteropServices.JavaScript.JSType; using IRoleManager = e_suite.API.Common.IRoleManager; namespace eSuite.API.Controllers; @@ -86,6 +89,24 @@ public class UserController : ESuiteControllerBase return Ok(); } + /// + /// Patching is useful when you only want to update a few fields of the user rather than the whole object. + /// + /// + /// + /// + [Route("user")] + [HttpPatch] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [AccessKey(SecurityAccess.EditUser)] + public async Task PatchUser([FromQuery] IGeneralIdRef userId, [FromBody] PatchUser patchUser, CancellationToken cancellationToken = default!) + { + + await _userManager.PatchUser(AuditUserDetails, userId, patchUser, cancellationToken); + return Ok(); + } + /// /// Create a new e-suite user /// diff --git a/e-suite.API/eSuite.API/Extensions/UserProfileExtensions.cs b/e-suite.API/eSuite.API/Extensions/UserProfileExtensions.cs index d47c8a9..597f081 100644 --- a/e-suite.API/eSuite.API/Extensions/UserProfileExtensions.cs +++ b/e-suite.API/eSuite.API/Extensions/UserProfileExtensions.cs @@ -17,7 +17,8 @@ internal static class UserProfileExtensions DomainSsoProviderId = userProfile.DomainSsoProviderId, SsoProviderId = userProfile.SsoProviderId ?? -1, SsoSubject = userProfile.SsoSubject, - SsoProviders = userProfile.SsoProviders + SsoProviders = userProfile.SsoProviders, + PreferredLocale = userProfile.PreferredLocale }; } diff --git a/e-suite.API/eSuite.API/Models/UserProfile.cs b/e-suite.API/eSuite.API/Models/UserProfile.cs index 3ce76d1..b677a85 100644 --- a/e-suite.API/eSuite.API/Models/UserProfile.cs +++ b/e-suite.API/eSuite.API/Models/UserProfile.cs @@ -87,4 +87,7 @@ public class UserProfile : IPasswordInformation /// List of SSO Providers that the user can select. /// public Dictionary SsoProviders { get; set; } = []; + + [Display(Name = "Preferred Locale")] + public string PreferredLocale { get; set; } } diff --git a/e-suite.Database.Core/e-suite.Database.Core/EsuiteDatabaseDbContext.cs b/e-suite.Database.Core/e-suite.Database.Core/EsuiteDatabaseDbContext.cs index 66e52c5..d628fa0 100644 --- a/e-suite.Database.Core/e-suite.Database.Core/EsuiteDatabaseDbContext.cs +++ b/e-suite.Database.Core/e-suite.Database.Core/EsuiteDatabaseDbContext.cs @@ -22,7 +22,15 @@ public class EsuiteDatabaseDbContext : SunDatabaseEntityContext, IEsuiteDatabase public EsuiteDatabaseDbContext(DbContextOptions options, IClock clock) : base(options, clock) { } - + + public IQueryable Set(Type entityType) + { + return (IQueryable)typeof(DbContext) + .GetMethod(nameof(DbContext.Set), Type.EmptyTypes)! + .MakeGenericMethod(entityType) + .Invoke(this, null)!; + } + public DbSet Users { get; set; } = null!; public DbSet SsoProviders { get; set; } = null!; public DbSet SingleUseGuids { get; set; } diff --git a/e-suite.Database.Core/e-suite.Database.Core/IEsuiteDatabaseDbContext.cs b/e-suite.Database.Core/e-suite.Database.Core/IEsuiteDatabaseDbContext.cs index 4be899c..f6ec146 100644 --- a/e-suite.Database.Core/e-suite.Database.Core/IEsuiteDatabaseDbContext.cs +++ b/e-suite.Database.Core/e-suite.Database.Core/IEsuiteDatabaseDbContext.cs @@ -17,5 +17,5 @@ namespace e_suite.Database.Core; public interface IEsuiteDatabaseDbContext : IAudit, ISentinel, IUserManager, IDiagnostics, ISequences, ICustomFields, IForm, IGlossaries, IPrinter, IContact, IMail, IDomain, IMiscellaneous, IWorkflowManager { - + IQueryable Set(Type entityType); } \ No newline at end of file diff --git a/e-suite.Database.Core/e-suite.Database.Core/Tables/UserManager/User.cs b/e-suite.Database.Core/e-suite.Database.Core/Tables/UserManager/User.cs index 701e415..9eff90e 100644 --- a/e-suite.Database.Core/e-suite.Database.Core/Tables/UserManager/User.cs +++ b/e-suite.Database.Core/e-suite.Database.Core/Tables/UserManager/User.cs @@ -81,6 +81,11 @@ public class User : IPassword, IEmailAddress, IGeneralId [ForeignKey(nameof(SsoProviderId))] public virtual SsoProvider? SsoProvider { get; set; } = null!; + [Required] + [MaxLength(20)] + [DefaultValue("en-GB")] + public string PreferredLocale { get; set; } = "en-GB"; + public static void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().HasData(new User @@ -100,7 +105,8 @@ public class User : IPassword, IEmailAddress, IGeneralId DomainId = 1, SsoProviderId = null, SsoSubject = string.Empty, - Guid = new Guid("{30cfcd5b-3385-43f1-b59a-fd35236f3d92}") + Guid = new Guid("{30cfcd5b-3385-43f1-b59a-fd35236f3d92}"), + PreferredLocale = "en-GB" }); } diff --git a/e-suite.Database.SqlServer/e-suite.Database.SqlServer/Migrations/20260127230112_PreferredLocale.Designer.cs b/e-suite.Database.SqlServer/e-suite.Database.SqlServer/Migrations/20260127230112_PreferredLocale.Designer.cs new file mode 100644 index 0000000..bfa3a50 --- /dev/null +++ b/e-suite.Database.SqlServer/e-suite.Database.SqlServer/Migrations/20260127230112_PreferredLocale.Designer.cs @@ -0,0 +1,2082 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using e_suite.Database.SqlServer; + +#nullable disable + +namespace esuite.Database.SqlServer.Migrations +{ + [DbContext(typeof(SqlEsuiteDatabaseDbContext))] + [Migration("20260127230112_PreferredLocale")] + partial class PreferredLocale + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("e_suite.Database.Audit.Tables.Audit.AuditDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Comment") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DateTime") + .HasColumnType("datetimeoffset"); + + b.Property("Fields") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("UserDisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("AuditDetails", "Audit"); + }); + + modelBuilder.Entity("e_suite.Database.Audit.Tables.Audit.AuditDrillHierarchy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChildAuditDrillDownEntityId") + .HasColumnType("bigint"); + + b.Property("ParentAuditDrillDownEntityId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ChildAuditDrillDownEntityId"); + + b.HasIndex("ParentAuditDrillDownEntityId"); + + b.ToTable("AuditDrillHierarchies", "Audit"); + }); + + modelBuilder.Entity("e_suite.Database.Audit.Tables.Audit.AuditEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuditLogId") + .HasColumnType("bigint"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EntityName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsPrimary") + .HasColumnType("bit"); + + b.Property("PrimaryKey") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.ToTable("AuditEntries", "Audit"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Contacts.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("JCard") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("Id") + .IsUnique(); + + b.ToTable("Contacts", "Contacts"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.CustomFields.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("FieldType") + .HasColumnType("int"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("MaxEntries") + .HasColumnType("bigint"); + + b.Property("MinEntries") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("CustomFields", "CustomFields"); + + b.HasData( + new + { + Id = 1L, + DefaultValue = "", + Deleted = false, + FieldType = 7, + Guid = new Guid("8d910089-3079-4a29-abad-8ddf82db6dbb"), + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + MaxEntries = 1L, + MinEntries = 1L, + Name = "Print Specification Form Template" + }); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.CustomFields.CustomFieldFormTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CustomFieldId") + .HasColumnType("bigint"); + + b.Property("FormTemplateId") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("CustomFieldId"); + + b.HasIndex("FormTemplateId"); + + b.ToTable("CustomFieldFormTemplates", "CustomFields"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.CustomFields.CustomFieldGlossary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CustomFieldId") + .HasColumnType("bigint"); + + b.Property("GlossaryId") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("CustomFieldId"); + + b.HasIndex("GlossaryId"); + + b.ToTable("CustomFieldGlossaries", "CustomFields"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.CustomFields.CustomFieldNumber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CustomFieldId") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("MaximumValue") + .HasPrecision(28, 8) + .HasColumnType("decimal(28,8)"); + + b.Property("MinimumValue") + .HasPrecision(28, 8) + .HasColumnType("decimal(28,8)"); + + b.Property("Step") + .HasPrecision(28, 8) + .HasColumnType("decimal(28,8)"); + + b.HasKey("Id"); + + b.HasIndex("CustomFieldId"); + + b.ToTable("CustomFieldNumbers", "CustomFields"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.CustomFields.CustomFieldSequence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CustomFieldId") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("SequenceId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CustomFieldId"); + + b.HasIndex("SequenceId"); + + b.ToTable("CustomFieldSequences", "CustomFields"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.CustomFields.CustomFieldText", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CustomFieldId") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("MultiLine") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("CustomFieldId"); + + b.ToTable("CustomFieldTexts", "CustomFields"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Diagnostics.ExceptionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Application") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ExceptionJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccuredAt") + .HasColumnType("datetimeoffset"); + + b.Property("StackTrace") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SupportingData") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ExceptionLogs", "Diagnostics"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Diagnostics.PerformanceReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActionName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ActionParameters") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ControllerName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Host") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RequestType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartDateTime") + .HasColumnType("datetimeoffset"); + + b.Property("Timings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TotalTimeMS") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("PerformanceReports", "Diagnostics"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Diagnostics.PerformanceThreshold", b => + { + b.Property("ControllerName") + .HasColumnType("nvarchar(450)"); + + b.Property("ActionName") + .HasColumnType("nvarchar(450)"); + + b.Property("RequestType") + .HasColumnType("nvarchar(450)"); + + b.Property("TotalTimeMS") + .HasColumnType("int"); + + b.HasKey("ControllerName", "ActionName", "RequestType"); + + b.ToTable("PerformanceThresholds", "Diagnostics"); + + b.HasData( + new + { + ControllerName = "", + ActionName = "", + RequestType = "", + TotalTimeMS = 2000 + }); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Domain.Domain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SigmaId") + .HasColumnType("bigint"); + + b.Property("SsoProviderId") + .HasColumnType("bigint"); + + b.Property("SunriseAppId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("SunriseCategoryId") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("nvarchar(16)"); + + b.Property("SunriseHostname") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("nvarchar(16)"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("SsoProviderId"); + + b.ToTable("Domains", "Domain"); + + b.HasData( + new + { + Id = 1L, + Deleted = false, + Guid = new Guid("4eb1b0c7-e81b-4c9d-ba91-384ed47a1cf9"), + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Name = "Sun-Strategy", + SunriseAppId = "", + SunriseCategoryId = "", + SunriseHostname = "" + }); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Domain.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CanDelete") + .HasColumnType("bit"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("DomainId") + .HasColumnType("bigint"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("IsAdministrator") + .HasColumnType("bit"); + + b.Property("IsSuperUser") + .HasColumnType("bit"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("DomainId", "Name") + .IsUnique(); + + b.ToTable("Roles", "Domain"); + + b.HasData( + new + { + Id = 1L, + CanDelete = false, + Deleted = false, + DomainId = 1L, + Guid = new Guid("32d65817-4de4-4bd5-912e-2b33054a2171"), + IsAdministrator = false, + IsSuperUser = true, + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Name = "Super Users" + }, + new + { + Id = 2L, + CanDelete = false, + Deleted = false, + DomainId = 1L, + Guid = new Guid("2f77f57c-fe2c-4a36-ad17-5d902afc86cb"), + IsAdministrator = true, + IsSuperUser = false, + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Name = "Administrators" + }); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Domain.RoleAccess", b => + { + b.Property("RoleId") + .HasColumnType("bigint"); + + b.Property("AccessKey") + .HasColumnType("int"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.HasKey("RoleId", "AccessKey"); + + b.ToTable("RoleAccess", "Domain"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Domain.UserAccess", b => + { + b.Property("AccessKey") + .HasColumnType("int"); + + b.Property("DomainId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasIndex("DomainId"); + + b.HasIndex("UserId"); + + b.ToTable("UserAccess", "Domain"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Domain.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", "Domain"); + + b.HasData( + new + { + Id = 1L, + Deleted = false, + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + RoleId = 1L, + UserId = 1L + }); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Forms.FormFieldInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CustomFieldId") + .HasColumnType("bigint"); + + b.Property("DisplayValue") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FormInstanceId") + .HasColumnType("bigint"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CustomFieldId"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("FormInstanceId", "CustomFieldId", "Index") + .IsUnique(); + + b.ToTable("FormFieldInstances", "Forms"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Forms.FormInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("FormTemplateVersionId") + .HasColumnType("bigint"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("FormTemplateVersionId"); + + b.HasIndex("Guid") + .IsUnique(); + + b.ToTable("FormInstances", "Forms"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Forms.FormTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("FormTemplates", "Forms"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Forms.FormTemplateVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("FormDefinition") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("TemplateId") + .HasColumnType("bigint"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("TemplateId", "Version") + .IsUnique(); + + b.ToTable("FormTemplateVersions", "Forms"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Glossaries.Glossary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ParentId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("ParentId"); + + b.HasIndex("Id", "ParentId") + .IsUnique() + .HasFilter("[ParentId] IS NOT NULL"); + + b.ToTable("Glossaries", "Glossaries"); + + b.HasData( + new + { + Id = 1L, + Deleted = false, + Guid = new Guid("fa6566f8-b4b0-48c5-9985-336c9284796e"), + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Name = "System" + }, + new + { + Id = 2L, + Deleted = false, + Guid = new Guid("90c48cf1-1a2b-4a76-b30b-260ecb149ccc"), + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Name = "Domain" + }, + new + { + Id = 3L, + Deleted = false, + Guid = new Guid("35eb8c23-4528-49a7-a798-b8bae3b06daf"), + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Name = "Print Specifications", + ParentId = 1L + }); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Glossaries.GlossaryCustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CustomFieldId") + .HasColumnType("bigint"); + + b.Property("GlossaryId") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasAlternateKey("GlossaryId", "CustomFieldId"); + + b.HasIndex("CustomFieldId"); + + b.ToTable("GlossaryCustomFields", "Glossaries"); + + b.HasData( + new + { + Id = 1L, + CustomFieldId = 1L, + GlossaryId = 3L, + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)) + }); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Glossaries.GlossaryCustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CustomFieldId") + .HasColumnType("bigint"); + + b.Property("DisplayValue") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GlossaryId") + .HasColumnType("bigint"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasAlternateKey("GlossaryId", "CustomFieldId", "Index"); + + b.HasIndex("CustomFieldId"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("GlossaryId", "CustomFieldId", "Index") + .IsUnique(); + + b.ToTable("GlossaryCustomFieldValues", "Glossaries"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Mail.MailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("DomainId") + .HasColumnType("bigint"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("MailType") + .HasColumnType("int"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TemplateDefinition") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("DomainId", "MailType") + .IsUnique(); + + b.ToTable("MailTemplates", "Mail"); + + b.HasData( + new + { + Id = 1L, + Deleted = false, + DomainId = 1L, + Guid = new Guid("32b657af-5e17-47f3-b8dc-59580516b141"), + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + MailType = 0, + Subject = "Confirmation e-mail", + TemplateDefinition = "

Please confirm your e-mail

Welcome to . Please click on this link to confirm your e-mail address is correct." + }, + new + { + Id = 2L, + Deleted = false, + DomainId = 1L, + Guid = new Guid("77fff2b0-dd8c-43e6-a9fd-304c79850f81"), + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + MailType = 2, + Subject = "Password reset", + TemplateDefinition = "

Please follow the link to reset your password


Reset Password" + }, + new + { + Id = 3L, + Deleted = false, + DomainId = 1L, + Guid = new Guid("2e2d84a5-be1c-4e3b-a86a-bdafed42801b"), + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + MailType = 1, + Subject = "Disable two factor authentication", + TemplateDefinition = "

Please follow the link to reset two factor authentication


Disable two factor authentication" + }, + new + { + Id = 4L, + Deleted = false, + DomainId = 1L, + Guid = new Guid("421b67c7-5f4b-4cad-adc5-528e2921790c"), + LastUpdated = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + MailType = 3, + Subject = "Password successfully reset", + TemplateDefinition = "

Your password has been reset. Please contact your admin if this wasn't you.

" + }); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Miscellaneous.ExternalKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("EntityName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ExternalSystemId") + .HasColumnType("bigint"); + + b.Property("PrimaryKey") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("EntityName", "PrimaryKey") + .IsUnique(); + + b.HasIndex("ExternalSystemId", "ExternalId") + .IsUnique(); + + b.ToTable("ExternalKeys", "Miscellaneous"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Miscellaneous.ExternalSystem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("SystemName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("SystemName") + .IsUnique(); + + b.ToTable("ExternalSystems", "Miscellaneous"); + + b.HasData( + new + { + Id = 1L, + SystemName = "e-flow" + }); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Printer.Organisation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("Id") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Organisations", "Printer"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Printer.OrganisationContact", b => + { + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("ContactId") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Primary") + .HasColumnType("bit"); + + b.HasKey("OrganisationId", "ContactId"); + + b.HasIndex("ContactId"); + + b.ToTable("OrganisationContacts", "Printer"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Printer.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganisationId") + .HasColumnType("bigint"); + + b.Property("SigmaId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("OrganisationId"); + + b.HasIndex("SigmaId") + .IsUnique() + .HasFilter("[SigmaId] IS NOT NULL"); + + b.ToTable("Sites", "Printer"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Printer.SiteContact", b => + { + b.Property("SiteId") + .HasColumnType("bigint"); + + b.Property("ContactId") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Primary") + .HasColumnType("bit"); + + b.HasKey("SiteId", "ContactId"); + + b.HasIndex("ContactId"); + + b.ToTable("SiteContacts", "Printer"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Printer.Specification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("FormInstanceId") + .HasColumnType("bigint"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SigmaId") + .HasColumnType("bigint"); + + b.Property("SiteId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("Id") + .IsUnique(); + + b.HasIndex("SigmaId") + .IsUnique() + .HasFilter("[SigmaId] IS NOT NULL"); + + b.HasIndex("SiteId"); + + b.ToTable("Specifications", "Printer"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Sentinel.FailedAccessAttempt", b => + { + b.Property("IPAddress") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("AttemptedTime") + .HasColumnType("datetimeoffset"); + + b.HasKey("IPAddress", "AttemptedTime"); + + b.ToTable("FailedAccessAttempts", "Sentinel"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Sequences.Sequence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("Increment") + .HasColumnType("bigint"); + + b.Property("LastIssueDate") + .HasColumnType("datetimeoffset"); + + b.Property("LastIssueValue") + .HasColumnType("bigint"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Pattern") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Rollover") + .HasColumnType("int"); + + b.Property("Seed") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Sequences", "Sequences"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.UserManager.EmailUserAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("EmailActionType") + .HasColumnType("int"); + + b.Property("Expires") + .HasColumnType("datetimeoffset"); + + b.Property("Token") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasAlternateKey("Token", "EmailActionType"); + + b.HasIndex("UserId"); + + b.ToTable("EmailUserActions", "UserManager"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.UserManager.SingleUseGuid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Expires") + .HasColumnType("datetimeoffset"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("SingleUseGuids", "UserManager"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.UserManager.SsoProvider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AuthorizationEndpoint") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("IsPublic") + .HasColumnType("bit"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TokenEndpoint") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ValidIssuer") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SsoProviders", "UserManager"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.UserManager.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Active") + .HasColumnType("bit"); + + b.Property("Created") + .HasColumnType("datetimeoffset"); + + b.Property("DomainId") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("MiddleNames") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PreferredLocale") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("SsoProviderId") + .HasColumnType("bigint"); + + b.Property("SsoSubject") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorAuthenticationKey") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UsingTwoFactorAuthentication") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("DomainId"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("SsoProviderId"); + + b.ToTable("Users", "UserManager"); + + b.HasData( + new + { + Id = 1L, + Active = true, + Created = new DateTimeOffset(new DateTime(2022, 6, 9, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DomainId = 1L, + Email = "testuser1@sun-strategy.com", + EmailConfirmed = true, + FirstName = "Test1", + Guid = new Guid("30cfcd5b-3385-43f1-b59a-fd35236f3d92"), + LastName = "User", + LastUpdated = new DateTimeOffset(new DateTime(2022, 6, 9, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + MiddleNames = "", + Password = "AgAAAAIAACcQAAAAAAAAABDGmF9Hre4l8k9lkZAxBRJk0zNHD2b6xfKz4C7h6UjuirkoyoP2fVR6d9b7riT03UnL5yAgFh2pSSVQDx+nQ5PBjZRB9UG4u5FrY8W7ouA/+w==", + PreferredLocale = "en-GB", + SsoSubject = "", + TwoFactorAuthenticationKey = "4EHHG42OWCN3L72TSRYSHTV6MRJXVOY3", + UsingTwoFactorAuthentication = false + }); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Workflow.Workflow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Workflows", "Workflow"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Workflow.WorkflowVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActivityNameTemplate") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DomainId") + .HasColumnType("bigint"); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("LastUpdated") + .HasColumnType("datetimeoffset"); + + b.Property("Tasks") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.Property("WorkflowId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DomainId"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("WorkflowId", "Version") + .IsUnique(); + + b.ToTable("WorkflowVersions", "Workflow"); + }); + + modelBuilder.Entity("e_suite.Database.Audit.Tables.Audit.AuditDrillHierarchy", b => + { + b.HasOne("e_suite.Database.Audit.Tables.Audit.AuditEntry", "ChildAuditDrillDownEntity") + .WithMany() + .HasForeignKey("ChildAuditDrillDownEntityId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("e_suite.Database.Audit.Tables.Audit.AuditEntry", "ParentAuditDrillDownEntity") + .WithMany() + .HasForeignKey("ParentAuditDrillDownEntityId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("ChildAuditDrillDownEntity"); + + b.Navigation("ParentAuditDrillDownEntity"); + }); + + modelBuilder.Entity("e_suite.Database.Audit.Tables.Audit.AuditEntry", b => + { + b.HasOne("e_suite.Database.Audit.Tables.Audit.AuditDetail", "AuditLog") + .WithMany() + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AuditLog"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.CustomFields.CustomFieldFormTemplate", b => + { + b.HasOne("e_suite.Database.Core.Tables.CustomFields.CustomField", "CustomField") + .WithMany() + .HasForeignKey("CustomFieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("e_suite.Database.Core.Tables.Forms.FormTemplate", "FormTemplate") + .WithMany() + .HasForeignKey("FormTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CustomField"); + + b.Navigation("FormTemplate"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.CustomFields.CustomFieldGlossary", b => + { + b.HasOne("e_suite.Database.Core.Tables.CustomFields.CustomField", "CustomField") + .WithMany() + .HasForeignKey("CustomFieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("e_suite.Database.Core.Tables.Glossaries.Glossary", "Glossary") + .WithMany() + .HasForeignKey("GlossaryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CustomField"); + + b.Navigation("Glossary"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.CustomFields.CustomFieldNumber", b => + { + b.HasOne("e_suite.Database.Core.Tables.CustomFields.CustomField", "CustomField") + .WithMany() + .HasForeignKey("CustomFieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CustomField"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.CustomFields.CustomFieldSequence", b => + { + b.HasOne("e_suite.Database.Core.Tables.CustomFields.CustomField", "CustomField") + .WithMany() + .HasForeignKey("CustomFieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("e_suite.Database.Core.Tables.Sequences.Sequence", "Sequence") + .WithMany() + .HasForeignKey("SequenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CustomField"); + + b.Navigation("Sequence"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.CustomFields.CustomFieldText", b => + { + b.HasOne("e_suite.Database.Core.Tables.CustomFields.CustomField", "CustomField") + .WithMany() + .HasForeignKey("CustomFieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CustomField"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Domain.Domain", b => + { + b.HasOne("e_suite.Database.Core.Tables.UserManager.SsoProvider", "SsoProvider") + .WithMany() + .HasForeignKey("SsoProviderId"); + + b.Navigation("SsoProvider"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Domain.Role", b => + { + b.HasOne("e_suite.Database.Core.Tables.Domain.Domain", "Domain") + .WithMany() + .HasForeignKey("DomainId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Domain"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Domain.RoleAccess", b => + { + b.HasOne("e_suite.Database.Core.Tables.Domain.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Domain.UserAccess", b => + { + b.HasOne("e_suite.Database.Core.Tables.Domain.Domain", "Domain") + .WithMany() + .HasForeignKey("DomainId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("e_suite.Database.Core.Tables.UserManager.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Domain"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Domain.UserRole", b => + { + b.HasOne("e_suite.Database.Core.Tables.Domain.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("e_suite.Database.Core.Tables.UserManager.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Forms.FormFieldInstance", b => + { + b.HasOne("e_suite.Database.Core.Tables.CustomFields.CustomField", "CustomField") + .WithMany() + .HasForeignKey("CustomFieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("e_suite.Database.Core.Tables.Forms.FormInstance", "FormInstance") + .WithMany("FormFields") + .HasForeignKey("FormInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CustomField"); + + b.Navigation("FormInstance"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Forms.FormInstance", b => + { + b.HasOne("e_suite.Database.Core.Tables.Forms.FormTemplateVersion", "FormTemplateVersion") + .WithMany() + .HasForeignKey("FormTemplateVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FormTemplateVersion"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Forms.FormTemplateVersion", b => + { + b.HasOne("e_suite.Database.Core.Tables.Forms.FormTemplate", "Template") + .WithMany("Versions") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Glossaries.Glossary", b => + { + b.HasOne("e_suite.Database.Core.Tables.Glossaries.Glossary", "Parent") + .WithMany() + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Glossaries.GlossaryCustomField", b => + { + b.HasOne("e_suite.Database.Core.Tables.CustomFields.CustomField", "CustomField") + .WithMany() + .HasForeignKey("CustomFieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("e_suite.Database.Core.Tables.Glossaries.Glossary", "Glossary") + .WithMany("CustomFieldDefinitions") + .HasForeignKey("GlossaryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CustomField"); + + b.Navigation("Glossary"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Glossaries.GlossaryCustomFieldValue", b => + { + b.HasOne("e_suite.Database.Core.Tables.CustomFields.CustomField", "CustomField") + .WithMany() + .HasForeignKey("CustomFieldId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("e_suite.Database.Core.Tables.Glossaries.Glossary", "Glossary") + .WithMany("CustomFieldValues") + .HasForeignKey("GlossaryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CustomField"); + + b.Navigation("Glossary"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Mail.MailTemplate", b => + { + b.HasOne("e_suite.Database.Core.Tables.Domain.Domain", "Domain") + .WithMany() + .HasForeignKey("DomainId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Domain"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Miscellaneous.ExternalKey", b => + { + b.HasOne("e_suite.Database.Core.Tables.Miscellaneous.ExternalSystem", "ExternalSystem") + .WithMany() + .HasForeignKey("ExternalSystemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExternalSystem"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Printer.OrganisationContact", b => + { + b.HasOne("e_suite.Database.Core.Tables.Contacts.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("e_suite.Database.Core.Tables.Printer.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + + b.Navigation("Organisation"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Printer.Site", b => + { + b.HasOne("e_suite.Database.Core.Tables.Printer.Organisation", "Organisation") + .WithMany("Sites") + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organisation"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Printer.SiteContact", b => + { + b.HasOne("e_suite.Database.Core.Tables.Contacts.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("e_suite.Database.Core.Tables.Printer.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Printer.Specification", b => + { + b.HasOne("e_suite.Database.Core.Tables.Printer.Site", "Site") + .WithMany("Specifications") + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.UserManager.EmailUserAction", b => + { + b.HasOne("e_suite.Database.Core.Tables.UserManager.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.UserManager.SingleUseGuid", b => + { + b.HasOne("e_suite.Database.Core.Tables.UserManager.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.UserManager.User", b => + { + b.HasOne("e_suite.Database.Core.Tables.Domain.Domain", "Domain") + .WithMany() + .HasForeignKey("DomainId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("e_suite.Database.Core.Tables.UserManager.SsoProvider", "SsoProvider") + .WithMany() + .HasForeignKey("SsoProviderId"); + + b.Navigation("Domain"); + + b.Navigation("SsoProvider"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Workflow.WorkflowVersion", b => + { + b.HasOne("e_suite.Database.Core.Tables.Domain.Domain", "Domain") + .WithMany() + .HasForeignKey("DomainId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("e_suite.Database.Core.Tables.Workflow.Workflow", "Workflow") + .WithMany() + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Domain"); + + b.Navigation("Workflow"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Forms.FormInstance", b => + { + b.Navigation("FormFields"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Forms.FormTemplate", b => + { + b.Navigation("Versions"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Glossaries.Glossary", b => + { + b.Navigation("CustomFieldDefinitions"); + + b.Navigation("CustomFieldValues"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Printer.Organisation", b => + { + b.Navigation("Sites"); + }); + + modelBuilder.Entity("e_suite.Database.Core.Tables.Printer.Site", b => + { + b.Navigation("Specifications"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/e-suite.Database.SqlServer/e-suite.Database.SqlServer/Migrations/20260127230112_PreferredLocale.cs b/e-suite.Database.SqlServer/e-suite.Database.SqlServer/Migrations/20260127230112_PreferredLocale.cs new file mode 100644 index 0000000..df7466d --- /dev/null +++ b/e-suite.Database.SqlServer/e-suite.Database.SqlServer/Migrations/20260127230112_PreferredLocale.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace esuite.Database.SqlServer.Migrations +{ + /// + public partial class PreferredLocale : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PreferredLocale", + schema: "UserManager", + table: "Users", + type: "nvarchar(20)", + maxLength: 20, + nullable: false, + defaultValue: ""); + + migrationBuilder.UpdateData( + schema: "UserManager", + table: "Users", + keyColumn: "Id", + keyValue: 1L, + column: "PreferredLocale", + value: "en-GB"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PreferredLocale", + schema: "UserManager", + table: "Users"); + } + } +} diff --git a/e-suite.Database.SqlServer/e-suite.Database.SqlServer/Migrations/SqlEsuiteDatabaseDbContextModelSnapshot.cs b/e-suite.Database.SqlServer/e-suite.Database.SqlServer/Migrations/SqlEsuiteDatabaseDbContextModelSnapshot.cs index 6e790d9..146bcd8 100644 --- a/e-suite.Database.SqlServer/e-suite.Database.SqlServer/Migrations/SqlEsuiteDatabaseDbContextModelSnapshot.cs +++ b/e-suite.Database.SqlServer/e-suite.Database.SqlServer/Migrations/SqlEsuiteDatabaseDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace esuite.Database.SqlServer.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.12") + .HasAnnotation("ProductVersion", "10.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -1503,6 +1503,11 @@ namespace esuite.Database.SqlServer.Migrations .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("PreferredLocale") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + b.Property("SsoProviderId") .HasColumnType("bigint"); @@ -1546,6 +1551,7 @@ namespace esuite.Database.SqlServer.Migrations LastUpdated = new DateTimeOffset(new DateTime(2022, 6, 9, 12, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), MiddleNames = "", Password = "AgAAAAIAACcQAAAAAAAAABDGmF9Hre4l8k9lkZAxBRJk0zNHD2b6xfKz4C7h6UjuirkoyoP2fVR6d9b7riT03UnL5yAgFh2pSSVQDx+nQ5PBjZRB9UG4u5FrY8W7ouA/+w==", + PreferredLocale = "en-GB", SsoSubject = "", TwoFactorAuthenticationKey = "4EHHG42OWCN3L72TSRYSHTV6MRJXVOY3", UsingTwoFactorAuthentication = false diff --git a/e-suite.Modules.FormsManager/e_suite.Modules.Form.ManagerUnitTest/FormsManagerUnitTestBase.cs b/e-suite.Modules.FormsManager/e_suite.Modules.Form.ManagerUnitTest/FormsManagerUnitTestBase.cs index f26851d..c42c500 100644 --- a/e-suite.Modules.FormsManager/e_suite.Modules.Form.ManagerUnitTest/FormsManagerUnitTestBase.cs +++ b/e-suite.Modules.FormsManager/e_suite.Modules.Form.ManagerUnitTest/FormsManagerUnitTestBase.cs @@ -1,6 +1,7 @@ using e_suite.API.Common; using e_suite.API.Common.repository; using e_suite.Database.Audit; +using e_suite.Database.Audit.AuditEngine; using e_suite.Database.Core.Tables.CustomFields; using e_suite.UnitTestCore; using eSuite.Core.Miscellaneous; @@ -70,4 +71,9 @@ public class FakeCustomFieldRepository : ICustomFieldRepository { throw new NotImplementedException(); } + + public Task AddAdhocAuditEntry(AuditUserDetails auditUserDetails, AuditType auditType, Dictionary fields, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/e-suite.Modules.PerformanceManager/e-suite.Modules.PerformanceManager/obj/e-suite.Modules.PerformanceManager.csproj.nuget.dgspec.json b/e-suite.Modules.PerformanceManager/e-suite.Modules.PerformanceManager/obj/e-suite.Modules.PerformanceManager.csproj.nuget.dgspec.json index 11acce9..818e31e 100644 --- a/e-suite.Modules.PerformanceManager/e-suite.Modules.PerformanceManager/obj/e-suite.Modules.PerformanceManager.csproj.nuget.dgspec.json +++ b/e-suite.Modules.PerformanceManager/e-suite.Modules.PerformanceManager/obj/e-suite.Modules.PerformanceManager.csproj.nuget.dgspec.json @@ -36,6 +36,9 @@ "C:\\Users\\me\\OneDrive\\Code\\Gitea\\e-suite\\e-suite.Backend\\e-suite.Database.Core\\e-suite.Database.Core\\e-suite.Database.Core.csproj": { "projectPath": "C:\\Users\\me\\OneDrive\\Code\\Gitea\\e-suite\\e-suite.Backend\\e-suite.Database.Core\\e-suite.Database.Core\\e-suite.Database.Core.csproj" }, + "C:\\Users\\me\\OneDrive\\Code\\Gitea\\e-suite\\e-suite.Backend\\e-suite.DependencyInjection\\e-suite.DependencyInjection.csproj": { + "projectPath": "C:\\Users\\me\\OneDrive\\Code\\Gitea\\e-suite\\e-suite.Backend\\e-suite.DependencyInjection\\e-suite.DependencyInjection.csproj" + }, "C:\\Users\\me\\OneDrive\\Code\\Gitea\\e-suite\\e-suite.Backend\\e-suite.Utilities.Pagination\\e-suite.Utilities.Pagination\\e-suite.Utilities.Pagination.csproj": { "projectPath": "C:\\Users\\me\\OneDrive\\Code\\Gitea\\e-suite\\e-suite.Backend\\e-suite.Utilities.Pagination\\e-suite.Utilities.Pagination\\e-suite.Utilities.Pagination.csproj" } diff --git a/e-suite.Modules.PerformanceManager/e-suite.Modules.PerformanceManager/obj/project.assets.json b/e-suite.Modules.PerformanceManager/e-suite.Modules.PerformanceManager/obj/project.assets.json index e213b92..dea9a08 100644 --- a/e-suite.Modules.PerformanceManager/e-suite.Modules.PerformanceManager/obj/project.assets.json +++ b/e-suite.Modules.PerformanceManager/e-suite.Modules.PerformanceManager/obj/project.assets.json @@ -398,6 +398,7 @@ "dependencies": { "Microsoft.Extensions.Configuration.Binder": "10.0.2", "e-suite.Database.Core": "1.0.0", + "e-suite.DependencyInjection": "1.0.0", "e-suite.Utilities.Pagination": "1.0.0" }, "compile": { diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/Helpers/UserManagerTestBase.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/Helpers/UserManagerTestBase.cs index 085edf7..8a35e47 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/Helpers/UserManagerTestBase.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/Helpers/UserManagerTestBase.cs @@ -1,4 +1,5 @@ using e_suite.API.Common; +using e_suite.API.Common.models; using e_suite.Database.Audit; using e_suite.Modules.UserManager.Facade; using e_suite.Modules.UserManager.Services; @@ -12,7 +13,7 @@ using UserManager.UnitTests.Repository; namespace UserManager.UnitTests.Helpers; -public class UserManagerTestBase : TestBase +public class UserManagerTestBase : TestBase { protected Mock> CustomPasswordHasherMock = null!; protected Mock TwoFactorAuthenticatorMock = null!; @@ -21,6 +22,8 @@ public class UserManagerTestBase : TestBase protected Mock RandomNumberGeneratorMock = null!; protected FakeUserManagerRepository UserManagerRepository = null!; protected FakeDomainRepository DomainRepository = null!; + protected Mock PatchFactoryMock = null!; + protected Mock> PatchMock = null!; protected AuditUserDetails AuditUserDetails = null!; protected SetupCode SetupCode = null!; @@ -52,8 +55,14 @@ public class UserManagerTestBase : TestBase RandomNumberGeneratorMock = new Mock(); UserManagerRepository = new FakeUserManagerRepository(_fakeClock); DomainRepository = new FakeDomainRepository(); + + PatchFactoryMock = new Mock(); + PatchMock = new Mock>(); + PatchFactoryMock + .Setup(f => f.Create(It.IsAny())) + .Returns(PatchMock.Object); UserManager = new e_suite.Modules.UserManager.UserManager(_configuration, CustomPasswordHasherMock.Object, - TwoFactorAuthenticatorMock.Object, JwtServiceMock.Object, MailServiceMock.Object, RandomNumberGeneratorMock.Object, _fakeClock, UserManagerRepository, DomainRepository); + TwoFactorAuthenticatorMock.Object, JwtServiceMock.Object, MailServiceMock.Object, RandomNumberGeneratorMock.Object, _fakeClock, UserManagerRepository, DomainRepository, PatchFactoryMock.Object); } } \ No newline at end of file diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/CompleteEmailActionUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/CompleteEmailActionUnitTests.cs index 1e5bc80..a21aa90 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/CompleteEmailActionUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/CompleteEmailActionUnitTests.cs @@ -10,7 +10,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class CompleteEmailActionUnitTests : UserManagerTestBase +public class CompleteEmailActionUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/CreateSingleUseGuidUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/CreateSingleUseGuidUnitTests.cs index 9e9473a..cdfeff2 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/CreateSingleUseGuidUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/CreateSingleUseGuidUnitTests.cs @@ -7,7 +7,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class CreateSingleUseGuidUnitTests : UserManagerTestBase +public class CreateSingleUseGuidUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/CreateUserUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/CreateUserUnitTests.cs index 7a96675..ff52a65 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/CreateUserUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/CreateUserUnitTests.cs @@ -11,7 +11,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class CreateUserUnitTests : UserManagerTestBase +public class CreateUserUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/DeactivateUserUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/DeactivateUserUnitTests.cs index f589d1f..3ab71b7 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/DeactivateUserUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/DeactivateUserUnitTests.cs @@ -8,7 +8,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class DeactivateUserUnitTests : UserManagerTestBase +public class DeactivateUserUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/EditUserUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/EditUserUnitTests.cs index c4516b0..3a26d04 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/EditUserUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/EditUserUnitTests.cs @@ -1,16 +1,16 @@ -using System.Data; -using e_suite.API.Common.exceptions; +using e_suite.API.Common.exceptions; using e_suite.API.Common.models; using e_suite.Database.Core.Extensions; using e_suite.Database.Core.Tables.UserManager; using eSuite.Core.Miscellaneous; using NUnit.Framework; +using System.Data; using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class EditUserUnitTests : UserManagerTestBase +public class EditUserUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/ForgotPasswordUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/ForgotPasswordUnitTests.cs index e4915c9..5757af5 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/ForgotPasswordUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/ForgotPasswordUnitTests.cs @@ -9,7 +9,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class ForgotPasswordUnitTests : UserManagerTestBase +public class ForgotPasswordUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetCurrentEmailActionUrlUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetCurrentEmailActionUrlUnitTests.cs index db804f2..5fab061 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetCurrentEmailActionUrlUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetCurrentEmailActionUrlUnitTests.cs @@ -7,7 +7,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class GetCurrentEmailActionUrlUnitTests : UserManagerTestBase +public class GetCurrentEmailActionUrlUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetProfileUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetProfileUnitTests.cs index b6b67b0..bf82d9c 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetProfileUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetProfileUnitTests.cs @@ -8,7 +8,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class GetProfileUnitTests : UserManagerTestBase +public class GetProfileUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetSsoProviderByIdUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetSsoProviderByIdUnitTests.cs index 55d55be..40fbcef 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetSsoProviderByIdUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetSsoProviderByIdUnitTests.cs @@ -4,7 +4,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class GetSsoProviderByIdUnitTests : UserManagerTestBase +public class GetSsoProviderByIdUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetSsoProviderForEmailUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetSsoProviderForEmailUnitTests.cs index 2b07169..0313dd5 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetSsoProviderForEmailUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetSsoProviderForEmailUnitTests.cs @@ -6,7 +6,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class GetSsoProviderForEmailUnitTests : UserManagerTestBase +public class GetSsoProviderForEmailUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUserAsyncUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUserAsyncUnitTests.cs index 321e4fa..2231721 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUserAsyncUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUserAsyncUnitTests.cs @@ -6,7 +6,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class GetUserAsyncUnitTests : UserManagerTestBase +public class GetUserAsyncUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUserByEmailAsyncUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUserByEmailAsyncUnitTests.cs index e2e2e6e..590f518 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUserByEmailAsyncUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUserByEmailAsyncUnitTests.cs @@ -5,7 +5,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class GetUserByEmailAsyncUnitTests : UserManagerTestBase +public class GetUserByEmailAsyncUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUserWithSingleUseGuidUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUserWithSingleUseGuidUnitTests.cs index dddf1c2..a06edb7 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUserWithSingleUseGuidUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUserWithSingleUseGuidUnitTests.cs @@ -5,7 +5,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class GetUserWithSingleUseGuidUnitTests : UserManagerTestBase +public class GetUserWithSingleUseGuidUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUsersAsyncUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUsersAsyncUnitTests.cs index e61e563..211fdaa 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUsersAsyncUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/GetUsersAsyncUnitTests.cs @@ -5,7 +5,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class GetUsersAsyncUnitTests : UserManagerTestBase +public class GetUsersAsyncUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/LinkSsoProfileToUserUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/LinkSsoProfileToUserUnitTests.cs index aa877ca..dea9efb 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/LinkSsoProfileToUserUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/LinkSsoProfileToUserUnitTests.cs @@ -7,7 +7,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class LinkSsoProfileToUserUnitTests : UserManagerTestBase +public class LinkSsoProfileToUserUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/LoginSsoUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/LoginSsoUnitTests.cs index 23a09d8..e359cbc 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/LoginSsoUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/LoginSsoUnitTests.cs @@ -6,7 +6,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class LoginSsoUnitTests : UserManagerTestBase +public class LoginSsoUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/LoginUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/LoginUnitTests.cs index 2d6268e..2a98651 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/LoginUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/LoginUnitTests.cs @@ -11,7 +11,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class LoginUnitTests : UserManagerTestBase +public class LoginUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/PatchProfileUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/PatchProfileUnitTests.cs new file mode 100644 index 0000000..ec0513b --- /dev/null +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/PatchProfileUnitTests.cs @@ -0,0 +1,52 @@ +using e_suite.API.Common; +using e_suite.API.Common.models; +using e_suite.Database.Core.Tables.UserManager; +using Moq; +using NUnit.Framework; +using UserManager.UnitTests.Helpers; + +namespace UserManager.UnitTests.UserManager; + +[TestFixture] +public class PatchProfileUnitTests : UserManagerTestBase +{ + [SetUp] + public override async Task Setup() + { + await base.Setup(); + } + + [Test] + public async Task PatchProfile_AppliesPatchToUser() + { + // Arrange + var dto = new PatchUserProfile + { + FirstName = "Updated" + }; + + const string existingEmail = "testuser@sun-strategy.com"; + var existingUser = new User + { + Id = 12, + Email = existingEmail + }; + await UserManagerRepository.AddUser(AuditUserDetails, existingUser, default); + + PatchMock + .Setup(p => p.ApplyTo(It.IsAny())) + .Callback(target => + { + ((User)target).FirstName = "Updated"; + }); + + // Act + await UserManager.PatchProfile(AuditUserDetails, existingUser.Email, dto, CancellationToken.None); + + // Assert + PatchFactoryMock.Verify(f => f.Create(dto), Times.Once); + PatchMock.Verify(p => p.ApplyTo(It.IsAny()), Times.Once); + + Assert.That(existingUser.FirstName, Is.EqualTo("Updated")); + } +} \ No newline at end of file diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/PatchUserUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/PatchUserUnitTests.cs new file mode 100644 index 0000000..c099c3c --- /dev/null +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/PatchUserUnitTests.cs @@ -0,0 +1,129 @@ +using e_suite.API.Common; +using e_suite.API.Common.models; +using e_suite.Database.Core.Extensions; +using e_suite.Database.Core.Tables.Domain; +using e_suite.Database.Core.Tables.UserManager; +using eSuite.Core.Miscellaneous; +using Moq; +using NUnit.Framework; +using UserManager.UnitTests.Helpers; + +namespace UserManager.UnitTests.UserManager; + +[TestFixture] +public class PatchUserUnitTests : UserManagerTestBase +{ + [SetUp] + public override async Task Setup() + { + await base.Setup(); + } + + [Test] + public async Task PatchUser_AppliesPatchToUser() + { + // Arrange + var existingUser = new User + { + Id = 123, + FirstName = "Original", + LastName = "User", + Active = true + }; + + // Seed the fake repository + await UserManagerRepository.AddUser(AuditUserDetails, existingUser, default); + + var dto = new PatchUser + { + FirstName = "Updated" + }; + + // Configure the patch mock to mutate the user + PatchMock + .Setup(p => p.ApplyTo(It.IsAny())) + .Callback(target => + { + var user = (User)target; + user.FirstName = "Updated"; + }); + + // Act + await UserManager.PatchUser(AuditUserDetails, existingUser.ToGeneralIdRef()!, dto, CancellationToken.None); + + // Assert: factory was used + PatchFactoryMock.Verify(f => f.Create(dto), Times.Once); + + // Assert: patch was applied + PatchMock.Verify(p => p.ApplyTo(It.IsAny()), Times.Once); + + // Assert: user was updated + Assert.That(existingUser.FirstName, Is.EqualTo("Updated")); + } + + [Test] + public async Task PatchUser_UpdatesDomain_WhenDomainRefProvided() + { + // Arrange + var existingDomain = new Domain + { + Id = 10, + Guid = Guid.NewGuid(), + Name = "Original Domain" + }; + + var newDomain = new Domain + { + Id = 20, + Guid = Guid.NewGuid(), + Name = "New Domain" + }; + + // Seed both domains into the fake repository + DomainRepository.Domains.Add(existingDomain); + DomainRepository.Domains.Add(newDomain); + + var existingUser = new User + { + Id = 123, + FirstName = "Original", + Active = true, + DomainId = existingDomain.Id, + Domain = existingDomain + }; + UserManagerRepository.Users.Add(existingUser); + + var dto = new PatchUser + { + Domain = new GeneralIdRef { Id = newDomain.Id } + }; + + var userId = new GeneralIdRef { Id = 123 }; + + // Patch engine should not touch Domain (nested object) + PatchMock + .Setup(p => p.ApplyTo(It.IsAny())) + .Callback(target => + { + ((User)target).DomainId = newDomain.Id; + ((User)target).Domain = newDomain; + }); + + PatchFactoryMock + .Setup(f => f.Create(dto)) + .Returns(PatchMock.Object); + + // Act + await UserManager.PatchUser(AuditUserDetails, userId, dto, CancellationToken.None); + + // Assert: factory was used + PatchFactoryMock.Verify(f => f.Create(dto), Times.Once); + + // Assert: patch engine was invoked + PatchMock.Verify(p => p.ApplyTo(It.IsAny()), Times.Once); + + // Assert: domain was updated + Assert.That(existingUser.DomainId, Is.EqualTo(newDomain.Id)); + Assert.That(existingUser.Domain, Is.EqualTo(newDomain)); + } +} \ No newline at end of file diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/RefreshTokenByEmailUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/RefreshTokenByEmailUnitTests.cs index 2bd813c..4f830cf 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/RefreshTokenByEmailUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/RefreshTokenByEmailUnitTests.cs @@ -6,7 +6,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class RefreshTokenByEmailUnitTests : UserManagerTestBase +public class RefreshTokenByEmailUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/RefreshTokenByIdUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/RefreshTokenByIdUnitTests.cs index f533409..c1a0e8a 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/RefreshTokenByIdUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/RefreshTokenByIdUnitTests.cs @@ -7,7 +7,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class RefreshTokenByIdUnitTests : UserManagerTestBase +public class RefreshTokenByIdUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/ResendConfirmEmailUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/ResendConfirmEmailUnitTests.cs index 7433aeb..69a113e 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/ResendConfirmEmailUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/ResendConfirmEmailUnitTests.cs @@ -8,7 +8,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class ResendConfirmEmailUnitTests : UserManagerTestBase +public class ResendConfirmEmailUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/SetAuthenticationUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/SetAuthenticationUnitTests.cs index 7c674ad..9af5143 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/SetAuthenticationUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/SetAuthenticationUnitTests.cs @@ -9,7 +9,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class SetAuthenticationUnitTests : UserManagerTestBase +public class SetAuthenticationUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/TurnOfSsoForUserUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/TurnOfSsoForUserUnitTests.cs index eb3f9d2..203090f 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/TurnOfSsoForUserUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/TurnOfSsoForUserUnitTests.cs @@ -7,7 +7,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class TurnOfSsoForUserUnitTests : UserManagerTestBase +public class TurnOfSsoForUserUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/UpdateProfileUnitTests.cs b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/UpdateProfileUnitTests.cs index da6f11f..85453fa 100644 --- a/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/UpdateProfileUnitTests.cs +++ b/e-suite.Modules.UserManager/UserManager.UnitTests/UserManager/UpdateProfileUnitTests.cs @@ -10,7 +10,7 @@ using UserManager.UnitTests.Helpers; namespace UserManager.UnitTests.UserManager; [TestFixture] -public class UpdateProfileUnitTests : UserManagerTestBase +public class UpdateProfileUnitTests : UserManagerTestBase { [SetUp] public override async Task Setup() diff --git a/e-suite.Modules.UserManager/e-suite.Modules.UserManager/Services/ESuiteClaimTypes.cs b/e-suite.Modules.UserManager/e-suite.Modules.UserManager/Services/ESuiteClaimTypes.cs index 75a83ca..4c03178 100644 --- a/e-suite.Modules.UserManager/e-suite.Modules.UserManager/Services/ESuiteClaimTypes.cs +++ b/e-suite.Modules.UserManager/e-suite.Modules.UserManager/Services/ESuiteClaimTypes.cs @@ -5,4 +5,6 @@ public static class ESuiteClaimTypes public const string DomainId = "domainid"; public const string SecurityPrivileges = "securityPrivileges"; + + public const string PreferredLocale = "preferredLocale"; } \ No newline at end of file diff --git a/e-suite.Modules.UserManager/e-suite.Modules.UserManager/Services/JwtService.cs b/e-suite.Modules.UserManager/e-suite.Modules.UserManager/Services/JwtService.cs index c52fa79..3fb4ec6 100644 --- a/e-suite.Modules.UserManager/e-suite.Modules.UserManager/Services/JwtService.cs +++ b/e-suite.Modules.UserManager/e-suite.Modules.UserManager/Services/JwtService.cs @@ -55,7 +55,8 @@ public class JwtService : IJwtService new Claim(ClaimTypes.Name, user.DisplayName), new Claim(ESuiteClaimTypes.DomainId, user.Domain.Id.ToString()), //todo remove this from the token, and make it a separate call, so that I can keep the token small and keep the application running fast - new Claim(ESuiteClaimTypes.SecurityPrivileges, GetSecurityPrivileges(user)) + new Claim(ESuiteClaimTypes.SecurityPrivileges, GetSecurityPrivileges(user)), + new Claim(ESuiteClaimTypes.PreferredLocale, user.PreferredLocale) }), Expires = DateTime.UtcNow.AddMinutes(double.Parse(_expDate)), diff --git a/e-suite.Modules.UserManager/e-suite.Modules.UserManager/UserManager.cs b/e-suite.Modules.UserManager/e-suite.Modules.UserManager/UserManager.cs index 24794ed..77b51a2 100644 --- a/e-suite.Modules.UserManager/e-suite.Modules.UserManager/UserManager.cs +++ b/e-suite.Modules.UserManager/e-suite.Modules.UserManager/UserManager.cs @@ -21,6 +21,7 @@ using System.Text.Json; using e_suite.Modules.UserManager.Extensions; using IUserManager = e_suite.API.Common.IUserManager; using System.Data; +using e_suite.API.Common; namespace e_suite.Modules.UserManager; @@ -33,11 +34,13 @@ public partial class UserManager : IUserManager private readonly IMailService _mailService; private readonly IRandomNumberGenerator _randomNumberGenerator; private readonly IClock _clock; + private readonly IPatchFactory _patchFactory; private readonly IUserManagerRepository _userManagerRepository; private readonly IDomainRepository _domainRepository; + - public UserManager(IConfiguration configuration, IPasswordHasher passwordHasher, ITwoFactorAuthenticator twoFactorAuthenticator, IJwtService jwtService, IMailService mailService, IRandomNumberGenerator randomNumberGenerator, IClock clock, IUserManagerRepository userManagerRepository, IDomainRepository domainRepository) + public UserManager(IConfiguration configuration, IPasswordHasher passwordHasher, ITwoFactorAuthenticator twoFactorAuthenticator, IJwtService jwtService, IMailService mailService, IRandomNumberGenerator randomNumberGenerator, IClock clock, IUserManagerRepository userManagerRepository, IDomainRepository domainRepository, IPatchFactory patchFactory) { _configuration = configuration; _passwordHasher = passwordHasher; @@ -48,6 +51,7 @@ public partial class UserManager : IUserManager _clock = clock; _userManagerRepository = userManagerRepository; _domainRepository = domainRepository; + _patchFactory = patchFactory; } private void HashPassword(User user, string password) @@ -468,7 +472,8 @@ public partial class UserManager : IUserManager SsoProviderId = user.SsoProviderId, SsoSubject = user.SsoSubject, SsoProviders = ssoProviders - .ToDictionary(x => x.Id, x => x.Name) + .ToDictionary(x => x.Id, x => x.Name), + PreferredLocale = user.PreferredLocale }; return userProfile; @@ -483,9 +488,8 @@ public partial class UserManager : IUserManager public async Task UpdateProfile(AuditUserDetails auditUserDetails, string email, UpdatedUserProfile userProfile, CancellationToken cancellationToken) { - await _userManagerRepository.TransactionAsync(async () => + await AlterProfile(auditUserDetails, email, async user => { - var user = await GetUserByEmailAsync(email, cancellationToken); userProfile.Email = userProfile.Email.Trim(); if (!string.IsNullOrWhiteSpace(userProfile.Email)) @@ -504,18 +508,52 @@ public partial class UserManager : IUserManager user.MiddleNames = userProfile.MiddleNames; user.LastName = userProfile.LastName; user.Email = userProfile.Email; + user.PreferredLocale = userProfile.PreferredLocale; await SetInternalAuthenticationDetails(user, userProfile, true, cancellationToken); + }, + cancellationToken ); + } + public async Task PatchProfile( AuditUserDetails auditUserDetails, string email, PatchUserProfile patchUserProfile, CancellationToken cancellationToken) + { + var patch = _patchFactory.Create(patchUserProfile); + + var user = await AlterProfile(auditUserDetails, email, async user => + { + await patch.ApplyTo(user); + }, + cancellationToken); + + return CreateLoginResponse(user); + } + + private async Task AlterProfile( + AuditUserDetails auditUserDetails, + string email, + Func applyChanges, + CancellationToken cancellationToken + ) + { + User user = null!; + await _userManagerRepository.TransactionAsync(async () => + { + user = await GetUserByEmailAsync(email, cancellationToken); + + await applyChanges(user); + await _userManagerRepository.EditUser(auditUserDetails, user, cancellationToken); if (!user.EmailConfirmed) { - await SendEmailUserAction(auditUserDetails, user, EmailUserActionType.ConfirmEmailAddress, cancellationToken); + await SendEmailUserAction(auditUserDetails, user, EmailUserActionType.ConfirmEmailAddress, + cancellationToken); } + }); - } + return user; + } private async Task SetInternalAuthenticationDetails( User user, UpdatedUserProfile userProfile, @@ -546,30 +584,62 @@ public partial class UserManager : IUserManager } } - public async Task EditUser(AuditUserDetails auditUserDetails, EditUser user, CancellationToken cancellationToken) + public async Task EditUser( + AuditUserDetails auditUserDetails, + EditUser user, + CancellationToken cancellationToken) { - await _userManagerRepository.TransactionAsync(async () => + await AlterUser(auditUserDetails, user.Id, async editUser => { - var editUser = await _userManagerRepository.GetUserById(user.Id! , cancellationToken) - ?? throw new NotFoundException("unable to find user"); - - if (!editUser.Active) - throw new DeletedRowInaccessibleException("This user is inactive so cannot be modified. You will need to create the user again."); - - var userDomain = await _userManagerRepository.GetDomainById(user.Domain, cancellationToken) + var userDomain = await _userManagerRepository.GetDomainById(user.Domain, cancellationToken) ?? throw new NotFoundException("unable to find domain"); editUser.FirstName = user.FirstName; editUser.MiddleNames = user.MiddleNames; editUser.LastName = user.LastName; - editUser.Email = user.Email; + editUser.Email = user.Email; editUser.DomainId = userDomain.Id; editUser.Domain = userDomain; - await _userManagerRepository.EditUser(auditUserDetails, editUser, cancellationToken); + }, cancellationToken); + } + + public async Task PatchUser( + AuditUserDetails auditUserDetails, + IGeneralIdRef userId, + PatchUser patchUser, + CancellationToken cancellationToken + ) + { + var patch = _patchFactory.Create(patchUser); + + await AlterUser(auditUserDetails, userId, async editUser => + { + await patch.ApplyTo(editUser); + }, cancellationToken); + } + + private async Task AlterUser( + AuditUserDetails auditUserDetails, + IGeneralIdRef userId, + Func applyChanges, + CancellationToken cancellationToken) + { + await _userManagerRepository.TransactionAsync(async () => + { + var user = await _userManagerRepository.GetUserById(userId, cancellationToken) + ?? throw new NotFoundException("unable to find user"); + + if (!user.Active) + throw new DeletedRowInaccessibleException("This user is inactive so cannot be modified. You will need to create the user again."); + + await applyChanges(user); + + await _userManagerRepository.EditUser(auditUserDetails, user, cancellationToken); }); } + public async Task> GetUsersAsync(Paging paging, CancellationToken cancellationToken) { var users = _userManagerRepository.GetUsers().Where(x => x.Active == true); diff --git a/e-suite.UnitTest.Core/e-suite.UnitTestCore/FakeRepository.cs b/e-suite.UnitTest.Core/e-suite.UnitTestCore/FakeRepository.cs index 96f6785..b5e1b31 100644 --- a/e-suite.UnitTest.Core/e-suite.UnitTestCore/FakeRepository.cs +++ b/e-suite.UnitTest.Core/e-suite.UnitTestCore/FakeRepository.cs @@ -1,4 +1,6 @@ -using e_suite.Database.Core; +using e_suite.Database.Audit; +using e_suite.Database.Audit.AuditEngine; +using e_suite.Database.Core; using Microsoft.EntityFrameworkCore; using Moq; @@ -29,4 +31,9 @@ public class FakeRepository : IRepository return dbSet.Object; } + + public Task AddAdhocAuditEntry(AuditUserDetails auditUserDetails, AuditType auditType, Dictionary fields, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/eSuite.sln.DotSettings.user b/eSuite.sln.DotSettings.user index 1c6f5ab..10a5de7 100644 --- a/eSuite.sln.DotSettings.user +++ b/eSuite.sln.DotSettings.user @@ -9,10 +9,10 @@ </TestAncestor> </SessionState> True - (Doc Ln 69 Col 34) - ABA889AD-4D7F-45BF-8968-8BD388F33849/f:PerformanceManager.cs + (Doc Ln 120 Col 0) + 7DC1F493-76A5-3740-E774-C8DAA51ED83A/f:PatchUnitTests.cs NumberedBookmarkManager True - (Doc Ln 40 Col 59) - 8A9B7274-A7D4-4270-A374-285B2B1BB7CD/d:Repository/f:CustomFieldRepository.cs + (Doc Ln 606 Col 8) + A00B7AED-96DF-49A5-BA8F-9BE74021F3CF/f:UserManager.cs NumberedBookmarkManager \ No newline at end of file