From f54ffa574b658ada753ff1294b91cfb8bb3d43cc Mon Sep 17 00:00:00 2001 From: Matt Hoffmeister Date: Thu, 9 Feb 2023 09:19:38 -0600 Subject: [PATCH] Add Update Song Director use case --- .../Domain/SongDirectorTests.cs | 130 +++++++- Asaph.Core.UnitTests/Domain/UserTests.cs | 186 ++++++++++++ .../UpdateSongDirectorTestBoundary.cs | 40 +++ .../UpdateSongDirectorTestCaseBuilder.cs | 135 +++++++++ .../UseCases/UpdateSongDirectorTestData.cs | 224 ++++++++++++++ .../UseCases/UpdateSongDirectorTests.cs | 104 +++++++ .../SongDirectorAggregate/SongDirector.cs | 57 +++- .../Domain/UnchangedPropertyValueError.cs | 18 ++ Asaph.Core/Domain/UserAggregate/User.cs | 79 ++++- .../AddSongDirector/AddSongDirectorRequest.cs | 107 ++++--- .../IUpdateSongDirectorBoundary.cs | 44 +++ .../UpdateSongDirectorInteractor.cs | 279 ++++++++++++++++++ .../UpdateSongDirectorRequest.cs | 20 ++ .../UpdateSongDirectorResponse.cs | 84 ++++++ .../AggregateSongDirectorRepository.cs | 4 +- .../AzureAdb2cSongDirectorRepository.cs | 21 +- 16 files changed, 1451 insertions(+), 81 deletions(-) create mode 100644 Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTestBoundary.cs create mode 100644 Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTestCaseBuilder.cs create mode 100644 Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTestData.cs create mode 100644 Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTests.cs create mode 100644 Asaph.Core/Domain/UnchangedPropertyValueError.cs create mode 100644 Asaph.Core/UseCases/UpdateSongDirector/IUpdateSongDirectorBoundary.cs create mode 100644 Asaph.Core/UseCases/UpdateSongDirector/UpdateSongDirectorInteractor.cs create mode 100644 Asaph.Core/UseCases/UpdateSongDirector/UpdateSongDirectorRequest.cs create mode 100644 Asaph.Core/UseCases/UpdateSongDirector/UpdateSongDirectorResponse.cs diff --git a/Asaph.Core.UnitTests/Domain/SongDirectorTests.cs b/Asaph.Core.UnitTests/Domain/SongDirectorTests.cs index 368da91..5226eaa 100644 --- a/Asaph.Core.UnitTests/Domain/SongDirectorTests.cs +++ b/Asaph.Core.UnitTests/Domain/SongDirectorTests.cs @@ -1,5 +1,7 @@ -using Asaph.Core.Domain.SongDirectorAggregate; +using Asaph.Core.Domain; +using Asaph.Core.Domain.SongDirectorAggregate; using FluentResults; +using System.Linq; using Xunit; namespace Asaph.Core.UnitTests.Domain @@ -48,5 +50,131 @@ public static void TryCreate_Multiple_ReturnsExpectedIsSuccess( Assert.Equal(expectedIsSuccess, songDirectorCreateResult.IsSuccess); } + + /// + /// Tests update a user's full name. + /// + /// New active indicator. + /// Expected success indicator. + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(null, false)] + public static void TryUpdateIsActive_Multiple_ReturnsExpectedIsSuccess( + bool? isActive, + bool expectedIsSuccess) + { + // Arrange + + SongDirector songDirector = SongDirector + .TryCreate( + "Jane Doe", + "jane.doe@example.com", + "123-456-7890", + "Apprentice", + true) + .Value; + + // Act + + Result updateIsActiveResult = songDirector.TryUpdateIsActive(isActive); + + // Assert + + Assert.Equal(expectedIsSuccess, updateIsActiveResult.IsSuccess); + } + + /// + /// Tests that an is returned in the result when + /// an attempt to change a song director's active indicator to the same value is made. + /// + [Fact] + public static void + TryUpdateIsActive_UnchangedIsActive_ReturnsUnchangedPropertyValueError() + { + // Arrange + + bool isActive = true; + + SongDirector songDirector = SongDirector + .TryCreate( + "Jane Doe", + "jane.doe@example.com", + "123-456-7890", + "Apprentice", + isActive) + .Value; + + // Act + + Result updateIsActiveResult = songDirector.TryUpdateIsActive(isActive); + + // Assert + + Assert.IsType(updateIsActiveResult.Errors.Single()); + } + + /// + /// Tests update a user's full name. + /// + /// New rank. + /// Expected success indicator. + [Theory] + [InlineData("Apprentice", false)] + [InlineData("Journeyer", true)] + [InlineData("", false)] + [InlineData(" ", false)] + [InlineData(null, true)] + public static void TryUpdateRank_Multiple_ReturnsExpectedIsSuccess( + string? rank, bool expectedIsSuccess) + { + // Arrange + + SongDirector songDirector = SongDirector + .TryCreate( + "Jane Doe", + "jane.doe@example.com", + "123-456-7890", + "Apprentice", + true) + .Value; + + // Act + + Result updateRankResult = songDirector.TryUpdateRank(rank); + + // Assert + + Assert.Equal(expectedIsSuccess, updateRankResult.IsSuccess); + } + + /// + /// Tests that an is returned in the result when + /// an attempt to change a song director's rank is made. + /// + [Fact] + public static void TryUpdateRank_UnchangedRank_ReturnsUnchangedPropertyValueError() + { + // Arrange + + string rank = "Apprentice"; + + SongDirector songDirector = SongDirector + .TryCreate( + "Jane Doe", + "jane.doe@example.com", + "123-456-7890", + rank, + true) + .Value; + + // Act + + Result updateRankResult = songDirector.TryUpdateRank(rank); + + // Assert + + Assert.IsType(updateRankResult.Errors.Single()); + } } } diff --git a/Asaph.Core.UnitTests/Domain/UserTests.cs b/Asaph.Core.UnitTests/Domain/UserTests.cs index 7f64eba..1f68515 100644 --- a/Asaph.Core.UnitTests/Domain/UserTests.cs +++ b/Asaph.Core.UnitTests/Domain/UserTests.cs @@ -1,5 +1,7 @@ +using Asaph.Core.Domain; using Asaph.Core.Domain.UserAggregate; using FluentResults; +using System.Linq; using Xunit; namespace Asaph.Core.UnitTests.Domain @@ -37,5 +39,189 @@ public static void TryCreate_Multiple_ReturnsExpectedIsSuccess( Assert.Equal(expectedIsSuccess, result.IsSuccess); } + + /// + /// Tests updating a user's email address. + /// + /// Email address. + /// Expected success indicator. + [Theory] + [InlineData("vera.ilyinichna@example.com", false)] + [InlineData("vera.ilyinichna@example2.com", true)] + [InlineData("vera.ilyinichnaexample2.com", false)] + [InlineData("vera.ilyinichna@example2com", false)] + [InlineData("", false)] + [InlineData(" ", false)] + [InlineData(null, false)] + public static void TryUpdateEmailAddress_Multiple_ReturnsExpectedIsSuccess( + string? emailAddress, + bool expectedIsSuccess) + { + // Arrange + + User user = User + .TryCreate( + "Vera Ilyinichna", + "vera.ilyinichna@example.com", + "123-456-7890") + .Value; + + // Act + + Result updateEmailAddressResult = user.TryUpdateEmailAddress(emailAddress); + + // Assert + + Assert.Equal(expectedIsSuccess, updateEmailAddressResult.IsSuccess); + } + + /// + /// Tests that an is returned in the result when + /// an attempt to change a user's email address with the same value is made. + /// + [Fact] + public static void + TryUpdateEmailAddress_UnchangedEmailAddress_ReturnsUnchangedPropertyValueError() + { + // Arrange + + string emailAddress = "vera.ilyinichna@example.com"; + + User user = User + .TryCreate( + "Vera Ilyinichna", + emailAddress, + "123-456-7890") + .Value; + + // Act + + Result updateEmailAddressResult = user.TryUpdateEmailAddress(emailAddress); + + // Assert + + Assert.IsType(updateEmailAddressResult.Errors.Single()); + } + + /// + /// Tests update a user's full name. + /// + /// New full name. + /// Expected success indicator. + [Theory] + [InlineData("Harpa Stefansdottir", false)] + [InlineData("Harpa Gunnarsson", true)] + [InlineData("", false)] + [InlineData(" ", false)] + [InlineData(null, false)] + public static void TryUpdateFullName_Multiple_ReturnsExpectedIsSuccess( + string? fullName, + bool expectedIsSuccess) + { + // Arrange + + User user = User + .TryCreate( + "Harpa Stefansdottir", + "harpa.stefansdottir@example.com", + "123-456-7890") + .Value; + + // Act + + Result updateFullNameResult = user.TryUpdateFullName(fullName); + + // Assert + + Assert.Equal(expectedIsSuccess, updateFullNameResult.IsSuccess); + } + + /// + /// Tests that an is returned in the result when + /// an attempt to change a user's full name with the same value is made. + /// + [Fact] + public static void TryUpdateFullName_UnchangedFullName_ReturnsUnchangedPropertyValueError() + { + // Arrange + + string fullName = "Sato Gota"; + + User user = User + .TryCreate( + fullName, + "sato.gota@example.com", + "123-456-7890") + .Value; + + // Act + + Result updateFullNameResult = user.TryUpdateFullName(fullName); + + // Assert + + Assert.IsType(updateFullNameResult.Errors.Single()); + } + + /// + /// Tests update a user's full name. + /// + /// New phone number. + /// Expected success indicator. + [Theory] + [InlineData("123-456-7890", false)] + [InlineData("234-567-8901", true)] + [InlineData("", false)] + [InlineData(" ", false)] + [InlineData(null, true)] + public static void TryUpdatePhoneNumber_Multiple_ReturnsExpectedIsSuccess( + string? phoneNumber, + bool expectedIsSuccess) + { + // Arrange + + User user = User + .TryCreate( + "Zhang Xia", + "zhang.xia@example.com", + "123-456-7890") + .Value; + + // Act + + Result updateFullNameResult = user.TryUpdatePhoneNumber(phoneNumber); + + // Assert + + Assert.Equal(expectedIsSuccess, updateFullNameResult.IsSuccess); + } + + /// + /// Tests that an is returned in the result when + /// an attempt to change a user's phone number with the same value is made. + /// + [Fact] + public static void + TryUpdatePhoneNumber_UnchangedPhoneNumber_ReturnsUnchangedPropertyValueError() + { + // Arrange + + string phoneNumber = "123-456-7890"; + + User user = User + .TryCreate( + "Zhang Xia", + "zhang.xia@example.com", + phoneNumber) + .Value; + + // Act + + Result updateFullNameResult = user.TryUpdatePhoneNumber(phoneNumber); + + // Assert + + Assert.IsType(updateFullNameResult.Errors.Single()); + } } } diff --git a/Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTestBoundary.cs b/Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTestBoundary.cs new file mode 100644 index 0000000..57e5f66 --- /dev/null +++ b/Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTestBoundary.cs @@ -0,0 +1,40 @@ +using Asaph.Core.UseCases.UpdateSongDirector; + +namespace Asaph.Core.UnitTests.UseCases; + +/// +/// Test implementation of the update song director boundary. +/// +internal class UpdateSongDirectorTestBoundary + : IUpdateSongDirectorBoundary +{ + /// + public UpdateSongDirectorResponse InsufficientPermissions(UpdateSongDirectorResponse response) + { + return response; + } + + /// + public UpdateSongDirectorResponse InvalidRequest(UpdateSongDirectorResponse response) + { + return response; + } + + /// + public UpdateSongDirectorResponse RequesterRankNotFound(UpdateSongDirectorResponse response) + { + return response; + } + + /// + public UpdateSongDirectorResponse SongDirectorUpdated(UpdateSongDirectorResponse response) + { + return response; + } + + /// + public UpdateSongDirectorResponse SongDirectorUpdateFailed(UpdateSongDirectorResponse response) + { + return response; + } +} diff --git a/Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTestCaseBuilder.cs b/Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTestCaseBuilder.cs new file mode 100644 index 0000000..5131c36 --- /dev/null +++ b/Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTestCaseBuilder.cs @@ -0,0 +1,135 @@ +using Asaph.Core.Domain.SongDirectorAggregate; +using Asaph.Core.UseCases.UpdateSongDirector; +using System.Collections.Generic; + +namespace Asaph.Core.UnitTests.UseCases; + +/// +/// Builds test cases for the Update Song Director use case. +/// +internal class UpdateSongDirectorTestCaseBuilder +{ + private readonly string? _requesterId; + + private readonly string? _requesterRank; + + private string? _expectedMessage; + + private UpdateSongDirectorRequest? _request; + + private IEnumerable _existingSongDirectors = new List(); + + private SongDirector? _songDirectorToUpdate; + + private UpdateSongDirectorTestCaseBuilder(string? requesterId, string? requesterRank) + { + _requesterId = requesterId; + _requesterRank = requesterRank; + } + + /// + /// Sets up the requester for the test case. + /// + /// Requester id. + /// Requester rank. + /// . + public static UpdateSongDirectorTestCaseBuilder Requester( + string? requesterId, string? requesterRank) + { + return new(requesterId, requesterRank); + } + + /// + /// Builds the test case. + /// + /// Test case parameters. + public object?[] Build() + { + return new object?[] + { + _requesterRank, + _request, + _songDirectorToUpdate, + _existingSongDirectors, + _expectedMessage, + }; + } + + /// + /// Sets existing song directors. + /// + /// Existing song directors. + /// . + public UpdateSongDirectorTestCaseBuilder ExistingSongDirectors( + IEnumerable existingSongDirectors) + { + _existingSongDirectors = existingSongDirectors; + + return this; + } + + /// + /// Configures the expected message. + /// + /// Expected message. + /// . + public UpdateSongDirectorTestCaseBuilder ExpectedMessage(string expectedMessage) + { + _expectedMessage = expectedMessage; + + return this; + } + + /// + /// Configures the request. + /// + /// Song director id. + /// Full name. + /// Email address. + /// Phone number. + /// Rank. + /// Active indicator. + /// . + public UpdateSongDirectorTestCaseBuilder Request( + string? songDirectorId, + string? fullName, + string? emailAddress, + string? phoneNumber, + string? rank, + bool? isActive) + { + _request = new UpdateSongDirectorRequest( + _requesterId, songDirectorId, fullName, emailAddress, phoneNumber, rank, isActive); + + return this; + } + + /// + /// Configures the song director to update. + /// + /// Id. + /// Full name. + /// Email address. + /// Phone number. + /// Rank. + /// Active indicator. + /// . + public UpdateSongDirectorTestCaseBuilder SongDirectorToUpdate( + string? id, + string? fullName, + string? emailAddress, + string? phoneNumber, + string? rank, + bool? isActive) + { + SongDirector songDirectorToUpdate = SongDirector + .TryCreate(fullName, emailAddress, phoneNumber, rank, isActive) + .Value; + + songDirectorToUpdate.UpdateId(id); + + _songDirectorToUpdate = songDirectorToUpdate; + + return this; + } +} \ No newline at end of file diff --git a/Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTestData.cs b/Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTestData.cs new file mode 100644 index 0000000..2f40837 --- /dev/null +++ b/Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTestData.cs @@ -0,0 +1,224 @@ +using Asaph.Core.Domain.SongDirectorAggregate; +using System.Collections; +using System.Collections.Generic; + +namespace Asaph.Core.UnitTests.UseCases; + +/// +/// Test data for Update Song Director tests. +/// +public class UpdateSongDirectorTestData : IEnumerable +{ + /// + public IEnumerator GetEnumerator() + { + // Invalid request due to missing requester id + yield return UpdateSongDirectorTestCaseBuilder + .Requester(null, null) + .Request( + "00000000-0000-0000-0000-000000000002", + "Panashe Mutsipa", + "panashe.mutsipa@example.com", + "123-456-7890", + "Master", + true) + .SongDirectorToUpdate( + "00000000-0000-0000-0000-000000000002", + "Panashe Mutsipa", + "panashe.mutsipa@example.com", + "123-456-7890", + "Master", + true) + .ExpectedMessage("Requester id is required.") + .Build(); + + // Unauthorized + yield return UpdateSongDirectorTestCaseBuilder + .Requester("00000000-0000-0000-0000-000000000003", "Journeyer") + .Request( + "00000000-0000-0000-0000-000000000002", + "Panashe Mutsipa", + "panashe.mutsipa@example.com", + "123-456-7890", + "Master", + true) + .SongDirectorToUpdate( + "00000000-0000-0000-0000-000000000002", + "Panashe Mutsipa", + "panashe.mutsipa@example.com", + "234-567-9012", + "Master", + true) + .ExpectedMessage("You don't have permission to update Panashe Mutsipa.") + .Build(); + + // Invalid update (email address) + yield return UpdateSongDirectorTestCaseBuilder + .Requester("00000000-0000-0000-0000-000000000001", "Grandmaster") + .Request( + "00000000-0000-0000-0000-000000000002", + "Panashe Mutsipa", + "foobar", + "123-456-7890", + "Master", + true) + .SongDirectorToUpdate( + "00000000-0000-0000-0000-000000000002", + "Panashe Mutsipa", + "panashe.mutsipa@example.com", + "123-456-7890", + "Master", + true) + .ExpectedMessage("Invalid email address.") + .Build(); + + // Valid update by grandmaster (phone number) + yield return UpdateSongDirectorTestCaseBuilder + .Requester("00000000-0000-0000-0000-000000000001", "Grandmaster") + .Request( + "00000000-0000-0000-0000-000000000002", + "Panashe Mutsipa", + "panashe.mutsipa@example.com", + "234-567-8901", + "Master", + true) + .SongDirectorToUpdate( + "00000000-0000-0000-0000-000000000002", + "Panashe Mutsipa", + "panashe.mutsipa@example.com", + "123-456-7890", + "Master", + true) + .ExpectedMessage("Updated Panashe Mutsipa.") + .Build(); + + // Valid update by self (phone number) + yield return UpdateSongDirectorTestCaseBuilder + .Requester("00000000-0000-0000-0000-000000000002", "Journeyer") + .Request( + "00000000-0000-0000-0000-000000000002", + "Panashe Mutsipa", + "panashe.mutsipa@example.com", + "234-567-8901", + "Master", + true) + .SongDirectorToUpdate( + "00000000-0000-0000-0000-000000000002", + "Panashe Mutsipa", + "panashe.mutsipa@example.com", + "123-456-7890", + "Master", + true) + .ExpectedMessage("Updated Panashe Mutsipa.") + .Build(); + + // Invalid grandmaster demotion + yield return UpdateSongDirectorTestCaseBuilder + .Requester("00000000-0000-0000-0000-000000000001", "Grandmaster") + .Request( + "00000000-0000-0000-0000-000000000001", + "Jane Doe", + "jane.doe@example.com", + "789-345-7898", + "Master", + true) + .SongDirectorToUpdate( + "00000000-0000-0000-0000-000000000001", + "Jane Doe", + "jane.doe@example.com", + "789-345-7898", + "Grandmaster", + true) + .ExistingSongDirectors(GetSongDirectors(withTwoGrandmasters: false)) + .ExpectedMessage( + "You must promote another song director to grandmaster before demoting yourself.") + .Build(); + + // Valid grandmaster demotion + yield return UpdateSongDirectorTestCaseBuilder + .Requester("00000000-0000-0000-0000-000000000001", "Grandmaster") + .Request( + "00000000-0000-0000-0000-000000000001", + "Jane Doe", + "jane.doe@example.com", + "789-345-7898", + "Master", + true) + .SongDirectorToUpdate( + "00000000-0000-0000-0000-000000000001", + "Jane Doe", + "jane.doe@example.com", + "789-345-7898", + "Grandmaster", + true) + .ExistingSongDirectors(GetSongDirectors(withTwoGrandmasters: true)) + .ExpectedMessage("Updated Jane Doe.") + .Build(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets song directors for the test. + /// + /// If true, includes an additional grandmaster. + /// Song directors. + private IEnumerable GetSongDirectors(bool withTwoGrandmasters) + { + SongDirector harpa = SongDirector + .TryCreate( + "Harpa Stefansdottir", + "harpa.stefansdottir@example.com", + "234-123-2345", + "Journeyer", + true) + .Value; + + harpa.UpdateId("00000000-0000-0000-0000-000000000003"); + + yield return harpa; + + SongDirector jane = SongDirector + .TryCreate( + "Jane Doe", + "jane.doe@example.com", + "789-345-7898", + "Grandmaster", + true) + .Value; + + jane.UpdateId("00000000-0000-0000-0000-000000000001"); + + yield return jane; + + SongDirector panashe = SongDirector + .TryCreate( + "Panashe Mutsipa", + "panashe.mutsipa@example.com", + "123-456-7890", + "Master", + true) + .Value; + + panashe.UpdateId("00000000-0000-0000-0000-000000000002"); + + yield return panashe; + + if (withTwoGrandmasters) + { + SongDirector sato = SongDirector + .TryCreate( + "Sato Gota", + "sato.gota@example.com", + "457-294-2847", + "Grandmaster", + true) + .Value; + + sato.UpdateId("00000000-0000-0000-0000-000000000004"); + + yield return sato; + } + } +} diff --git a/Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTests.cs b/Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTests.cs new file mode 100644 index 0000000..15cb4ae --- /dev/null +++ b/Asaph.Core.UnitTests/UseCases/UpdateSongDirectorTests.cs @@ -0,0 +1,104 @@ +using Asaph.Core.Domain.SongDirectorAggregate; +using Asaph.Core.Interfaces; +using Asaph.Core.UseCases.UpdateSongDirector; +using FluentResults; +using Moq; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace Asaph.Core.UnitTests.UseCases; + +/// +/// Tests updating song directors. +/// +public static class UpdateSongDirectorTests +{ + /// + /// Tests handling song director updates. + /// + /// Requester rank. + /// Request. + /// Song director to update. + /// Existing song directors. + /// Expected message. + /// The result of the async task. + [Theory] + [ClassData(typeof(UpdateSongDirectorTestData))] + public static async Task HandleAsync_Multiple_ReturnsExpectedMessage( + string requesterRank, + UpdateSongDirectorRequest request, + SongDirector songDirectorToUpdate, + IEnumerable existingSongDirectors, + string expectedMessage) + { + // Arrange + + IAsyncRepository songDirectorRepository = GetMockSongDirectorRepository( + songDirectorToUpdate, request.RequesterId, requesterRank, existingSongDirectors); + + UpdateSongDirectorTestBoundary boundary = new(); + + UpdateSongDirectorInteractor addSongDirectorInteractor = new( + songDirectorRepository, boundary); + + // Act + + UpdateSongDirectorResponse updateSongDirectorResponse = await addSongDirectorInteractor + .HandleAsync(request) + .ConfigureAwait(false); + + // Assert + + Assert.Equal(expectedMessage, updateSongDirectorResponse.Message); + } + + /// + /// Returns a mocked song director repository for supporting song director updates. + /// + /// Song director to update.. + /// Requester id. + /// Requester rank. + /// Existing song directors. + /// The mocked song director repository. + public static IAsyncRepository GetMockSongDirectorRepository( + SongDirector songDirectorToUpdate, + string? requesterId, + string requesterRank, + IEnumerable existingSongDirectors) + { + // Create a mock for the song director repository interface + Mock>? mockSongDirectorRepository = new(); + + // Configure the TryFindPropertyByIdAsync method to return the song director name + mockSongDirectorRepository + .Setup(m => m.TryFindPropertyByIdAsync( + It.Is(id => id == songDirectorToUpdate.Id), + It.Is(param => param == nameof(SongDirector.FullName)))) + .Returns(Task.FromResult(Result.Ok(songDirectorToUpdate.FullName))); + + // Configure the TryFindPropertyByIdAsync method to return the requester's rank + mockSongDirectorRepository + .Setup(m => m.TryFindPropertyByIdAsync( + It.Is(id => id == requesterId), + It.Is(param => param == nameof(SongDirector.Rank)))) + .Returns(Task.FromResult(Result.Ok(Rank.TryGetByName(requesterRank).Value))); + + // Configure the TryGetAll method + mockSongDirectorRepository + .Setup(m => m.TryGetAllAsync()) + .Returns(Task.FromResult(Result.Ok(existingSongDirectors))); + + // Configure the TryGetByIdAsync method + mockSongDirectorRepository + .Setup(m => m.TryGetByIdAsync(It.Is(id => id == songDirectorToUpdate.Id))) + .Returns(Task.FromResult(Result.Ok(songDirectorToUpdate))); + + // Configure the TryUpdateAsync method + mockSongDirectorRepository + .Setup(m => m.TryUpdateAsync(It.IsAny())) + .Returns(Task.FromResult(Result.Ok())); + + return mockSongDirectorRepository.Object; + } +} diff --git a/Asaph.Core/Domain/SongDirectorAggregate/SongDirector.cs b/Asaph.Core/Domain/SongDirectorAggregate/SongDirector.cs index a3cd76b..cf1a674 100644 --- a/Asaph.Core/Domain/SongDirectorAggregate/SongDirector.cs +++ b/Asaph.Core/Domain/SongDirectorAggregate/SongDirector.cs @@ -19,12 +19,12 @@ private SongDirector( /// /// True if the song director is active; false, otherwise. /// - public bool IsActive { get; } + public bool IsActive { get; private set; } /// /// Rank. /// - public Rank? Rank { get; } + public Rank? Rank { get; private set; } /// /// Tries to create a song director. @@ -46,23 +46,66 @@ public static Result TryCreate( Result createRankResult = Rank.TryGetByName(rankName); // Validate person properties - Result validatePersonResult = TryCreate(fullName, emailAddress, phoneNumber); + Result validateUserResult = TryCreate(fullName, emailAddress, phoneNumber); // Merge the results into a single validation result Result validationResult = Result.Merge( - createRankResult, validatePersonResult); + createRankResult, validateUserResult); // Return the failure result if the validation failed if (validationResult.IsFailed) return validationResult; + // Reference the valid user + User user = validateUserResult.Value; + // Return a success result with the new song director if the validation succeeded return Result.Ok(new SongDirector( - fullName!, - emailAddress!, - phoneNumber, + user.FullName, + user.EmailAddress, + user.PhoneNumber, createRankResult.Value, isActive ?? false)); } + + /// + /// Tries to update the song director's active indicator. + /// + /// New active indicator. + /// The result of the attempt. + public Result TryUpdateIsActive(bool? isActive) + { + if (isActive == null) + return Result.Fail("Active indicator is required."); + + if (IsActive == isActive) + return Result.Fail(new UnchangedPropertyValueError("Active indicator")); + + IsActive = isActive.Value; + + return Result.Ok(); + } + + /// + /// Tries to update the song director's rank. + /// + /// New Rank name. + /// The result of the attempt. + public Result TryUpdateRank(string? rankName) + { + Result getRankResult = Rank.TryGetByName(rankName); + + if (getRankResult.IsFailed) + return getRankResult.ToResult(); + + Rank? rank = getRankResult.Value; + + if (Rank == rank) + return Result.Fail(new UnchangedPropertyValueError("Rank")); + + Rank = rank; + + return Result.Ok(); + } } } diff --git a/Asaph.Core/Domain/UnchangedPropertyValueError.cs b/Asaph.Core/Domain/UnchangedPropertyValueError.cs new file mode 100644 index 0000000..761ab8c --- /dev/null +++ b/Asaph.Core/Domain/UnchangedPropertyValueError.cs @@ -0,0 +1,18 @@ +using FluentResults; + +namespace Asaph.Core.Domain; + +/// +/// Used to indicate that a property value change failed because its value didn't change. +/// +public class UnchangedPropertyValueError : Error +{ + /// + /// Initializes a new instance of the class. + /// + /// Property name. + public UnchangedPropertyValueError(string propertyName) + : base($"{propertyName} is unchanged") + { + } +} diff --git a/Asaph.Core/Domain/UserAggregate/User.cs b/Asaph.Core/Domain/UserAggregate/User.cs index c2e5ca6..60702b6 100644 --- a/Asaph.Core/Domain/UserAggregate/User.cs +++ b/Asaph.Core/Domain/UserAggregate/User.cs @@ -1,6 +1,7 @@ using FluentResults; using System; using System.Globalization; +using System.Reflection.Metadata.Ecma335; using System.Text.RegularExpressions; namespace Asaph.Core.Domain.UserAggregate; @@ -27,17 +28,17 @@ protected User( /// /// Email address. /// - public string EmailAddress { get; } + public string EmailAddress { get; private set; } /// /// Full name. /// - public string FullName { get; } + public string FullName { get; private set; } /// /// Phone number. /// - public string? PhoneNumber { get; } + public string? PhoneNumber { get; private set; } /// /// Tries to create a user. @@ -70,12 +71,72 @@ public static Result TryCreate( new User(fullName!, emailAddress!, phoneNumberNormalizationResult.Value)); } + /// + /// Tries to update the user's email address. + /// + /// Email address. + /// The result of the attempt. + public Result TryUpdateEmailAddress(string? emailAddress) + { + if (EmailAddress == emailAddress) + return Result.Fail(new UnchangedPropertyValueError("Email address")); + + Result emailAddressValidation = ValidateEmailAddress(emailAddress); + + if (emailAddressValidation.IsFailed) return emailAddressValidation; + + EmailAddress = emailAddress!; + + return Result.Ok(); + } + + /// + /// Tries to update the user's full name. + /// + /// New full name. + /// The result of the attempt. + public Result TryUpdateFullName(string? fullName) + { + if (FullName == fullName) + return Result.Fail(new UnchangedPropertyValueError("Full name")); + + Result fullNameValidation = ValidateFullName(fullName); + + if (fullNameValidation.IsFailed) return fullNameValidation; + + FullName = fullName!; + + return Result.Ok(); + } + + /// + /// Tries to update the user's phone number. + /// + /// New phone number. + /// The result of the attempt. + public Result TryUpdatePhoneNumber(string? phoneNumber) + { + Result normalizePhoneNumberResult = TryNormalizePhoneNumber(phoneNumber); + + if (normalizePhoneNumberResult.IsFailed) + return normalizePhoneNumberResult.ToResult(); + + string? normalizedPhoneNumber = normalizePhoneNumberResult.Value; + + if (PhoneNumber == normalizedPhoneNumber) + return Result.Fail(new UnchangedPropertyValueError("Phone number")); + + PhoneNumber = normalizedPhoneNumber; + + return Result.Ok(); + } + /// /// Determines whether an email address is valid. /// /// Email address. /// True if the email address is valid; false, otherwise. - public static Result ValidateEmailAddress(string? emailAddress) + private static Result ValidateEmailAddress(string? emailAddress) { if (string.IsNullOrWhiteSpace(emailAddress)) return Result.Fail("Email address is required."); @@ -90,10 +151,10 @@ public static Result ValidateEmailAddress(string? emailAddress) RegexOptions.None, TimeSpan.FromMilliseconds(200)); - // Examines the domain part of the email and normalizes it. + // Examines the domain part of the email and normalize it static string DomainMapper(Match match) { - // Use IdnMapping class to convert Unicode domain names. + // Use IdnMapping class to convert Unicode domain names IdnMapping idn = new(); // Pull out and process domain name (throws ArgumentException on invalid) @@ -133,7 +194,7 @@ static string DomainMapper(Match match) /// /// Full name. /// True if the full name is valid; false, otherwise. - public static Result ValidateFullName(string? fullName) => + private static Result ValidateFullName(string? fullName) => Result.OkIf(fullName?.Trim().Length > 0, "Full name is required."); /// @@ -141,7 +202,7 @@ public static Result ValidateFullName(string? fullName) => /// /// Phone number. /// True if the phone number was normalized; false, otherwise. - public static Result TryNormalizePhoneNumber(string? phoneNumber) + private static Result TryNormalizePhoneNumber(string? phoneNumber) { if (phoneNumber == null) return Result.Ok(null); @@ -153,7 +214,7 @@ public static Result ValidateFullName(string? fullName) => } catch (RegexMatchTimeoutException) { - return Result.Fail("A timeout occurred while trying to validate phone number."); + return Result.Fail("A timeout occurred while trying to normalize phone number."); } return phoneNumber.Length == 10 && double.TryParse(phoneNumber, out _) ? diff --git a/Asaph.Core/UseCases/AddSongDirector/AddSongDirectorRequest.cs b/Asaph.Core/UseCases/AddSongDirector/AddSongDirectorRequest.cs index ee18db3..6c270b3 100644 --- a/Asaph.Core/UseCases/AddSongDirector/AddSongDirectorRequest.cs +++ b/Asaph.Core/UseCases/AddSongDirector/AddSongDirectorRequest.cs @@ -1,65 +1,64 @@ -namespace Asaph.Core.UseCases.AddSongDirector +namespace Asaph.Core.UseCases.AddSongDirector; + +/// +/// Request for creating a new song director. +/// +public class AddSongDirectorRequest { /// - /// Request for creating a new song director. + /// Initializes a new instance of the class. /// - public class AddSongDirectorRequest + /// + /// The rank of the person requesting the song director add. + /// + /// Full name of the new song director. + /// Email address of the new song director. + /// Phone number of the new song director. + /// Rank of the new song director. + /// Active indicator. + public AddSongDirectorRequest( + string requesterId, + string? fullName, + string? emailAddress, + string? phoneNumber, + string? rankName, + bool? isActive) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The rank of the person requesting the song director add. - /// - /// Full name of the new song director. - /// Email address of the new song director. - /// Phone number of the new song director. - /// Rank of the new song director. - /// Active indicator. - public AddSongDirectorRequest( - string requesterId, - string? fullName, - string? emailAddress, - string? phoneNumber, - string? rankName, - bool? isActive) - { - EmailAddress = emailAddress; - FullName = fullName; - IsActive = isActive; - PhoneNumber = phoneNumber; - RankName = rankName; - RequesterId = requesterId; - } + EmailAddress = emailAddress; + FullName = fullName; + IsActive = isActive; + PhoneNumber = phoneNumber; + RankName = rankName; + RequesterId = requesterId; + } - /// - /// Email address of the new song director. - /// - public string? EmailAddress { get; } + /// + /// Email address of the new song director. + /// + public string? EmailAddress { get; } - /// - /// Full name of the new song director. - /// - public string? FullName { get; } + /// + /// Full name of the new song director. + /// + public string? FullName { get; } - /// - /// True if the new song director is active; false, otherwise. - /// - public bool? IsActive { get; } + /// + /// True if the new song director is active; false, otherwise. + /// + public bool? IsActive { get; } - /// - /// Phone number of the new song director. - /// - public string? PhoneNumber { get; } + /// + /// Phone number of the new song director. + /// + public string? PhoneNumber { get; } - /// - /// Rank of the new song director. - /// - public string? RankName { get; } + /// + /// Rank of the new song director. + /// + public string? RankName { get; } - /// - /// The rank of the person trying to add a song director. - /// - public string RequesterId { get; } - } + /// + /// The rank of the person trying to add a song director. + /// + public string RequesterId { get; } } diff --git a/Asaph.Core/UseCases/UpdateSongDirector/IUpdateSongDirectorBoundary.cs b/Asaph.Core/UseCases/UpdateSongDirector/IUpdateSongDirectorBoundary.cs new file mode 100644 index 0000000..ae67ba6 --- /dev/null +++ b/Asaph.Core/UseCases/UpdateSongDirector/IUpdateSongDirectorBoundary.cs @@ -0,0 +1,44 @@ +namespace Asaph.Core.UseCases.UpdateSongDirector; + +/// +/// Boundary for the Update Song Director use case. +/// +/// Output type. +public interface IUpdateSongDirectorBoundary + : IBoundary +{ + /// + /// Insufficient permissions. + /// + /// Response. + /// Output. + TOutput InsufficientPermissions(UpdateSongDirectorResponse response); + + /// + /// Invalid request. + /// + /// Response. + /// Output. + TOutput InvalidRequest(UpdateSongDirectorResponse response); + + /// + /// Requester rank not found. + /// + /// Response. + /// Output. + TOutput RequesterRankNotFound(UpdateSongDirectorResponse response); + + /// + /// Song director updated. + /// + /// Response. + /// Output. + TOutput SongDirectorUpdated(UpdateSongDirectorResponse response); + + /// + /// Failed to update song director. + /// + /// Response. + /// Output. + TOutput SongDirectorUpdateFailed(UpdateSongDirectorResponse response); +} diff --git a/Asaph.Core/UseCases/UpdateSongDirector/UpdateSongDirectorInteractor.cs b/Asaph.Core/UseCases/UpdateSongDirector/UpdateSongDirectorInteractor.cs new file mode 100644 index 0000000..aabb56e --- /dev/null +++ b/Asaph.Core/UseCases/UpdateSongDirector/UpdateSongDirectorInteractor.cs @@ -0,0 +1,279 @@ +using Asaph.Core.Domain.SongDirectorAggregate; +using Asaph.Core.Interfaces; +using FluentResults; +using Microsoft.Extensions.Logging; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Asaph.Core.UseCases.UpdateSongDirector; + +/// +/// Interactor for the Update Song Director use case. +/// +/// Output type. +public class UpdateSongDirectorInteractor + : IAsyncUseCaseInteractor +{ + // Use case boundary + private readonly IUpdateSongDirectorBoundary _boundary; + + // Song director repository + private readonly IAsyncRepository _songDirectorRepository; + + /// + /// Initializes a new instance of the class. + /// + /// Song director repository. + /// Boundary. + public UpdateSongDirectorInteractor( + IAsyncRepository songDirectorRepository, + IUpdateSongDirectorBoundary boundary) + { + _boundary = boundary; + _songDirectorRepository = songDirectorRepository; + } + + /// + public async Task HandleAsync(UpdateSongDirectorRequest request) + { + // Validate requester id + Result requesterIdValidation = ValidateId("Requester", request.RequesterId); + + // If the validation failed, output invalid request + if (requesterIdValidation.IsFailed) + return InvalidRequest(requesterIdValidation.GetErrorMessagesString()); + + // Validate song director id + Result songDirectorIdValidation = ValidateId( + "Song director", request.SongDirectorId); + + // If the song director id validation failed, output invalid request + if (songDirectorIdValidation.IsFailed) + return InvalidRequest(songDirectorIdValidation.GetErrorMessagesString()); + + // Reference the validated ids + string requesterId = requesterIdValidation.Value; + string songDirectorId = songDirectorIdValidation.Value; + + // Get the requester's rank + Result getRequesterRankResult = await _songDirectorRepository + .TryFindPropertyByIdAsync(requesterId, nameof(SongDirector.Rank)) + .ConfigureAwait(false); + + // If the requester's rank couldn't be retrieved, return requester rank not found + if (getRequesterRankResult.IsFailed) + { + return await RequesterRankNotFound( + requesterId, getRequesterRankResult.GetErrorMessagesString()) + .ConfigureAwait(false); + } + + // Reference the requester's rank + Rank? requesterRank = getRequesterRankResult.Value; + + // Try to update if a song director is updating their own information + if (requesterId == songDirectorId) + return await TryUpdateSelf(requesterId, requesterRank, request).ConfigureAwait(false); + + // If the requester isn't a grandmaster, return insufficient permissions + if (getRequesterRankResult.Value != Rank.Grandmaster) + return await InsufficientPermissions(songDirectorId).ConfigureAwait(false); + + // Try to update the song director and return the output + return await TryUpdateSongDirector(request).ConfigureAwait(false); + } + + /// + /// Validates an id. + /// + /// Label for the id. Used in failure messages. + /// Id. + /// The validation result. + private static Result ValidateId(string label, string? id) + { + if (string.IsNullOrWhiteSpace(id)) + return Result.Fail($"{label} id is required."); + + return Result.Ok(id!); + } + + /// + /// Outputs insufficient permissions. + /// + /// Song director id. + /// Output. + private async Task InsufficientPermissions(string songDirectorId) + { + Result getSongDirectorFullName = await _songDirectorRepository + .TryFindPropertyByIdAsync(songDirectorId, nameof(SongDirector.FullName)) + .ConfigureAwait(false); + + return _boundary.InsufficientPermissions( + UpdateSongDirectorResponse.InsufficientPermissions( + getSongDirectorFullName.ValueOrDefault)); + } + + /// + /// Outputs song director updated. + /// + /// Song director full name. + /// Output. + private TOutput SongDirectorUpdated(string songDirectorFullName) + { + return _boundary.SongDirectorUpdated( + UpdateSongDirectorResponse.SongDirectorUpdated(songDirectorFullName)); + } + + /// + /// Outputs failed to update song director. + /// + /// Song director full name. + /// Failure result. + /// Output. + private TOutput SongDirectorUpdateFailed(string songDirectorFullName, Result failureResult) + { + return _boundary.SongDirectorUpdateFailed( + UpdateSongDirectorResponse.SongDirectorUpdateFailed( + songDirectorFullName, failureResult.GetErrorMessagesString())); + } + + /// + /// Outputs invalid request. + /// + /// Message. + /// Output. + private TOutput InvalidRequest(string message) + { + return _boundary.InvalidRequest(UpdateSongDirectorResponse.InvalidRequest(message)); + } + + /// + /// Outputs requester rank not found. + /// + /// Requester id. + /// Message. + /// Output. + private async Task RequesterRankNotFound(string requesterId, string message) + { + Result getRequesterFullName = await _songDirectorRepository + .TryFindPropertyByIdAsync(requesterId, nameof(SongDirector.FullName)) + .ConfigureAwait(false); + + // Return the output + return _boundary.RequesterRankNotFound( + UpdateSongDirectorResponse.RequesterRankNotFound( + message, getRequesterFullName.ValueOrDefault)); + } + + /// + /// Tries to make updates a song director is requesting for themself. + /// + /// Requester id. + /// Requester rank. + /// Request. + /// Output. + private async Task TryUpdateSelf( + string requesterId, Rank? requesterRank, UpdateSongDirectorRequest request) + { + Result parseNewRank = Rank.TryGetByName(request.RankName); + + if (parseNewRank.IsFailed) + return InvalidRequest(parseNewRank.GetErrorMessagesString()); + + Rank? newRank = parseNewRank.Value; + + if (requesterRank != Rank.Grandmaster || newRank == Rank.Grandmaster) + return await TryUpdateSongDirector(request).ConfigureAwait(false); + + Result updateGrandmasterRankValidation = await ValidateGrandmasterRankUpdate( + requesterId, newRank).ConfigureAwait(false); + + if (updateGrandmasterRankValidation.IsFailed) + return InvalidRequest(updateGrandmasterRankValidation.GetErrorMessagesString()); + + return await TryUpdateSongDirector(request).ConfigureAwait(false); + } + + /// + /// Tries to update a song director. + /// + /// Request. + /// Output. + private async Task TryUpdateSongDirector(UpdateSongDirectorRequest request) + { + // Try to create a song director from the request + Result createSongDirectorResult = SongDirector + .TryCreate( + request.FullName, + request.EmailAddress, + request.PhoneNumber, + request.RankName, + request.IsActive); + + // If the song director creation failed, return invalid request + if (createSongDirectorResult.IsFailed) + return InvalidRequest(createSongDirectorResult.GetErrorMessagesString()); + + // Reference the song director and set their id + SongDirector songDirector = createSongDirectorResult.Value; + songDirector.UpdateId(request.SongDirectorId); + + // Try to update the song director + Result updateSongDirectorResult = await _songDirectorRepository + .TryUpdateAsync(songDirector) + .ConfigureAwait(false); + + // If the update failed, return song director update failed + if (updateSongDirectorResult.IsFailed) + return SongDirectorUpdateFailed(songDirector.FullName, updateSongDirectorResult); + + // Output song director updated + return SongDirectorUpdated(songDirector.FullName); + } + + /// + /// Validates a grandmaster rank change. + /// + /// Requester id. + /// New rank. + /// The validation result. + private async Task ValidateGrandmasterRankUpdate(string requesterId, Rank? newRank) + { + // If there's no rank change, return an ok result + if (newRank == Rank.Grandmaster) + return Result.Ok(); + + // Get song directors for evaluating if there are additional grandmasters + Result> getSongDirectors = await _songDirectorRepository + .TryGetAllAsync() + .ConfigureAwait(false); + + // Return a failure result if song directors couldn't be retrieved + if (getSongDirectors.IsFailed) + { + return Result.Fail( + "Could not validate that demoting a grandmaster would not result in there" + + $"being no remaining grandmasters. " + + $"{getSongDirectors.GetErrorMessagesString()}"); + } + + // Determine if there is an additional grandmaster + bool isAdditionalGrandmaster = getSongDirectors.Value + .Where(songDirector => songDirector.Id != requesterId) + .Any(songDirector => songDirector.Rank == Rank.Grandmaster); + + // If there isn't, return a failure result + if (!isAdditionalGrandmaster) + { + return Result.Fail( + "You must promote another song director to grandmaster before demoting " + + "yourself."); + } + + // Otherwise, return an ok result + return Result.Ok(); + } +} diff --git a/Asaph.Core/UseCases/UpdateSongDirector/UpdateSongDirectorRequest.cs b/Asaph.Core/UseCases/UpdateSongDirector/UpdateSongDirectorRequest.cs new file mode 100644 index 0000000..06f47f8 --- /dev/null +++ b/Asaph.Core/UseCases/UpdateSongDirector/UpdateSongDirectorRequest.cs @@ -0,0 +1,20 @@ +namespace Asaph.Core.UseCases.UpdateSongDirector; + +/// +/// Request for updating a song director. +/// +/// Requester id. +/// Id of the song director to update. +/// Full name. +/// Email address. +/// Phone number. +/// Rank name. +/// Is active. +public record UpdateSongDirectorRequest( + string? RequesterId, + string? SongDirectorId, + string? FullName, + string? EmailAddress, + string? PhoneNumber, + string? RankName, + bool? IsActive); diff --git a/Asaph.Core/UseCases/UpdateSongDirector/UpdateSongDirectorResponse.cs b/Asaph.Core/UseCases/UpdateSongDirector/UpdateSongDirectorResponse.cs new file mode 100644 index 0000000..791b8d2 --- /dev/null +++ b/Asaph.Core/UseCases/UpdateSongDirector/UpdateSongDirectorResponse.cs @@ -0,0 +1,84 @@ +using System; + +namespace Asaph.Core.UseCases.UpdateSongDirector; + +/// +/// Update song director response. +/// +public record UpdateSongDirectorResponse +{ + /// + /// Initializes a new instance of the record. + /// + /// Message. + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "StyleCop.CSharp.DocumentationRules", + "SA1642:Constructor summary documentation should begin with standard text", + Justification = "UpdateSongDirectorResponse is a record, not a class.")] + private UpdateSongDirectorResponse(string message) => Message = message; + + /// + /// Message. + /// + public string Message { get; } + + /// + /// Creates a response indicating the the requester doesn't have sufficient permissions to + /// perform the update. + /// + /// Song director first name. + /// . + public static UpdateSongDirectorResponse InsufficientPermissions(string? songDirectorFullName) + { + return new UpdateSongDirectorResponse( + "You don't have permission to update " + + $"{(songDirectorFullName != null ? $"{songDirectorFullName}" : "the song director")}."); + } + + /// + /// Creates a response indicating an invalid request. + /// + /// Message. + /// . + public static UpdateSongDirectorResponse InvalidRequest(string message) + { + return new UpdateSongDirectorResponse(message); + } + + /// + /// Creates a response indicating that the requester rank wasn't found. + /// + /// Message. + /// Requester full name. + /// . + public static UpdateSongDirectorResponse RequesterRankNotFound( + string message, string? requesterFullName) + { + return new UpdateSongDirectorResponse( + "Couldn't find requester rank" + + $"{(requesterFullName != null ? $" for {requesterFullName}" : "")}. {message}"); + } + + /// + /// Creates a response indicating that the song director was updated. + /// + /// The song director's full name. + /// . + public static UpdateSongDirectorResponse SongDirectorUpdated(string songDirectorFullName) + { + return new UpdateSongDirectorResponse($"Updated {songDirectorFullName}."); + } + + /// + /// Creates a response indicating that the song director update failed. + /// + /// Song director full name. + /// Message. + /// . + public static UpdateSongDirectorResponse SongDirectorUpdateFailed( + string songDirectorFullName, string message) + { + return new UpdateSongDirectorResponse( + $"Failed to update {songDirectorFullName}. {message}"); + } +} diff --git a/Asaph.Infrastructure/SongDirectorRepository/AggregateSongDirectorRepository.cs b/Asaph.Infrastructure/SongDirectorRepository/AggregateSongDirectorRepository.cs index 8112462..c810707 100644 --- a/Asaph.Infrastructure/SongDirectorRepository/AggregateSongDirectorRepository.cs +++ b/Asaph.Infrastructure/SongDirectorRepository/AggregateSongDirectorRepository.cs @@ -93,7 +93,7 @@ public async Task> TryFindPropertyByIdAsync( .WithErrors(findPropertyByIdResults.SelectMany(r => r.Errors)); } - // If there are multiple successful results that contain different value return a + // If there are multiple successful results that contain different value, return a // failure result return Result.Fail($"The song director with id {id} has inconsistent values for " + $"{propertyName} across data sources. Manual remediation is required."); @@ -398,7 +398,7 @@ private async Task TryOperationWithRollBackSupportAsync( Func> tryRollBackAsync) { - // Cache the song director so that it can be restored if any deletes fail + // Cache the song director so that it can be restored if any changes fail Task> getSongDirectorTask = TryGetByIdAsync(songDirectorId); // Wait for the caching to complete before trying any operations on that song director diff --git a/Asaph.Infrastructure/SongDirectorRepository/AzureAdb2cSongDirectorRepository.cs b/Asaph.Infrastructure/SongDirectorRepository/AzureAdb2cSongDirectorRepository.cs index a5f0dac..b202b20 100644 --- a/Asaph.Infrastructure/SongDirectorRepository/AzureAdb2cSongDirectorRepository.cs +++ b/Asaph.Infrastructure/SongDirectorRepository/AzureAdb2cSongDirectorRepository.cs @@ -71,7 +71,7 @@ public async Task> TryAddAsync( SongDirectorDataModel songDirectorDataModel) { // Create a user object for the user to add - User userToAdd = GetUserFromDataModel(songDirectorDataModel); + User userToAdd = GetUserFromDataModel(songDirectorDataModel, true); try { @@ -283,7 +283,7 @@ public async Task TryUpdateAsync(SongDirectorDataModel songDirectorDataM await _graphServiceClient .Users[songDirectorId] .Request() - .UpdateAsync(GetUserFromDataModel(songDirectorDataModel)) + .UpdateAsync(GetUserFromDataModel(songDirectorDataModel, false)) .ConfigureAwait(false); return Result.Ok(); @@ -322,13 +322,13 @@ await _graphServiceClient /// /// The data model to convert. /// . - private User GetUserFromDataModel(SongDirectorDataModel songDirectorDataModel) + private User GetUserFromDataModel(SongDirectorDataModel songDirectorDataModel, bool isNewUser) { string? fullName = songDirectorDataModel.FullName; string? mailNickname = fullName?.ToLower()?.Replace(' ', '.'); - // Create a user object for the user to add + // Create a user object for the user User microsoftGraphUser = new() { AccountEnabled = true, @@ -337,13 +337,18 @@ private User GetUserFromDataModel(SongDirectorDataModel songDirectorDataModel) Mail = songDirectorDataModel.EmailAddress, MailNickname = mailNickname, MobilePhone = songDirectorDataModel.PhoneNumber, - PasswordProfile = new() + UserPrincipalName = $"{mailNickname}@{_domain}", + }; + + // Configure the password profile for a new user + if (isNewUser) + { + microsoftGraphUser.PasswordProfile = new() { ForceChangePasswordNextSignIn = false, Password = Guid.NewGuid().ToString(), - }, - UserPrincipalName = $"{mailNickname}@{_domain}", - }; + }; + } // Add the song director rank role, if the song director has a rank if (songDirectorDataModel.RankName is string rankName)