Skip to content

Commit

Permalink
Refactored exceptions and completed unit tests. (#34)
Browse files Browse the repository at this point in the history
* Refactored exceptions and completed unit tests.

* Code Review.
  • Loading branch information
Utar94 authored Jan 12, 2024
1 parent 2fab433 commit b65bc3c
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ public string TypeName
/// <summary>
/// Gets or sets the identifier of the tenant in which the conflict occurred.
/// </summary>
public string? TenantId
public TenantId? TenantId
{
get => (string?)Data[nameof(TenantId)];
private set => Data[nameof(TenantId)] = value;
get => TenantId.TryCreate((string?)Data[nameof(TenantId)]);
private set => Data[nameof(TenantId)] = value?.Value;
}
/// <summary>
/// Gets or sets the conflicting identifier key.
Expand Down Expand Up @@ -54,7 +54,7 @@ public CustomIdentifierAlreadyUsedException(Type type, TenantId? tenantId, strin
: base(BuildMessage(type, tenantId, key, value))
{
TypeName = type.GetNamespaceQualifiedName();
TenantId = tenantId?.Value;
TenantId = tenantId;
Key = key;
Value = value;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Logitar.Identity.Domain/Shared/TenantMismatchException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ public class TenantMismatchException : Exception
/// </summary>
public TenantId? ExpectedTenantId
{
get => Data[nameof(ExpectedTenantId)] is not string value ? null : new TenantId(value);
get => TenantId.TryCreate((string?)Data[nameof(ExpectedTenantId)]);
private set => Data[nameof(ExpectedTenantId)] = value?.Value;
}
/// <summary>
/// Gets or sets the actual tenant identifier.
/// </summary>
public TenantId? ActualTenantId
{
get => Data[nameof(ActualTenantId)] is not string value ? null : new TenantId(value);
get => TenantId.TryCreate((string?)Data[nameof(ActualTenantId)]);
private set => Data[nameof(ActualTenantId)] = value?.Value;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Logitar.Identity.Domain.Shared;
using Logitar.Identity.Domain.Settings;

namespace Logitar.Identity.Domain.Shared;

/// <summary>
/// The exception raised when an unique name conflict occurs.
Expand All @@ -10,6 +12,11 @@ public class UniqueNameAlreadyUsedException : Exception
/// </summary>
public const string ErrorMessage = "The specified unique name is already used.";

private static readonly UniqueNameSettings _uniqueNameSettings = new()
{
AllowedCharacters = null // NOTE(fpion): strict validation is not required when deserializing an unique name.
};

/// <summary>
/// Gets or sets the name of the type of the object that caused the conflict.
/// </summary>
Expand All @@ -21,18 +28,18 @@ public string TypeName
/// <summary>
/// Gets or sets the identifier of the tenant in which the conflict occurred.
/// </summary>
public string? TenantId
public TenantId? TenantId
{
get => (string?)Data[nameof(TenantId)];
private set => Data[nameof(TenantId)] = value;
get => TenantId.TryCreate((string?)Data[nameof(TenantId)]);
private set => Data[nameof(TenantId)] = value?.Value;
}
/// <summary>
/// Gets or sets the conflicting unique name.
/// </summary>
public string UniqueName
public UniqueNameUnit UniqueName
{
get => (string)Data[nameof(UniqueName)]!;
private set => Data[nameof(UniqueName)] = value;
get => new(_uniqueNameSettings, (string)Data[nameof(UniqueName)]!);
private set => Data[nameof(UniqueName)] = value.Value;
}

/// <summary>
Expand All @@ -45,8 +52,8 @@ public UniqueNameAlreadyUsedException(Type type, TenantId? tenantId, UniqueNameU
: base(BuildMessage(type, tenantId, uniqueName))
{
TypeName = type.GetNamespaceQualifiedName();
TenantId = tenantId?.Value;
UniqueName = uniqueName.Value;
TenantId = tenantId;
UniqueName = uniqueName;
}

private static string BuildMessage(Type type, TenantId? tenantId, UniqueNameUnit uniqueName) => new ErrorMessageBuilder(ErrorMessage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ public class EmailAddressAlreadyUsedException : Exception
/// <summary>
/// Gets or sets the identifier of the tenant in which the conflict occurred.
/// </summary>
public string? TenantId
public TenantId? TenantId
{
get => (string?)Data[nameof(TenantId)];
private set => Data[nameof(TenantId)] = value;
get => TenantId.TryCreate((string?)Data[nameof(TenantId)]);
private set => Data[nameof(TenantId)] = value?.Value;
}
/// <summary>
/// Gets or sets the conflicting email address.
/// </summary>
public string EmailAddress
public EmailUnit Email
{
get => (string)Data[nameof(EmailAddress)]!;
private set => Data[nameof(EmailAddress)] = value;
get => new((string)Data[nameof(Email)]!);
private set => Data[nameof(Email)] = value.Address;
}

/// <summary>
Expand All @@ -37,12 +37,12 @@ public string EmailAddress
public EmailAddressAlreadyUsedException(TenantId? tenantId, EmailUnit email)
: base(BuildMessage(tenantId, email))
{
TenantId = tenantId?.Value;
EmailAddress = email.Address;
TenantId = tenantId;
Email = email;
}

private static string BuildMessage(TenantId? tenantId, EmailUnit email) => new ErrorMessageBuilder(ErrorMessage)
.AddData(nameof(TenantId), tenantId?.Value, "<null>")
.AddData(nameof(EmailAddress), email.Address)
.AddData(nameof(Email), email.Address)
.Build();
}
13 changes: 8 additions & 5 deletions src/Logitar.Identity.Domain/Users/UserManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,16 @@ public virtual async Task SaveAsync(UserAggregate user, ActorId actorId, Cancell
}
}

IUserSettings userSettings = UserSettingsResolver.Resolve();
if (hasEmailChanged && user.Email != null && userSettings.RequireUniqueEmail)
if (hasEmailChanged && user.Email != null)
{
IEnumerable<UserAggregate> users = await UserRepository.LoadAsync(user.TenantId, user.Email, cancellationToken);
if (users.Any(other => !other.Equals(user)))
IUserSettings userSettings = UserSettingsResolver.Resolve();
if (userSettings.RequireUniqueEmail)
{
throw new EmailAddressAlreadyUsedException(user.TenantId, user.Email);
IEnumerable<UserAggregate> users = await UserRepository.LoadAsync(user.TenantId, user.Email, cancellationToken);
if (users.Any(other => !other.Equals(user)))
{
throw new EmailAddressAlreadyUsedException(user.TenantId, user.Email);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<PackageReference Include="Bogus" Version="35.3.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.6.5" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
101 changes: 101 additions & 0 deletions tests/Logitar.Identity.Domain.UnitTests/Roles/RoleManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using Logitar.EventSourcing;
using Logitar.Identity.Domain.ApiKeys;
using Logitar.Identity.Domain.Settings;
using Logitar.Identity.Domain.Shared;
using Logitar.Identity.Domain.Users;
using Moq;

namespace Logitar.Identity.Domain.Roles;

[Trait(Traits.Category, Categories.Unit)]
public class RoleManagerTests
{
private static readonly ActorId _actorId = default;
private static readonly CancellationToken _cancellationToken = default;

private readonly UniqueNameSettings _uniqueNameSettings = new();
private readonly RoleAggregate _role;

private readonly Mock<IApiKeyRepository> _apiKeyRepository = new();
private readonly Mock<IRoleRepository> _roleRepository = new();
private readonly Mock<IUserRepository> _userRepository = new();
private readonly RoleManager _roleManager;

public RoleManagerTests()
{
UniqueNameUnit uniqueName = new(_uniqueNameSettings, "admin");
_role = new(uniqueName);

_roleManager = new(_apiKeyRepository.Object, _roleRepository.Object, _userRepository.Object);
}

[Fact(DisplayName = "SaveAsync: it should not load any API key or user when it has not been deleted.")]
public async Task SaveAsync_it_should_not_load_any_API_key_or_user_when_it_has_not_been_deleted()
{
_roleRepository.Setup(x => x.LoadAsync(_role.TenantId, _role.UniqueName, _cancellationToken)).ReturnsAsync(_role);

await _roleManager.SaveAsync(_role, _actorId, _cancellationToken);

_roleRepository.Verify(x => x.SaveAsync(_role, _cancellationToken), Times.Once);

_apiKeyRepository.Verify(x => x.LoadAsync(_role, It.IsAny<CancellationToken>()), Times.Never);
_userRepository.Verify(x => x.LoadAsync(_role, It.IsAny<CancellationToken>()), Times.Never);
}

[Fact(DisplayName = "SaveAsync: it should not load any role when the unique name has not changed.")]
public async Task SaveAsync_it_should_not_load_any_role_when_the_unique_name_has_not_changed()
{
_role.ClearChanges();

_role.DisplayName = new DisplayNameUnit("Administrator");
_role.SetCustomAttribute("manage_users", bool.TrueString);
_role.Update();

await _roleManager.SaveAsync(_role, _actorId, _cancellationToken);

_roleRepository.Verify(x => x.SaveAsync(_role, _cancellationToken), Times.Once);

_roleRepository.Verify(x => x.LoadAsync(It.IsAny<TenantId>(), It.IsAny<UniqueNameUnit>(), It.IsAny<CancellationToken>()), Times.Never);
}

[Fact(DisplayName = "SaveAsync: it should remove associations when it has been deleted.")]
public async Task SaveAsync_it_should_remove_associations_when_it_has_been_deleted()
{
RoleAggregate guest = new(new UniqueNameUnit(_uniqueNameSettings, "guest"));

DisplayNameUnit displayName = new("Test");
PasswordMock secret = new("S3cr3+!*");
ApiKeyAggregate apiKey = new(displayName, secret);
apiKey.AddRole(_role);
apiKey.AddRole(guest);
_apiKeyRepository.Setup(x => x.LoadAsync(_role, _cancellationToken)).ReturnsAsync([apiKey]);

UserAggregate user = new(new UniqueNameUnit(_uniqueNameSettings, "test"));
user.AddRole(_role);
user.AddRole(guest);
_userRepository.Setup(x => x.LoadAsync(_role, _cancellationToken)).ReturnsAsync([user]);

_role.Delete();
await _roleManager.SaveAsync(_role, _actorId, _cancellationToken);

_apiKeyRepository.Verify(x => x.SaveAsync(It.Is<IEnumerable<ApiKeyAggregate>>(y => y.Single().Equals(apiKey)), _cancellationToken), Times.Once);
_roleRepository.Verify(x => x.SaveAsync(_role, _cancellationToken), Times.Once);
_userRepository.Verify(x => x.SaveAsync(It.Is<IEnumerable<UserAggregate>>(y => y.Single().Equals(user)), _cancellationToken), Times.Once);

Assert.Equal(guest.Id, apiKey.Roles.Single());
Assert.Equal(guest.Id, user.Roles.Single());
}

[Fact(DisplayName = "SaveAsync: it should throw UniqueNameAlreadyUsedException when an unique name conflict occurs.")]
public async Task SaveAsync_it_should_throw_UniqueNameAlreadyUsedException_when_an_unique_name_conflict_occurs()
{
RoleAggregate other = new(_role.UniqueName);
_roleRepository.Setup(x => x.LoadAsync(_role.TenantId, _role.UniqueName, _cancellationToken)).ReturnsAsync(other);

var exception = await Assert.ThrowsAsync<UniqueNameAlreadyUsedException<RoleAggregate>>(
async () => await _roleManager.SaveAsync(_role, _actorId, _cancellationToken)
);
Assert.Equal(_role.TenantId, exception.TenantId);
Assert.Equal(_role.UniqueName, exception.UniqueName);
}
}
Loading

0 comments on commit b65bc3c

Please sign in to comment.