Skip to content

Commit

Permalink
Change Nickname (#16)
Browse files Browse the repository at this point in the history
* added change nickname endpoint, contract, command and command handler
* added change nickname endpoint tests
* fixed variable naming in team endpoints (query to command)
  • Loading branch information
skrasekmichael authored Feb 16, 2024
1 parent 2d42f1c commit 5066cda
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 10 deletions.
46 changes: 36 additions & 10 deletions src/TeamUp.Api/Endpoints/TeamEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<IResult> CreateTeamAsync(
Expand Down Expand Up @@ -113,8 +123,8 @@ private async Task<IResult> 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);
}

Expand All @@ -125,12 +135,12 @@ private async Task<IResult> 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);
}

Expand All @@ -141,12 +151,12 @@ private async Task<IResult> 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);
}

Expand All @@ -157,12 +167,12 @@ private async Task<IResult> 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);
}

Expand All @@ -174,13 +184,29 @@ private async Task<IResult> 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<IResult> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Result>;
Original file line number Diff line number Diff line change
@@ -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<ChangeNicknameCommand, Result>
{
private readonly ITeamRepository _teamRepository;
private readonly IUnitOfWork _unitOfWork;

public ChangeNicknameCommandHandler(ITeamRepository teamRepository, IUnitOfWork unitOfWork)
{
_teamRepository = teamRepository;
_unitOfWork = unitOfWork;
}

public async Task<Result> 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));
}
}
24 changes: 24 additions & 0 deletions src/TeamUp.Contracts/Teams/ChangeNicknameRequest.cs
Original file line number Diff line number Diff line change
@@ -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<ChangeNicknameRequest>
{
public Validator()
{
RuleFor(x => x.Nickname)
.NotEmpty()
.MinimumLength(Team.NICKNAME_MIN_SIZE)
.MaximumLength(Team.NICKNAME_MAX_SIZE);
}
}
}
2 changes: 2 additions & 0 deletions tests/TeamUp.EndToEndTests/DataGenerators/TeamGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Team> EmptyTeam = new Faker<Team>(binder: TeamBinder)
.UsePrivateConstructor()
.RuleFor(t => t.Id, f => TeamId.FromGuid(f.Random.Uuid()))
Expand Down
199 changes: 199 additions & 0 deletions tests/TeamUp.EndToEndTests/EndpointTests/Teams/ChangeNicknameTests.cs
Original file line number Diff line number Diff line change
@@ -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<TeamMember>()
.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<TeamMember>()
.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>();
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>();
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<ValidationProblemDetails>();
problemDetails.ShouldContainValidationErrorFor(nameof(ChangeNicknameRequest.Nickname));
}
}

0 comments on commit 5066cda

Please sign in to comment.