diff --git a/Src/DeUrgenta.Api/Startup.cs b/Src/DeUrgenta.Api/Startup.cs index 334c863..6bb27ae 100644 --- a/Src/DeUrgenta.Api/Startup.cs +++ b/Src/DeUrgenta.Api/Startup.cs @@ -48,7 +48,7 @@ public void ConfigureServices(IServiceCollection services) services.AddUserApiServices(); services.AddBackpackApiServices(); - services.AddGroupApiServices(); + services.AddGroupApiServices(Configuration); services.AddCertificationsApiServices(); services.AddEventsApiServices(); services.AddAdminApiServices(); diff --git a/Src/DeUrgenta.Api/appsettings.json b/Src/DeUrgenta.Api/appsettings.json index 30b3067..d2692a9 100644 --- a/Src/DeUrgenta.Api/appsettings.json +++ b/Src/DeUrgenta.Api/appsettings.json @@ -44,5 +44,9 @@ "Region": "", "AWS_ACCESS_KEY_ID": "", "AWS_SECRET_ACCESS_KEY": "" + }, + "Group": { + "MaxJoinedGroupsPerUser": 5, + "MaxCreatedGroupsPerUser": 5 } } \ No newline at end of file diff --git a/Src/DeUrgenta.Group.Api/BootstrappingExtensions.cs b/Src/DeUrgenta.Group.Api/BootstrappingExtensions.cs index ba9a9f3..725f00c 100644 --- a/Src/DeUrgenta.Group.Api/BootstrappingExtensions.cs +++ b/Src/DeUrgenta.Group.Api/BootstrappingExtensions.cs @@ -1,17 +1,19 @@ using DeUrgenta.Common.Validation; using DeUrgenta.Group.Api.Commands; using DeUrgenta.Group.Api.Models; +using DeUrgenta.Group.Api.Options; using DeUrgenta.Group.Api.Queries; using DeUrgenta.Group.Api.Validators; using DeUrgenta.Group.Api.Validators.RequestValidators; using FluentValidation; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace DeUrgenta.Group.Api { public static class BootstrappingExtensions { - public static IServiceCollection AddGroupApiServices(this IServiceCollection services) + public static IServiceCollection AddGroupApiServices(this IServiceCollection services, IConfiguration configuration) { services.AddTransient, AddGroupValidator>(); services.AddTransient, AddSafeLocationValidator>(); @@ -29,6 +31,8 @@ public static IServiceCollection AddGroupApiServices(this IServiceCollection ser services.AddTransient, GroupRequestValidator>(); services.AddTransient, SafeLocationRequestValidator>(); + services.Configure(configuration.GetSection(GroupsConfig.SectionName)); + return services; } } diff --git a/Src/DeUrgenta.Group.Api/CommandHandlers/AddGroupHandler.cs b/Src/DeUrgenta.Group.Api/CommandHandlers/AddGroupHandler.cs index 4404ab8..3707d2a 100644 --- a/Src/DeUrgenta.Group.Api/CommandHandlers/AddGroupHandler.cs +++ b/Src/DeUrgenta.Group.Api/CommandHandlers/AddGroupHandler.cs @@ -31,7 +31,7 @@ public async Task> Handle(AddGroup request, CancellationToken } var user = await _context.Users.FirstAsync(u => u.Sub == request.UserSub, cancellationToken); - + var newBackpack = new Backpack { Name = $"Backpack for {request.Group.Name}" diff --git a/Src/DeUrgenta.Group.Api/CommandHandlers/InviteToGroupHandler.cs b/Src/DeUrgenta.Group.Api/CommandHandlers/InviteToGroupHandler.cs index 5e1f5cb..5ef76c8 100644 --- a/Src/DeUrgenta.Group.Api/CommandHandlers/InviteToGroupHandler.cs +++ b/Src/DeUrgenta.Group.Api/CommandHandlers/InviteToGroupHandler.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Configuration; +using System.Threading; using System.Threading.Tasks; using CSharpFunctionalExtensions; using DeUrgenta.Common.Validation; @@ -6,6 +7,7 @@ using DeUrgenta.Domain.Entities; using DeUrgenta.Group.Api.Commands; using MediatR; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace DeUrgenta.Group.Api.CommandHandlers diff --git a/Src/DeUrgenta.Group.Api/Options/GroupsConfig.cs b/Src/DeUrgenta.Group.Api/Options/GroupsConfig.cs new file mode 100644 index 0000000..85b78f1 --- /dev/null +++ b/Src/DeUrgenta.Group.Api/Options/GroupsConfig.cs @@ -0,0 +1,10 @@ +namespace DeUrgenta.Group.Api.Options +{ + public class GroupsConfig + { + public const string SectionName = "Groups"; + + public int MaxJoinedGroupsPerUser { get; set; } + public int MaxCreatedGroupsPerUser { get; set; } + } +} \ No newline at end of file diff --git a/Src/DeUrgenta.Group.Api/Validators/AddGroupValidator.cs b/Src/DeUrgenta.Group.Api/Validators/AddGroupValidator.cs index f61fb8f..cb1ebfb 100644 --- a/Src/DeUrgenta.Group.Api/Validators/AddGroupValidator.cs +++ b/Src/DeUrgenta.Group.Api/Validators/AddGroupValidator.cs @@ -2,17 +2,27 @@ using DeUrgenta.Common.Validation; using DeUrgenta.Domain; using DeUrgenta.Group.Api.Commands; +using DeUrgenta.Group.Api.Options; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; namespace DeUrgenta.Group.Api.Validators { public class AddGroupValidator : IValidateRequest { private readonly DeUrgentaContext _context; + private readonly GroupsConfig _groupsConfig; - public AddGroupValidator(DeUrgentaContext context) + public AddGroupValidator(DeUrgentaContext context, GroupsConfig groupsConfig) { _context = context; + _groupsConfig = groupsConfig; + } + + public AddGroupValidator(DeUrgentaContext context, IOptions groupsConfig) + { + _context = context; + _groupsConfig = groupsConfig.Value; } public async Task IsValidAsync(AddGroup request) @@ -22,6 +32,11 @@ public async Task IsValidAsync(AddGroup request) { return false; } + + if (user.GroupsAdministered.Count >= _groupsConfig.MaxCreatedGroupsPerUser) + { + return false; + } return true; } diff --git a/Src/DeUrgenta.Group.Api/Validators/InviteToGroupValidator.cs b/Src/DeUrgenta.Group.Api/Validators/InviteToGroupValidator.cs index a54d838..739f690 100644 --- a/Src/DeUrgenta.Group.Api/Validators/InviteToGroupValidator.cs +++ b/Src/DeUrgenta.Group.Api/Validators/InviteToGroupValidator.cs @@ -2,17 +2,27 @@ using DeUrgenta.Common.Validation; using DeUrgenta.Domain; using DeUrgenta.Group.Api.Commands; +using DeUrgenta.Group.Api.Options; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; namespace DeUrgenta.Group.Api.Validators { public class InviteToGroupValidator : IValidateRequest { private readonly DeUrgentaContext _context; + private readonly GroupsConfig _groupsConfig; - public InviteToGroupValidator(DeUrgentaContext context) + public InviteToGroupValidator(DeUrgentaContext context, GroupsConfig groupsConfig) { _context = context; + _groupsConfig = groupsConfig; + } + + public InviteToGroupValidator(DeUrgentaContext context, IOptions groupsConfig) + { + _context = context; + _groupsConfig = groupsConfig.Value; } public async Task IsValidAsync(InviteToGroup request) @@ -53,6 +63,11 @@ public async Task IsValidAsync(InviteToGroup request) { return false; } + + if (invitedUser.GroupsMember.Count >= _groupsConfig.MaxJoinedGroupsPerUser) + { + return false; + } return true; } diff --git a/Src/DeUrgenta.User.Api/Controller/UserController.cs b/Src/DeUrgenta.User.Api/Controller/UserController.cs index efceb89..d392f6d 100644 --- a/Src/DeUrgenta.User.Api/Controller/UserController.cs +++ b/Src/DeUrgenta.User.Api/Controller/UserController.cs @@ -247,14 +247,14 @@ public async Task DeleteLocationAsync([FromRoute] Guid locationId) /// /// [HttpGet("group-invites")] - [SwaggerResponse(StatusCodes.Status200OK, "Get group invites for current user", typeof(IImmutableList))] + [SwaggerResponse(StatusCodes.Status200OK, "Get group invites for current user", typeof(IImmutableList))] [SwaggerResponse(StatusCodes.Status400BadRequest, "A business rule was violated", typeof(ProblemDetails))] [SwaggerResponse(StatusCodes.Status500InternalServerError, "Something bad happened", typeof(ProblemDetails))] [SwaggerResponseExample(StatusCodes.Status200OK, typeof(GetGroupInvitesResponseExample))] [SwaggerResponseExample(StatusCodes.Status400BadRequest, typeof(BusinessRuleViolationResponseExample))] [SwaggerResponseExample(StatusCodes.Status500InternalServerError, typeof(ApplicationErrorResponseExample))] - public async Task> GetGroupInvitesAsync() + public async Task> GetGroupInvitesAsync() { var sub = User.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; var query = new GetGroupInvites(sub); diff --git a/Src/DeUrgenta.User.Api/Models/GropInviteModel.cs b/Src/DeUrgenta.User.Api/Models/GroupInviteModel.cs similarity index 89% rename from Src/DeUrgenta.User.Api/Models/GropInviteModel.cs rename to Src/DeUrgenta.User.Api/Models/GroupInviteModel.cs index 7dee7a6..0b5326c 100644 --- a/Src/DeUrgenta.User.Api/Models/GropInviteModel.cs +++ b/Src/DeUrgenta.User.Api/Models/GroupInviteModel.cs @@ -2,7 +2,7 @@ namespace DeUrgenta.User.Api.Models { - public sealed record GropInviteModel + public sealed record GroupInviteModel { public Guid InviteId { get; init; } public Guid GroupId { get; init; } diff --git a/Src/DeUrgenta.User.Api/Queries/GetGroupInvites.cs b/Src/DeUrgenta.User.Api/Queries/GetGroupInvites.cs index ae296df..75e4533 100644 --- a/Src/DeUrgenta.User.Api/Queries/GetGroupInvites.cs +++ b/Src/DeUrgenta.User.Api/Queries/GetGroupInvites.cs @@ -5,7 +5,7 @@ namespace DeUrgenta.User.Api.Queries { - public class GetGroupInvites : IRequest>> + public class GetGroupInvites : IRequest>> { public string UserSub { get; } diff --git a/Src/DeUrgenta.User.Api/QueryHandlers/GetGroupInvitesHandler.cs b/Src/DeUrgenta.User.Api/QueryHandlers/GetGroupInvitesHandler.cs index b8378ab..e77900b 100644 --- a/Src/DeUrgenta.User.Api/QueryHandlers/GetGroupInvitesHandler.cs +++ b/Src/DeUrgenta.User.Api/QueryHandlers/GetGroupInvitesHandler.cs @@ -12,7 +12,7 @@ namespace DeUrgenta.User.Api.QueryHandlers { - public class GetGroupInvitesHandler : IRequestHandler>> + public class GetGroupInvitesHandler : IRequestHandler>> { private readonly IValidateRequest _validator; private readonly DeUrgentaContext _context; @@ -23,18 +23,18 @@ public GetGroupInvitesHandler(IValidateRequest validator, DeUrg _context = context; } - public async Task>> Handle(GetGroupInvites request, CancellationToken cancellationToken) + public async Task>> Handle(GetGroupInvites request, CancellationToken cancellationToken) { var isValid = await _validator.IsValidAsync(request); if (!isValid) { - return Result.Failure>("Validation failed"); + return Result.Failure>("Validation failed"); } var groupInvites = await _context .GroupInvites .Where(gi => gi.InvitationReceiver.Sub == request.UserSub) - .Select(x => new GropInviteModel + .Select(x => new GroupInviteModel { SenderId = x.InvitationSenderId, GroupId = x.GroupId, diff --git a/Src/DeUrgenta.User.Api/Swagger/GetGroupInvitesResponseExample.cs b/Src/DeUrgenta.User.Api/Swagger/GetGroupInvitesResponseExample.cs index d0e26d9..a094c20 100644 --- a/Src/DeUrgenta.User.Api/Swagger/GetGroupInvitesResponseExample.cs +++ b/Src/DeUrgenta.User.Api/Swagger/GetGroupInvitesResponseExample.cs @@ -6,11 +6,11 @@ namespace DeUrgenta.User.Api.Swagger { - public class GetGroupInvitesResponseExample : IExamplesProvider> + public class GetGroupInvitesResponseExample : IExamplesProvider> { - public IImmutableList GetExamples() + public IImmutableList GetExamples() { - return new List + return new List { new() { diff --git a/Src/Tests/DeUrgenta.Group.Api.Tests/CommandsHandlers/AddGroupHandlerShould.cs b/Src/Tests/DeUrgenta.Group.Api.Tests/CommandsHandlers/AddGroupHandlerShould.cs index 770bd41..2ad8b9b 100644 --- a/Src/Tests/DeUrgenta.Group.Api.Tests/CommandsHandlers/AddGroupHandlerShould.cs +++ b/Src/Tests/DeUrgenta.Group.Api.Tests/CommandsHandlers/AddGroupHandlerShould.cs @@ -16,7 +16,7 @@ namespace DeUrgenta.Group.Api.Tests.CommandsHandlers public class AddGroupHandlerShould { private readonly DeUrgentaContext _dbContext; - + public AddGroupHandlerShould(DatabaseFixture fixture) { _dbContext = fixture.Context; diff --git a/Src/Tests/DeUrgenta.Group.Api.Tests/CommandsHandlers/InviteToGroupHandlerShould.cs b/Src/Tests/DeUrgenta.Group.Api.Tests/CommandsHandlers/InviteToGroupHandlerShould.cs index 76e52b8..5d9ecb9 100644 --- a/Src/Tests/DeUrgenta.Group.Api.Tests/CommandsHandlers/InviteToGroupHandlerShould.cs +++ b/Src/Tests/DeUrgenta.Group.Api.Tests/CommandsHandlers/InviteToGroupHandlerShould.cs @@ -35,7 +35,8 @@ public async Task Return_failed_result_when_validation_fails() var sut = new InviteToGroupHandler(validator, _dbContext); // Act - var result = await sut.Handle(new InviteToGroup("a-sub", Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); + var result = await sut.Handle(new InviteToGroup("a-sub", Guid.NewGuid(), Guid.NewGuid()), + CancellationToken.None); // Assert result.IsFailure.ShouldBeTrue(); diff --git a/Src/Tests/DeUrgenta.Group.Api.Tests/Validators/AddGroupValidatorShould.cs b/Src/Tests/DeUrgenta.Group.Api.Tests/Validators/AddGroupValidatorShould.cs index da0a837..f211aa3 100644 --- a/Src/Tests/DeUrgenta.Group.Api.Tests/Validators/AddGroupValidatorShould.cs +++ b/Src/Tests/DeUrgenta.Group.Api.Tests/Validators/AddGroupValidatorShould.cs @@ -1,8 +1,10 @@ using System; using System.Threading.Tasks; using DeUrgenta.Domain; +using DeUrgenta.Domain.Entities; using DeUrgenta.Group.Api.Commands; using DeUrgenta.Group.Api.Models; +using DeUrgenta.Group.Api.Options; using DeUrgenta.Group.Api.Validators; using DeUrgenta.Tests.Helpers; using DeUrgenta.Tests.Helpers.Builders; @@ -15,10 +17,12 @@ namespace DeUrgenta.Group.Api.Tests.Validators public class AddGroupValidatorShould { private readonly DeUrgentaContext _dbContext; + private readonly GroupsConfig _groupsConfig; public AddGroupValidatorShould(DatabaseFixture fixture) { _dbContext = fixture.Context; + _groupsConfig = new GroupsConfig {MaxCreatedGroupsPerUser = 5}; } [Theory] @@ -28,7 +32,7 @@ public AddGroupValidatorShould(DatabaseFixture fixture) public async Task Invalidate_request_when_no_user_found_by_sub(string sub) { // Arrange - var sut = new AddGroupValidator(_dbContext); + var sut = new AddGroupValidator(_dbContext, _groupsConfig); // Act var isValid = await sut.IsValidAsync(new AddGroup(sub, new GroupRequest())); @@ -40,7 +44,7 @@ public async Task Invalidate_request_when_no_user_found_by_sub(string sub) [Fact] public async Task Validate_when_user_was_found_by_sub() { - var sut = new AddGroupValidator(_dbContext); + var sut = new AddGroupValidator(_dbContext, _groupsConfig); // Arrange var userSub = Guid.NewGuid().ToString(); @@ -55,5 +59,33 @@ public async Task Validate_when_user_was_found_by_sub() // Assert isValid.ShouldBeTrue(); } + + [Fact] + public async Task Invalidate_when_user_exceeds_group_creation_limit() + { + // Arrange + var sut = new AddGroupValidator(_dbContext, _groupsConfig); + + // Seed user + var userSub = Guid.NewGuid().ToString(); + var user = new UserBuilder().WithSub(userSub).Build(); + await _dbContext.SaveChangesAsync(); + + // Seed groups + for (int i = 0; i < 5; i++) + { + await _dbContext.Groups.AddAsync(new Domain.Entities.Group + { + Name = i.ToString(), Admin = user + }); + } + await _dbContext.SaveChangesAsync(); + + // Act + var result = await sut.IsValidAsync(new AddGroup(user.Sub, new GroupRequest {Name = "TestGroup"})); + + // Assert + result.ShouldBeFalse(); + } } } \ No newline at end of file diff --git a/Src/Tests/DeUrgenta.Group.Api.Tests/Validators/InviteToGroupValidatorShould.cs b/Src/Tests/DeUrgenta.Group.Api.Tests/Validators/InviteToGroupValidatorShould.cs index 82f077d..19a1acc 100644 --- a/Src/Tests/DeUrgenta.Group.Api.Tests/Validators/InviteToGroupValidatorShould.cs +++ b/Src/Tests/DeUrgenta.Group.Api.Tests/Validators/InviteToGroupValidatorShould.cs @@ -1,8 +1,10 @@ using System; +using System.Linq; using System.Threading.Tasks; using DeUrgenta.Domain; using DeUrgenta.Domain.Entities; using DeUrgenta.Group.Api.Commands; +using DeUrgenta.Group.Api.Options; using DeUrgenta.Group.Api.Validators; using DeUrgenta.Tests.Helpers; using DeUrgenta.Tests.Helpers.Builders; @@ -15,10 +17,12 @@ namespace DeUrgenta.Group.Api.Tests.Validators public class InviteToGroupValidatorShould { private readonly DeUrgentaContext _dbContext; + private readonly GroupsConfig _groupsConfig; public InviteToGroupValidatorShould(DatabaseFixture fixture) { _dbContext = fixture.Context; + _groupsConfig = new GroupsConfig {MaxJoinedGroupsPerUser = 5}; } [Theory] @@ -28,7 +32,7 @@ public InviteToGroupValidatorShould(DatabaseFixture fixture) public async Task Invalidate_request_when_no_user_found_by_sub(string sub) { // Arrange - var sut = new InviteToGroupValidator(_dbContext); + var sut = new InviteToGroupValidator(_dbContext, _groupsConfig); // Act var isValid = await sut.IsValidAsync(new InviteToGroup(sub, Guid.NewGuid(), Guid.NewGuid())); @@ -41,7 +45,7 @@ public async Task Invalidate_request_when_no_user_found_by_sub(string sub) public async Task Invalidate_request_when_user_adds_himself_to_group() { // Arrange - var sut = new InviteToGroupValidator(_dbContext); + var sut = new InviteToGroupValidator(_dbContext, _groupsConfig); var userSub = Guid.NewGuid().ToString(); var user = new UserBuilder().WithSub(userSub).Build(); @@ -70,7 +74,7 @@ public async Task Invalidate_request_when_user_adds_himself_to_group() public async Task Invalidate_when_group_not_found() { // Arrange - var sut = new InviteToGroupValidator(_dbContext); + var sut = new InviteToGroupValidator(_dbContext, _groupsConfig); var userSub = Guid.NewGuid().ToString(); var user = new UserBuilder().WithSub(userSub).Build(); @@ -89,7 +93,7 @@ public async Task Invalidate_when_group_not_found() public async Task Invalidate_when_invited_user_not_found() { // Arrange - var sut = new InviteToGroupValidator(_dbContext); + var sut = new InviteToGroupValidator(_dbContext, _groupsConfig); var userSub = Guid.NewGuid().ToString(); var admin = new UserBuilder().WithSub(userSub).Build(); @@ -119,7 +123,7 @@ public async Task Invalidate_when_invited_user_not_found() public async Task Invalidate_when_user_is_already_invited() { // Arrange - var sut = new InviteToGroupValidator(_dbContext); + var sut = new InviteToGroupValidator(_dbContext, _groupsConfig); var userSub = Guid.NewGuid().ToString(); var nonGroupUserSub = Guid.NewGuid().ToString(); @@ -160,7 +164,7 @@ public async Task Invalidate_when_user_is_already_invited() public async Task Validate_when_user_is_admin_of_group_and_invited_existing_user() { // Arrange - var sut = new InviteToGroupValidator(_dbContext); + var sut = new InviteToGroupValidator(_dbContext, _groupsConfig); var userSub = Guid.NewGuid().ToString(); var nonGroupUserSub = Guid.NewGuid().ToString(); @@ -194,7 +198,7 @@ public async Task Validate_when_user_is_admin_of_group_and_invited_existing_user public async Task Validate_when_user_is_part_of_group_and_invited_existing_user() { // Arrange - var sut = new InviteToGroupValidator(_dbContext); + var sut = new InviteToGroupValidator(_dbContext, _groupsConfig); var userSub = Guid.NewGuid().ToString(); var nonGroupUserSub = Guid.NewGuid().ToString(); @@ -222,5 +226,57 @@ public async Task Validate_when_user_is_part_of_group_and_invited_existing_user( // Assert isValid.ShouldBeTrue(); } + + [Fact] + public async Task Invalidate_when_user_exceeds_group_membership_limit() + { + // Arrange + var sut = new InviteToGroupValidator(_dbContext, _groupsConfig); + + // Seed user + var adminSub = Guid.NewGuid().ToString(); + var admin = new UserBuilder().WithSub(adminSub).Build(); + var userSub = Guid.NewGuid().ToString(); + var user = new UserBuilder().WithSub(userSub).Build(); + await _dbContext.SaveChangesAsync(); + + // Seed groups + for (int i = 0; i < 5; i++) + { + await _dbContext.Groups.AddAsync(new Domain.Entities.Group + { + Name = i.ToString(), Admin = admin, AdminId = admin.Id + }); + } + await _dbContext.SaveChangesAsync(); + + // Seed users to groups + _dbContext.Groups.ToList().ForEach(group => + { + _dbContext.UsersToGroups.AddAsync(new UserToGroup + { + Group = group, GroupId = group.Id, User = user, UserId = user.Id + }); + }); + await _dbContext.SaveChangesAsync(); + + // Seed last group + var mainGroup = _dbContext.Groups.AddAsync( + new Domain.Entities.Group {Name = "TestGroup", Admin = admin, AdminId = admin.Id}).Result.Entity; + await _dbContext.SaveChangesAsync(); + + // Seed user to group + await _dbContext.UsersToGroups.AddAsync(new UserToGroup + { + Group = mainGroup, GroupId = mainGroup.Id, User = admin, UserId = admin.Id + }); + await _dbContext.SaveChangesAsync(); + + // Act + var result = await sut.IsValidAsync(new InviteToGroup("a-sub", mainGroup.Id, user.Id)); + + // Assert + result.ShouldBeFalse(); + } } } \ No newline at end of file