From c674d77fe5fa6b30607a2fd1ca9d333b01394dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20=C5=A0kr=C3=A1=C5=A1ek?= Date: Sun, 25 Feb 2024 17:37:49 +0100 Subject: [PATCH] Get My Invitations (#28) - added get my invitations endpoint, query and query handler - added get my invitations endpoint tests - fixed get team invitations --- .../Invitations/GetMyInvitationsEndpoint.cs | 31 +++++++ .../Endpoints/InvitationsEndpointGroup.cs | 3 +- .../GetMyInvitations/GetMyInvitationsQuery.cs | 8 ++ .../GetMyInvitationsQueryHandler.cs | 30 +++++++ .../GetTeamInvitationsQueryHandler.cs | 13 +-- .../Invitations/InvitationResponse.cs | 10 +++ .../Invitations/TeamInvitationResponse.cs | 2 +- .../DataGenerators/InvitationGenerator.cs | 13 ++- .../Invitations/GetMyInvitationsTests.cs | 83 +++++++++++++++++++ .../Invitations/GetTeamInvitationsTests.cs | 8 +- 10 files changed, 185 insertions(+), 16 deletions(-) create mode 100644 src/TeamUp.Api/Endpoints/Invitations/GetMyInvitationsEndpoint.cs create mode 100644 src/TeamUp.Application/Invitations/GetMyInvitations/GetMyInvitationsQuery.cs create mode 100644 src/TeamUp.Application/Invitations/GetMyInvitations/GetMyInvitationsQueryHandler.cs create mode 100644 src/TeamUp.Contracts/Invitations/InvitationResponse.cs create mode 100644 tests/TeamUp.EndToEndTests/EndpointTests/Invitations/GetMyInvitationsTests.cs diff --git a/src/TeamUp.Api/Endpoints/Invitations/GetMyInvitationsEndpoint.cs b/src/TeamUp.Api/Endpoints/Invitations/GetMyInvitationsEndpoint.cs new file mode 100644 index 0000000..644c47b --- /dev/null +++ b/src/TeamUp.Api/Endpoints/Invitations/GetMyInvitationsEndpoint.cs @@ -0,0 +1,31 @@ +using MediatR; + +using Microsoft.AspNetCore.Mvc; + +using TeamUp.Api.Extensions; +using TeamUp.Application.Invitations.GetMyInvitations; +using TeamUp.Contracts.Invitations; + +namespace TeamUp.Api.Endpoints.Invitations; + +public sealed class GetMyInvitationsEndpoint : IEndpointGroup +{ + public void MapEndpoints(RouteGroupBuilder group) + { + group.MapGet("/", GetTeamInvitationsAsync) + .Produces>(StatusCodes.Status200OK) + .ProducesProblem(StatusCodes.Status401Unauthorized) + .WithName(nameof(GetMyInvitationsEndpoint)) + .MapToApiVersion(1); + } + + private async Task GetTeamInvitationsAsync( + [FromServices] ISender sender, + HttpContext httpContext, + CancellationToken ct) + { + var query = new GetMyInvitationsQuery(httpContext.GetCurrentUserId()); + var result = await sender.Send(query, ct); + return result.Match(TypedResults.Ok); + } +} diff --git a/src/TeamUp.Api/Endpoints/InvitationsEndpointGroup.cs b/src/TeamUp.Api/Endpoints/InvitationsEndpointGroup.cs index b7615e8..1152500 100644 --- a/src/TeamUp.Api/Endpoints/InvitationsEndpointGroup.cs +++ b/src/TeamUp.Api/Endpoints/InvitationsEndpointGroup.cs @@ -10,6 +10,7 @@ public void MapEndpoints(RouteGroupBuilder group) group.RequireAuthorization() .MapEndpoint() .MapEndpoint() - .MapEndpoint(); + .MapEndpoint() + .MapEndpoint(); } } diff --git a/src/TeamUp.Application/Invitations/GetMyInvitations/GetMyInvitationsQuery.cs b/src/TeamUp.Application/Invitations/GetMyInvitations/GetMyInvitationsQuery.cs new file mode 100644 index 0000000..7f8f2c4 --- /dev/null +++ b/src/TeamUp.Application/Invitations/GetMyInvitations/GetMyInvitationsQuery.cs @@ -0,0 +1,8 @@ +using TeamUp.Application.Abstractions; +using TeamUp.Common; +using TeamUp.Contracts.Invitations; +using TeamUp.Contracts.Users; + +namespace TeamUp.Application.Invitations.GetMyInvitations; + +public sealed record GetMyInvitationsQuery(UserId InitiatorId) : IQuery>>; diff --git a/src/TeamUp.Application/Invitations/GetMyInvitations/GetMyInvitationsQueryHandler.cs b/src/TeamUp.Application/Invitations/GetMyInvitations/GetMyInvitationsQueryHandler.cs new file mode 100644 index 0000000..ed1b2f7 --- /dev/null +++ b/src/TeamUp.Application/Invitations/GetMyInvitations/GetMyInvitationsQueryHandler.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore; + +using TeamUp.Application.Abstractions; +using TeamUp.Common; +using TeamUp.Contracts.Invitations; + +namespace TeamUp.Application.Invitations.GetMyInvitations; + +internal sealed class GetMyInvitationsQueryHandler : IQueryHandler>> +{ + private readonly IAppQueryContext _appQueryContext; + + public GetMyInvitationsQueryHandler(IAppQueryContext appQueryContext) + { + _appQueryContext = appQueryContext; + } + + public async Task>> Handle(GetMyInvitationsQuery request, CancellationToken ct) + { + return await _appQueryContext.Invitations + .Where(invitation => invitation.RecipientId == request.InitiatorId) + .Select(invitation => new InvitationResponse + { + Id = invitation.Id, + TeamName = _appQueryContext.Teams.First(team => team.Id == invitation.TeamId).Name, + CreatedUtc = invitation.CreatedUtc + }) + .ToListAsync(ct); + } +} diff --git a/src/TeamUp.Application/Invitations/GetTeamInvitations/GetTeamInvitationsQueryHandler.cs b/src/TeamUp.Application/Invitations/GetTeamInvitations/GetTeamInvitationsQueryHandler.cs index 1cee0ff..8e40771 100644 --- a/src/TeamUp.Application/Invitations/GetTeamInvitations/GetTeamInvitationsQueryHandler.cs +++ b/src/TeamUp.Application/Invitations/GetTeamInvitations/GetTeamInvitationsQueryHandler.cs @@ -35,18 +35,13 @@ public async Task>> Handle(GetTeamInvitation .ThenAsync(team => { return _appQueryContext.Invitations - .Select(invitation => new - { - invitation.TeamId, - invitation.CreatedUtc, - _appQueryContext.Users - .Select(user => new { user.Id, user.Email }) - .First(user => user.Id == invitation.RecipientId).Email, - }) .Where(invitation => invitation.TeamId == request.TeamId) .Select(invitation => new TeamInvitationResponse { - Email = invitation.Email, + Id = invitation.Id, + Email = _appQueryContext.Users + .Select(user => new { user.Id, user.Email }) + .First(user => user.Id == invitation.RecipientId).Email, CreatedUtc = invitation.CreatedUtc }) .ToListAsync(ct); diff --git a/src/TeamUp.Contracts/Invitations/InvitationResponse.cs b/src/TeamUp.Contracts/Invitations/InvitationResponse.cs new file mode 100644 index 0000000..85fc4e0 --- /dev/null +++ b/src/TeamUp.Contracts/Invitations/InvitationResponse.cs @@ -0,0 +1,10 @@ +using TeamUp.Contracts.Teams; + +namespace TeamUp.Contracts.Invitations; + +public sealed class InvitationResponse +{ + public required InvitationId Id { get; init; } + public required string TeamName { get; init; } + public required DateTime CreatedUtc { get; init; } +} diff --git a/src/TeamUp.Contracts/Invitations/TeamInvitationResponse.cs b/src/TeamUp.Contracts/Invitations/TeamInvitationResponse.cs index 91353db..9edb162 100644 --- a/src/TeamUp.Contracts/Invitations/TeamInvitationResponse.cs +++ b/src/TeamUp.Contracts/Invitations/TeamInvitationResponse.cs @@ -2,7 +2,7 @@ public sealed class TeamInvitationResponse { + public required InvitationId Id { get; init; } public required string Email { get; init; } - public required DateTime CreatedUtc { get; init; } } diff --git a/tests/TeamUp.EndToEndTests/DataGenerators/InvitationGenerator.cs b/tests/TeamUp.EndToEndTests/DataGenerators/InvitationGenerator.cs index 005eb6d..ef697c7 100644 --- a/tests/TeamUp.EndToEndTests/DataGenerators/InvitationGenerator.cs +++ b/tests/TeamUp.EndToEndTests/DataGenerators/InvitationGenerator.cs @@ -24,7 +24,7 @@ public static Invitation GenerateInvitation(UserId userId, TeamId teamId, DateTi .Generate(); } - public static List GenerateInvitations(TeamId teamId, DateTime createdUtc, List users) + public static List GenerateTeamInvitations(TeamId teamId, DateTime createdUtc, List users) { return users.Select(user => EmptyInvitation @@ -35,6 +35,17 @@ public static List GenerateInvitations(TeamId teamId, DateTime creat ).ToList(); } + public static List GenerateUserInvitations(UserId userId, DateTime createdUtc, List teams) + { + return teams.Select(team => + EmptyInvitation + .RuleForBackingField(i => i.RecipientId, userId) + .RuleForBackingField(i => i.TeamId, team.Id) + .RuleFor(i => i.CreatedUtc, createdUtc) + .Generate() + ).ToList(); + } + public sealed class InvalidInviteUserRequest : TheoryData> { public InvalidInviteUserRequest() diff --git a/tests/TeamUp.EndToEndTests/EndpointTests/Invitations/GetMyInvitationsTests.cs b/tests/TeamUp.EndToEndTests/EndpointTests/Invitations/GetMyInvitationsTests.cs new file mode 100644 index 0000000..730c364 --- /dev/null +++ b/tests/TeamUp.EndToEndTests/EndpointTests/Invitations/GetMyInvitationsTests.cs @@ -0,0 +1,83 @@ +using TeamUp.Contracts.Invitations; + +namespace TeamUp.EndToEndTests.EndpointTests.Invitations; + +public sealed class GetMyInvitationsTests : BaseInvitationTests +{ + public GetMyInvitationsTests(TeamApiWebApplicationFactory appFactory) : base(appFactory) { } + + public const string URL = "/api/v1/invitations"; + + [Fact] + public async Task GetMyInvitations_Should_ReturnListOfInvitations() + { + //arrange + var owner = UserGenerator.ActivatedUser.Generate(); + var initiatorUser = UserGenerator.ActivatedUser.Generate(); + var members = UserGenerator.ActivatedUser.Generate(19); + var teams = new List + { + TeamGenerator.GenerateTeamWith(owner, members), + TeamGenerator.GenerateTeamWith(owner, members), + TeamGenerator.GenerateTeamWith(owner, members) + }; + + //need to remove milliseconds as there is slight shift when saving to database + var utcNow = new DateTime(DateTime.UtcNow.Ticks / TimeSpan.TicksPerSecond * TimeSpan.TicksPerSecond, DateTimeKind.Utc); + var invitations = InvitationGenerator.GenerateUserInvitations(initiatorUser.Id, utcNow, teams); + + await UseDbContextAsync(dbContext => + { + dbContext.Users.AddRange([owner, initiatorUser]); + dbContext.Users.AddRange(members); + dbContext.Teams.AddRange(teams); + dbContext.Invitations.AddRange(invitations); + return dbContext.SaveChangesAsync(); + }); + + Authenticate(initiatorUser); + + //act + var response = await Client.GetAsync(URL); + + //assert + response.Should().Be200Ok(); + + var userInvitations = await response.ReadFromJsonAsync>(); + invitations.Should().BeEquivalentTo(userInvitations, o => o.ExcludingMissingMembers()); + } + + [Fact] + public async Task GetMyInvitations_WhenNotInvited_Should_ReturnEmptyList() + { + //arrange + var owner = UserGenerator.ActivatedUser.Generate(); + var initiatorUser = UserGenerator.ActivatedUser.Generate(); + var members = UserGenerator.ActivatedUser.Generate(19); + var teams = new List + { + TeamGenerator.GenerateTeamWith(owner, members), + TeamGenerator.GenerateTeamWith(owner, members), + TeamGenerator.GenerateTeamWith(owner, members) + }; + + await UseDbContextAsync(dbContext => + { + dbContext.Users.AddRange([owner, initiatorUser]); + dbContext.Users.AddRange(members); + dbContext.Teams.AddRange(teams); + return dbContext.SaveChangesAsync(); + }); + + Authenticate(initiatorUser); + + //act + var response = await Client.GetAsync(URL); + + //assert + response.Should().Be200Ok(); + + var invitations = await response.ReadFromJsonAsync>(); + invitations.Should().BeEmpty(); + } +} diff --git a/tests/TeamUp.EndToEndTests/EndpointTests/Invitations/GetTeamInvitationsTests.cs b/tests/TeamUp.EndToEndTests/EndpointTests/Invitations/GetTeamInvitationsTests.cs index 5c79ff0..c2ce237 100644 --- a/tests/TeamUp.EndToEndTests/EndpointTests/Invitations/GetTeamInvitationsTests.cs +++ b/tests/TeamUp.EndToEndTests/EndpointTests/Invitations/GetTeamInvitationsTests.cs @@ -21,9 +21,9 @@ public async Task GetTeamInvitations_AsCoordinatorOrHigher_Should_ReturnListOfIn var members = UserGenerator.ActivatedUser.Generate(19); var team = TeamGenerator.GenerateTeamWith(initiatorUser, teamRole, members); - //need to remove milliseconds as when saving to database, there is slight shift + //need to remove milliseconds as there is slight shift when saving to database var utcNow = new DateTime(DateTime.UtcNow.Ticks / TimeSpan.TicksPerSecond * TimeSpan.TicksPerSecond, DateTimeKind.Utc); - var invitations = InvitationGenerator.GenerateInvitations(team.Id, utcNow, members); + var invitations = InvitationGenerator.GenerateTeamInvitations(team.Id, utcNow, members); await UseDbContextAsync(dbContext => { @@ -53,7 +53,7 @@ public async Task GetTeamInvitations_AsMember_Should_ResultInForbidden() var initiatorUser = UserGenerator.ActivatedUser.Generate(); var members = UserGenerator.ActivatedUser.Generate(19); var team = TeamGenerator.GenerateTeamWith(initiatorUser, TeamRole.Member, members); - var invitations = InvitationGenerator.GenerateInvitations(team.Id, DateTime.UtcNow, members); + var invitations = InvitationGenerator.GenerateTeamInvitations(team.Id, DateTime.UtcNow, members); await UseDbContextAsync(dbContext => { @@ -84,7 +84,7 @@ public async Task GetTeamInvitations_WhenNotMemberOfTeam_Should_ResultInForbidde var initiatorUser = UserGenerator.ActivatedUser.Generate(); var members = UserGenerator.ActivatedUser.Generate(19); var team = TeamGenerator.GenerateTeamWith(owner, members); - var invitations = InvitationGenerator.GenerateInvitations(team.Id, DateTime.UtcNow, members); + var invitations = InvitationGenerator.GenerateTeamInvitations(team.Id, DateTime.UtcNow, members); await UseDbContextAsync(dbContext => {