From 5066cda3feace0b31454902404713789cef98a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20=C5=A0kr=C3=A1=C5=A1ek?= Date: Fri, 16 Feb 2024 13:51:01 +0100 Subject: [PATCH] Change Nickname (#16) * added change nickname endpoint, contract, command and command handler * added change nickname endpoint tests * fixed variable naming in team endpoints (query to command) --- src/TeamUp.Api/Endpoints/TeamEndpoints.cs | 46 +++- .../ChangeNickname/ChangeNicknameCommand.cs | 8 + .../ChangeNicknameCommandHandler.cs | 27 +++ .../Teams/ChangeNicknameRequest.cs | 24 +++ .../DataGenerators/TeamGenerator.cs | 2 + .../Teams/ChangeNicknameTests.cs | 199 ++++++++++++++++++ 6 files changed, 296 insertions(+), 10 deletions(-) create mode 100644 src/TeamUp.Application/Teams/ChangeNickname/ChangeNicknameCommand.cs create mode 100644 src/TeamUp.Application/Teams/ChangeNickname/ChangeNicknameCommandHandler.cs create mode 100644 src/TeamUp.Contracts/Teams/ChangeNicknameRequest.cs create mode 100644 tests/TeamUp.EndToEndTests/EndpointTests/Teams/ChangeNicknameTests.cs diff --git a/src/TeamUp.Api/Endpoints/TeamEndpoints.cs b/src/TeamUp.Api/Endpoints/TeamEndpoints.cs index 2608ba6..be98bbe 100644 --- a/src/TeamUp.Api/Endpoints/TeamEndpoints.cs +++ b/src/TeamUp.Api/Endpoints/TeamEndpoints.cs @@ -8,6 +8,7 @@ using TeamUp.Application.Teams.DeleteTeam; using TeamUp.Application.Teams.GetTeam; using TeamUp.Application.Teams.RemoveTeamMember; +using TeamUp.Application.Teams.SetMemberNickname; using TeamUp.Application.Teams.SetMemberRole; using TeamUp.Application.Teams.SetTeamName; using TeamUp.Contracts.Teams; @@ -79,6 +80,15 @@ public void MapEndpoints(RouteGroupBuilder group) .ProducesValidationProblem() .WithName(nameof(UpdateTeamRoleAsync)) .MapToApiVersion(1); + + group.MapPut("/{teamId:guid}/nickname", ChangeNicknameAsync) + .Produces(StatusCodes.Status200OK) + .ProducesProblem(StatusCodes.Status401Unauthorized) + .ProducesProblem(StatusCodes.Status403Forbidden) + .ProducesProblem(StatusCodes.Status404NotFound) + .ProducesValidationProblem() + .WithName(nameof(ChangeNicknameAsync)) + .MapToApiVersion(1); } private async Task CreateTeamAsync( @@ -113,8 +123,8 @@ private async Task DeleteTeamAsync( [FromServices] IHttpContextAccessor httpContextAccessor, CancellationToken ct) { - var query = new DeleteTeamCommand(httpContextAccessor.GetLoggedUserId(), TeamId.FromGuid(teamId)); - var result = await sender.Send(query, ct); + var command = new DeleteTeamCommand(httpContextAccessor.GetLoggedUserId(), TeamId.FromGuid(teamId)); + var result = await sender.Send(command, ct); return result.Match(TypedResults.Ok); } @@ -125,12 +135,12 @@ private async Task UpdateTeamNameAsync( [FromServices] IHttpContextAccessor httpContextAccessor, CancellationToken ct) { - var query = new SetTeamNameCommand( + var command = new SetTeamNameCommand( httpContextAccessor.GetLoggedUserId(), TeamId.FromGuid(teamId), request.Name ); - var result = await sender.Send(query, ct); + var result = await sender.Send(command, ct); return result.Match(TypedResults.Ok); } @@ -141,12 +151,12 @@ private async Task ChangeOwnerShipAsync( [FromServices] IHttpContextAccessor httpContextAccessor, CancellationToken ct) { - var query = new ChangeOwnershipCommand( + var command = new ChangeOwnershipCommand( httpContextAccessor.GetLoggedUserId(), TeamId.FromGuid(teamId), TeamMemberId.FromGuid(teamMemberId) ); - var result = await sender.Send(query, ct); + var result = await sender.Send(command, ct); return result.Match(TypedResults.Ok); } @@ -157,12 +167,12 @@ private async Task RemoveTeamMemberAsync( [FromServices] IHttpContextAccessor httpContextAccessor, CancellationToken ct) { - var query = new RemoveTeamMemberCommand( + var command = new RemoveTeamMemberCommand( httpContextAccessor.GetLoggedUserId(), TeamId.FromGuid(teamId), TeamMemberId.FromGuid(teamMemberId) ); - var result = await sender.Send(query, ct); + var result = await sender.Send(command, ct); return result.Match(TypedResults.Ok); } @@ -174,13 +184,29 @@ private async Task UpdateTeamRoleAsync( [FromServices] IHttpContextAccessor httpContextAccessor, CancellationToken ct) { - var query = new SetMemberRoleCommand( + var command = new SetMemberRoleCommand( httpContextAccessor.GetLoggedUserId(), TeamId.FromGuid(teamId), TeamMemberId.FromGuid(teamMemberId), request.Role ); - var result = await sender.Send(query, ct); + var result = await sender.Send(command, ct); + return result.Match(TypedResults.Ok); + } + + private async Task ChangeNicknameAsync( + [FromRoute] Guid teamId, + [FromBody] ChangeNicknameRequest request, + [FromServices] ISender sender, + [FromServices] IHttpContextAccessor httpContextAccessor, + CancellationToken ct) + { + var command = new ChangeNicknameCommand( + httpContextAccessor.GetLoggedUserId(), + TeamId.FromGuid(teamId), + request.Nickname + ); + var result = await sender.Send(command, ct); return result.Match(TypedResults.Ok); } } diff --git a/src/TeamUp.Application/Teams/ChangeNickname/ChangeNicknameCommand.cs b/src/TeamUp.Application/Teams/ChangeNickname/ChangeNicknameCommand.cs new file mode 100644 index 0000000..4b07311 --- /dev/null +++ b/src/TeamUp.Application/Teams/ChangeNickname/ChangeNicknameCommand.cs @@ -0,0 +1,8 @@ +using TeamUp.Application.Abstractions; +using TeamUp.Common; +using TeamUp.Domain.Aggregates.Teams; +using TeamUp.Domain.Aggregates.Users; + +namespace TeamUp.Application.Teams.SetMemberNickname; + +public sealed record ChangeNicknameCommand(UserId InitiatorId, TeamId TeamId, string Nickname) : ICommand; diff --git a/src/TeamUp.Application/Teams/ChangeNickname/ChangeNicknameCommandHandler.cs b/src/TeamUp.Application/Teams/ChangeNickname/ChangeNicknameCommandHandler.cs new file mode 100644 index 0000000..6f8d690 --- /dev/null +++ b/src/TeamUp.Application/Teams/ChangeNickname/ChangeNicknameCommandHandler.cs @@ -0,0 +1,27 @@ +using TeamUp.Application.Abstractions; +using TeamUp.Common; +using TeamUp.Domain.Abstractions; +using TeamUp.Domain.Aggregates.Teams; + +namespace TeamUp.Application.Teams.SetMemberNickname; + +internal sealed class ChangeNicknameCommandHandler : ICommandHandler +{ + private readonly ITeamRepository _teamRepository; + private readonly IUnitOfWork _unitOfWork; + + public ChangeNicknameCommandHandler(ITeamRepository teamRepository, IUnitOfWork unitOfWork) + { + _teamRepository = teamRepository; + _unitOfWork = unitOfWork; + } + + public async Task Handle(ChangeNicknameCommand request, CancellationToken ct) + { + var team = await _teamRepository.GetTeamByIdAsync(request.TeamId, ct); + return await team + .EnsureNotNull(TeamErrors.TeamNotFound) + .Then(team => team.ChangeNickname(request.InitiatorId, request.Nickname)) + .TapAsync(() => _unitOfWork.SaveChangesAsync(ct)); + } +} diff --git a/src/TeamUp.Contracts/Teams/ChangeNicknameRequest.cs b/src/TeamUp.Contracts/Teams/ChangeNicknameRequest.cs new file mode 100644 index 0000000..c20e74a --- /dev/null +++ b/src/TeamUp.Contracts/Teams/ChangeNicknameRequest.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +using FluentValidation; + +using TeamUp.Domain.Aggregates.Teams; + +namespace TeamUp.Contracts.Teams; + +public sealed class ChangeNicknameRequest : IRequestBody +{ + [DataType(DataType.Text)] + public required string Nickname { get; init; } + + public sealed class Validator : AbstractValidator + { + public Validator() + { + RuleFor(x => x.Nickname) + .NotEmpty() + .MinimumLength(Team.NICKNAME_MIN_SIZE) + .MaximumLength(Team.NICKNAME_MAX_SIZE); + } + } +} diff --git a/tests/TeamUp.EndToEndTests/DataGenerators/TeamGenerator.cs b/tests/TeamUp.EndToEndTests/DataGenerators/TeamGenerator.cs index 80f8b18..65b5e20 100644 --- a/tests/TeamUp.EndToEndTests/DataGenerators/TeamGenerator.cs +++ b/tests/TeamUp.EndToEndTests/DataGenerators/TeamGenerator.cs @@ -14,6 +14,8 @@ public sealed class TeamGenerator : BaseGenerator public static string GenerateValidTeamName() => F.Random.AlphaNumeric(10); + public static string GenerateValidNickname() => F.Random.AlphaNumeric(10); + public static readonly Faker EmptyTeam = new Faker(binder: TeamBinder) .UsePrivateConstructor() .RuleFor(t => t.Id, f => TeamId.FromGuid(f.Random.Uuid())) diff --git a/tests/TeamUp.EndToEndTests/EndpointTests/Teams/ChangeNicknameTests.cs b/tests/TeamUp.EndToEndTests/EndpointTests/Teams/ChangeNicknameTests.cs new file mode 100644 index 0000000..bfafb76 --- /dev/null +++ b/tests/TeamUp.EndToEndTests/EndpointTests/Teams/ChangeNicknameTests.cs @@ -0,0 +1,199 @@ +using Microsoft.EntityFrameworkCore; + +using TeamUp.Contracts.Teams; +using TeamUp.Domain.Aggregates.Teams; + +namespace TeamUp.EndToEndTests.EndpointTests.Teams; + +public sealed class ChangeNicknameTests : BaseTeamTests +{ + public ChangeNicknameTests(TeamApiWebApplicationFactory appFactory) : base(appFactory) { } + + [Fact] + public async Task ChangeNickname_ToValidNickname_AsOwner_Should_UpdateNicknameInDatabase() + { + //arrange + var owner = UserGenerator.ActivatedUser.Generate(); + var members = UserGenerator.ActivatedUser.Generate(19); + var team = TeamGenerator.GenerateTeamWith(owner, members); + + await UseDbContextAsync(dbContext => + { + dbContext.Users.Add(owner); + dbContext.Users.AddRange(members); + dbContext.Teams.Add(team); + return dbContext.SaveChangesAsync(); + }); + + Authenticate(owner); + + var targetMemberId = team.Members.FirstOrDefault(member => member.UserId == owner.Id)!.Id; + var request = new ChangeNicknameRequest + { + Nickname = TeamGenerator.GenerateValidNickname() + }; + + //act + var response = await Client.PutAsJsonAsync($"/api/v1/teams/{team.Id.Value}/nickname", request); + + //assert + response.Should().Be200Ok(); + + var member = await UseDbContextAsync(dbContext => + { + return dbContext + .Set() + .SingleOrDefaultAsync(member => member.Id == targetMemberId); + }); + + member.ShouldNotBeNull(); + member.Nickname.Should().Be(request.Nickname); + } + + [Theory] + [InlineData(TeamRole.Member)] + [InlineData(TeamRole.Coordinator)] + [InlineData(TeamRole.Admin)] + public async Task ChangeNickname_ToValidNickname_AsAdminOrLower_Should_UpdateNicknameInDatabase(TeamRole teamRole) + { + //arrange + var owner = UserGenerator.ActivatedUser.Generate(); + var initiatorUser = UserGenerator.ActivatedUser.Generate(); + var members = UserGenerator.ActivatedUser.Generate(18); + var team = TeamGenerator.GenerateTeamWith(owner, members, (initiatorUser, teamRole)); + + await UseDbContextAsync(dbContext => + { + dbContext.Users.AddRange([owner, initiatorUser]); + dbContext.Users.AddRange(members); + dbContext.Teams.Add(team); + return dbContext.SaveChangesAsync(); + }); + + Authenticate(initiatorUser); + + var targetMemberId = team.Members.FirstOrDefault(member => member.UserId == initiatorUser.Id)!.Id; + var request = new ChangeNicknameRequest + { + Nickname = TeamGenerator.GenerateValidNickname() + }; + + //act + var response = await Client.PutAsJsonAsync($"/api/v1/teams/{team.Id.Value}/nickname", request); + + //assert + response.Should().Be200Ok(); + + var member = await UseDbContextAsync(dbContext => + { + return dbContext + .Set() + .SingleOrDefaultAsync(member => member.Id == targetMemberId); + }); + + member.ShouldNotBeNull(); + member.Nickname.Should().Be(request.Nickname); + } + + [Fact] + public async Task ChangeNickname_WhenNotMemberOfTeam_Should_ResultInForbidden() + { + //arrange + var owner = UserGenerator.ActivatedUser.Generate(); + var initiatorUser = UserGenerator.ActivatedUser.Generate(); + var members = UserGenerator.ActivatedUser.Generate(19); + var team = TeamGenerator.GenerateTeamWith(owner, members); + + await UseDbContextAsync(dbContext => + { + dbContext.Users.AddRange([owner, initiatorUser]); + dbContext.Users.AddRange(members); + dbContext.Teams.Add(team); + return dbContext.SaveChangesAsync(); + }); + + Authenticate(initiatorUser); + + var request = new ChangeNicknameRequest + { + Nickname = TeamGenerator.GenerateValidNickname() + }; + + //act + var response = await Client.PutAsJsonAsync($"/api/v1/teams/{team.Id.Value}/nickname", request); + + //assert + response.Should().Be403Forbidden(); + + var problemDetails = await response.Content.ReadFromJsonAsync(); + problemDetails.ShouldContainError(TeamErrors.NotMemberOfTeam); + } + + [Fact] + public async Task ChangeNickname_InTeamThatDoesNotExist_Should_ResultInNotFound() + { + //arrange + var user = UserGenerator.ActivatedUser.Generate(); + + await UseDbContextAsync(dbContext => + { + dbContext.Users.Add(user); + return dbContext.SaveChangesAsync(); + }); + + Authenticate(user); + + var teamId = F.Random.Guid(); + var request = new ChangeNicknameRequest + { + Nickname = TeamGenerator.GenerateValidNickname() + }; + + //act + var response = await Client.PutAsJsonAsync($"/api/v1/teams/{teamId}/nickname", request); + + //assert + response.Should().Be404NotFound(); + + var problemDetails = await response.Content.ReadFromJsonAsync(); + problemDetails.ShouldContainError(TeamErrors.TeamNotFound); + } + + [Theory] + [InlineData("")] + [InlineData("x")] + [InlineData("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")] + public async Task ChangeNickname_ToInvalidName_Should_ResultInBadRequest(string invalidName) + { + //arrange + var owner = UserGenerator.ActivatedUser.Generate(); + var initiatorUser = UserGenerator.ActivatedUser.Generate(); + var members = UserGenerator.ActivatedUser.Generate(18); + var team = TeamGenerator.GenerateTeamWith(owner, members, (initiatorUser, TeamRole.Member)); + + await UseDbContextAsync(dbContext => + { + dbContext.Users.AddRange([owner, initiatorUser]); + dbContext.Users.AddRange(members); + dbContext.Teams.Add(team); + return dbContext.SaveChangesAsync(); + }); + + Authenticate(initiatorUser); + + var targetMemberId = team.Members.FirstOrDefault(member => member.UserId == initiatorUser.Id)!.Id; + var request = new ChangeNicknameRequest + { + Nickname = invalidName + }; + + //act + var response = await Client.PutAsJsonAsync($"/api/v1/teams/{team.Id.Value}/nickname", request); + + //assert + response.Should().Be400BadRequest(); + + var problemDetails = await response.Content.ReadFromJsonAsync(); + problemDetails.ShouldContainValidationErrorFor(nameof(ChangeNicknameRequest.Nickname)); + } +}