Skip to content

Commit

Permalink
Implemented FindAsync user method. (#37)
Browse files Browse the repository at this point in the history
* Refactored UserNotFoundException.

* Implemented FoundUsers.

* Implemented FindAsync user method.

* Version bump.
  • Loading branch information
Utar94 authored Jan 13, 2024
1 parent 92a2e15 commit 91b499e
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 14 deletions.
6 changes: 3 additions & 3 deletions src/Logitar.Identity.Domain/Logitar.Identity.Domain.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/Logitar/Identity</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<AssemblyVersion>0.10.1.0</AssemblyVersion>
<AssemblyVersion>0.10.2.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<Version>0.10.1</Version>
<Version>0.10.2</Version>
<NeutralLanguage>en-CA</NeutralLanguage>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageReleaseNotes>Added serialization constructors to contact informations.</PackageReleaseNotes>
<PackageReleaseNotes>Implemented FindAsync user method.</PackageReleaseNotes>
<PackageTags>logitar;net;framework;identity;domain</PackageTags>
<PackageProjectUrl>https://github.com/Logitar/Identity/tree/main/src/Logitar.Identity.Domain</PackageProjectUrl>
</PropertyGroup>
Expand Down
70 changes: 70 additions & 0 deletions src/Logitar.Identity.Domain/Users/FoundUsers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
namespace Logitar.Identity.Domain.Users;

/// <summary>
/// The results of an user search.
/// </summary>
public record FoundUsers
{
/// <summary>
/// Gets or sets the user found by unique identifier.
/// </summary>
public UserAggregate? ById { get; set; }
/// <summary>
/// Gets or sets the user found by unique name.
/// </summary>
public UserAggregate? ByUniqueName { get; set; }
/// <summary>
/// Gets or sets the user found by email address.
/// </summary>
public UserAggregate? ByEmail { get; set; }

/// <summary>
/// Gets all found users.
/// </summary>
public IEnumerable<UserAggregate> All
{
get
{
List<UserAggregate> users = new(capacity: 3);

if (ById != null)
{
users.Add(ById);
}
if (ByUniqueName != null)
{
users.Add(ByUniqueName);
}
if (ByEmail != null)
{
users.Add(ByEmail);
}

return users.AsReadOnly();
}
}

/// <summary>
/// Returns the first user found, ordered by unique identifier, then by unique name, and then by email address.
/// </summary>
/// <returns>The first user found.</returns>
public UserAggregate First() => All.First();
/// <summary>
/// Returns the first user found, ordered by unique identifier, then by unique name, and then by email address.
/// </summary>
/// <returns>The first user found, or null if none were found.</returns>
public UserAggregate? FirstOrDefault() => All.FirstOrDefault();

/// <summary>
/// Returns the single user found.
/// </summary>
/// <exception cref="InvalidOperationException">More than one users have been found.</exception>
/// <returns>The single user found.</returns>
public UserAggregate Single() => All.Single();
/// <summary>
/// Returns the single user found.
/// </summary>
/// <exception cref="InvalidOperationException">More than one users have been found.</exception>
/// <returns>The single user found, or null if none were found.</returns>
public UserAggregate? SingleOrDefault() => All.SingleOrDefault();
}
9 changes: 9 additions & 0 deletions src/Logitar.Identity.Domain/Users/IUserManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ namespace Logitar.Identity.Domain.Users;
/// </summary>
public interface IUserManager
{
/// <summary>
/// Tries finding an user by its unique identifier, unique name, or email address if they are unique.
/// </summary>
/// <param name="tenantId">The identifier of the tenant in which to search.</param>
/// <param name="id">The identifier of the user to find.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The found users.</returns>
Task<FoundUsers> FindAsync(string? tenantId, string id, CancellationToken cancellationToken = default);

/// <summary>
/// Saves the specified user, performing model validation such as unique name and email address unicity.
/// </summary>
Expand Down
72 changes: 71 additions & 1 deletion src/Logitar.Identity.Domain/Users/UserManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Logitar.EventSourcing;
using FluentValidation;
using Logitar.EventSourcing;
using Logitar.Identity.Domain.Sessions;
using Logitar.Identity.Domain.Settings;
using Logitar.Identity.Domain.Shared;
Expand Down Expand Up @@ -37,6 +38,75 @@ public UserManager(ISessionRepository sessionRepository, IUserRepository userRep
UserSettingsResolver = userSettingsResolver;
}

/// <summary>
/// Tries finding an user by its unique identifier, unique name, or email address if they are unique.
/// </summary>
/// <param name="tenantIdValue">The identifier of the tenant in which to search.</param>
/// <param name="id">The identifier of the user to find.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The found users.</returns>
public virtual async Task<FoundUsers> FindAsync(string? tenantIdValue, string id, CancellationToken cancellationToken)
{
IUserSettings userSettings = UserSettingsResolver.Resolve();

TenantId? tenantId = null;
try
{
tenantId = TenantId.TryCreate(tenantIdValue);
}
catch (ValidationException)
{
}

UserId? userId = null;
try
{
userId = UserId.TryCreate(id);
}
catch (ValidationException)
{
}

UniqueNameUnit? uniqueName = null;
try
{
uniqueName = new(userSettings.UniqueName, id);
}
catch (ValidationException)
{
}

EmailUnit? email = null;
try
{
email = new(id);
}
catch (ValidationException)
{
}

FoundUsers found = new();

if (userId != null)
{
found.ById = await UserRepository.LoadAsync(userId, cancellationToken);
}
if (uniqueName != null)
{
found.ByUniqueName = await UserRepository.LoadAsync(tenantId, uniqueName, cancellationToken);
}
if (email != null && userSettings.RequireUniqueEmail)
{
IEnumerable<UserAggregate> users = await UserRepository.LoadAsync(tenantId, email, cancellationToken);
if (users.Count() == 1)
{
found.ByEmail = users.Single();
}
}

return found;
}

/// <summary>
/// Saves the specified user, performing model validation such as unique name and email address unicity.
/// </summary>
Expand Down
26 changes: 16 additions & 10 deletions src/Logitar.Identity.Domain/Users/UserNotFoundException.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Logitar.Identity.Domain.Shared;
using Logitar.Identity.Domain.Settings;
using Logitar.Identity.Domain.Shared;

namespace Logitar.Identity.Domain.Users;

Expand All @@ -12,20 +13,25 @@ public class UserNotFoundException : InvalidCredentialsException
/// </summary>
public new const string ErrorMessage = "The specified user could not be found.";

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 identifier of the tenant in which the user was searched.
/// </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 unique name of the user who was searched.
/// </summary>
public string UniqueName
public UniqueNameUnit UniqueName
{
get => (string)Data[nameof(UniqueName)]!;
get => new(_uniqueNameSettings, (string)Data[nameof(UniqueName)]!);
private set => Data[nameof(UniqueName)] = value;
}

Expand All @@ -34,14 +40,14 @@ public string UniqueName
/// </summary>
/// <param name="tenantId">The identifier of the tenant in which the user was searched.</param>
/// <param name="uniqueName">The unique name of the user who was searched.</param>
public UserNotFoundException(string? tenantId, string uniqueName) : base(BuildMessage(tenantId, uniqueName))
public UserNotFoundException(TenantId? tenantId, UniqueNameUnit uniqueName) : base(BuildMessage(tenantId, uniqueName))
{
TenantId = tenantId;
UniqueName = uniqueName;
}

private static string BuildMessage(string? tenantId, string uniqueName) => new ErrorMessageBuilder(ErrorMessage)
.AddData(nameof(TenantId), tenantId, "<null>")
.AddData(nameof(UniqueName), uniqueName)
private static string BuildMessage(TenantId? tenantId, UniqueNameUnit uniqueName) => new ErrorMessageBuilder(ErrorMessage)
.AddData(nameof(TenantId), tenantId?.Value, "<null>")
.AddData(nameof(UniqueName), uniqueName.Value)
.Build();
}
96 changes: 96 additions & 0 deletions tests/Logitar.Identity.Domain.UnitTests/Users/FoundUsersTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Bogus;
using Logitar.Identity.Domain.Settings;
using Logitar.Identity.Domain.Shared;

namespace Logitar.Identity.Domain.Users;

[Trait(Traits.Category, Categories.Unit)]
public class FoundUsersTests
{
private readonly Faker _faker = new();
private readonly UniqueNameSettings _uniqueNameSettings = new();

[Fact(DisplayName = "All: it should return all the users found.")]
public void All_it_should_return_all_the_users_found()
{
FoundUsers users = new();
Assert.Empty(users.All);

UserAggregate byId = new(new UniqueNameUnit(_uniqueNameSettings, _faker.Person.UserName));
UserAggregate byEmail = new(new UniqueNameUnit(_uniqueNameSettings, _faker.Person.Email));
users = new()
{
ById = byId,
ByEmail = byEmail
};

IEnumerable<UserAggregate> all = users.All;
Assert.Equal(2, all.Count());
Assert.Contains(byId, all);
Assert.Contains(byEmail, all);
}

[Fact(DisplayName = "First: it should return the first user found.")]
public void First_it_should_return_the_first_user_found()
{
UserAggregate byUniqueName = new(new UniqueNameUnit(_uniqueNameSettings, _faker.Person.UserName));
UserAggregate byEmail = new(new UniqueNameUnit(_uniqueNameSettings, _faker.Person.Email));
FoundUsers users = new()
{
ByUniqueName = byUniqueName,
ByEmail = byEmail
};

UserAggregate first = users.First();
Assert.Equal(byUniqueName, first);
}

[Fact(DisplayName = "FirstOrDefault: it should return the first user found or null if none found.")]
public void FirstOrDefault_it_should_return_the_first_user_found_or_null_if_none_found()
{
FoundUsers users = new();
Assert.Null(users.FirstOrDefault());

UserAggregate byUniqueName = new(new UniqueNameUnit(_uniqueNameSettings, _faker.Person.UserName));
UserAggregate byEmail = new(new UniqueNameUnit(_uniqueNameSettings, _faker.Person.Email));
users = new()
{
ByUniqueName = byUniqueName,
ByEmail = byEmail
};

UserAggregate? first = users.FirstOrDefault();
Assert.NotNull(first);
Assert.Equal(byUniqueName, first);
}

[Fact(DisplayName = "Single: it should return the only user found.")]
public void Single_it_should_return_the_only_user_found()
{
UserAggregate user = new(new UniqueNameUnit(_uniqueNameSettings, _faker.Person.UserName));
FoundUsers users = new()
{
ById = user
};

UserAggregate single = users.Single();
Assert.Equal(user, single);
}

[Fact(DisplayName = "SingleOrDefault: it should return the only user found or null if none found.")]
public void SingleOrDefault_it_should_return_the_only_user_found_or_null_if_none_found()
{
FoundUsers users = new();
Assert.Null(users.SingleOrDefault());

UserAggregate byId = new(new UniqueNameUnit(_uniqueNameSettings, _faker.Person.UserName));
users = new()
{
ById = byId
};

UserAggregate? single = users.SingleOrDefault();
Assert.NotNull(single);
Assert.Equal(byId, single);
}
}
Loading

0 comments on commit 91b499e

Please sign in to comment.