diff --git a/Govor.API.Tests/IntegrationTests/EF/Repositories/AdminsRepositoryTests.cs b/Govor.API.Tests/IntegrationTests/EF/Repositories/AdminsRepositoryTests.cs index ddb9337..42a85d6 100644 --- a/Govor.API.Tests/IntegrationTests/EF/Repositories/AdminsRepositoryTests.cs +++ b/Govor.API.Tests/IntegrationTests/EF/Repositories/AdminsRepositoryTests.cs @@ -91,6 +91,7 @@ public class AdminsRepositoryTests [Test] public async Task Given_InvalidUserId_When_GetByIdAsync_Should_Throw_NotFoundByKeyException() { + // Arrange await using var context = new GovorDbContext(_options); var repository = new AdminsRepository(context, _validator); diff --git a/Govor.API.Tests/IntegrationTests/EF/Repositories/InvitesRepositoryTests.cs b/Govor.API.Tests/IntegrationTests/EF/Repositories/InvitesRepositoryTests.cs new file mode 100644 index 0000000..59b18ee --- /dev/null +++ b/Govor.API.Tests/IntegrationTests/EF/Repositories/InvitesRepositoryTests.cs @@ -0,0 +1,244 @@ +using AutoFixture; +using Govor.Core.Infrastructure.Validators; +using Govor.Core.Models; +using Govor.Data; +using Govor.Data.Repositories; +using Govor.Data.Repositories.Exceptions; +using Microsoft.EntityFrameworkCore; + +namespace Govor.API.Tests.IntegrationTests.EF.Repositories; + +[TestFixture] +public class InvitesRepositoryTests +{ + private Fixture _fixture; + private DbContextOptions _options; + private IObjectValidator _validator = new InvitationValidator(); + private int _testIteration = 0; + + [SetUp] + public void Setup() + { + _fixture = new Fixture(); + + _fixture.Behaviors + .OfType() + .ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: $"DbGovor_{nameof(InvitesRepositoryTests)}_{_testIteration}") + .Options; + _testIteration += 1; + } + + [Test] + public async Task Given_NotEmptyDbSet_When_GetAllAsync_Then_Invites_Are_Returned() + { + // Arrange + var random = new Random(); + var invitations = _fixture.CreateMany(random.Next(3, 10)).ToList(); + + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + context.Invitations.AddRange(invitations); + await context.SaveChangesAsync(); + + // Act + var result = await repository.GetAllAsync(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Count(), Is.EqualTo(invitations.Count())); + Assert.That(result, Is.EquivalentTo(invitations)); + } + + [Test] + public async Task Given_EmptyDbSet_When_GetAllAsync_Should_Throw_NotFoundException() + { + // Arrange + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + // Act & Assert + Assert.ThrowsAsync(async () => await repository.GetAllAsync()); + } + + [Test] + public async Task Given_ValidInvitesId_When_FindByIdAsync_Then_Invites_Are_Returned() + { + // Arrange + var invitation = _fixture.Create(); + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + context.Invitations.Add(invitation); + await context.SaveChangesAsync(); + + // Act + var result = await repository.FindByIdAsync(invitation.Id); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Id, Is.EqualTo(invitation.Id)); + } + + [Test] + public async Task Given_InvalidInvitesId_When_FindByIdAsync_Should_Throw_NotFoundByKeyException() + { + // Arrange + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + // Act & Assert + Assert.ThrowsAsync>(async () => await repository.FindByIdAsync(Guid.Empty)); + } + + [Test] + public async Task Given_ValidInvitesCode_When_FindByCodeAsync_Then_Invites_Are_Returned() + { + // Arrange + var invitation = _fixture.Create(); + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + context.Invitations.Add(invitation); + await context.SaveChangesAsync(); + + // Act + var result = await repository.FindByCodeAsync(invitation.Code); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Id, Is.EqualTo(invitation.Id)); + } + + [Test] + public async Task Given_InvalidCode_When_FindByCodeAsync_Should_Throw_NotFoundByKeyException() + { + // Arrange + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + // Act & Assert + Assert.ThrowsAsync>(async () => await repository.FindByCodeAsync(_fixture.Create())); + } + + [Test] + public async Task Given_ValidAdminInvites_When_FindAdminsInvitesAsync_Then_Invites_Are_Returned() + { + // Arrange + var random = new Random(); + var invitations = _fixture.CreateMany(random.Next(3, 10)).ToList(); + + foreach (var inv in invitations) + { + inv.IsAdmin = true; + } + + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + context.Invitations.AddRange(invitations); + await context.SaveChangesAsync(); + + // Act + var result = await repository.FindAdminsInvitesAsync(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Count(), Is.EqualTo(invitations.Count())); + Assert.That(result, Is.EquivalentTo(invitations)); + } + + [Test] + public async Task Given_InvalidAdminsInvites_When_FindAdminsInvitesAsync_Should_Throw_NotFoundByKeyException() + { + // Arrange + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + // Act & Assert + Assert.ThrowsAsync>(async () => await repository.FindAdminsInvitesAsync()); + } + + [Test] + public async Task Given_ValidAdmin_When_AddAsync_Then_Added() + { + // Arrange + var invitation = _fixture.Create(); + invitation.Code = string.Join("", _fixture.CreateMany(12)); + invitation.EndDate = invitation.DateCreated.AddHours(1); + + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + // Act + await repository.AddAsync(invitation); + + // Assert + Assert.That(context.Invitations.Count, Is.EqualTo(1)); + Assert.That(context.Invitations.First(), Is.EqualTo(invitation)); + } + + [Test] + public async Task Given_InvalidAdmin_When_AddAsync_Should_Throw_AdditionException() + { + // Arrange + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + // Act & Assert + Assert.ThrowsAsync(async () => await repository.AddAsync(default)); + } + + [Test] + public async Task Given_NotExistInvitesCode_When_Exist_Then_Returns_False() + { + // Arrange + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + // Act + var result = repository.Exist(Guid.NewGuid()); + + Assert.That(result, Is.False); + } + + [Test] + public async Task Given_ExistInvitesCode_When_Exist_Then_Returns_False() + { + // Arrange + var invitation = _fixture.Create(); + + invitation.Code = string.Join("", _fixture.CreateMany(12)); + invitation.EndDate = invitation.DateCreated.AddHours(1); + + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + context.Invitations.Add(invitation); + await context.SaveChangesAsync(); + + // Act + var result = repository.Exist(invitation.Id); + var result2 = repository.Exist(invitation); + + Assert.That(result, Is.True); + Assert.That(result2, Is.True); + } + + [Test] + public async Task Given_InvalidInvites_When_Exist_Then_Returns_False() + { + // Arrange + await using var context = new GovorDbContext(_options); + var repository = new InvitesRepository(context, _validator); + + // Act & Assert + Assert.Throws>(() => repository.Exist(default(Invitation))); + } +} \ No newline at end of file diff --git a/Govor.API.Tests/UnitTests/Infrastructure/Validators/InvitationValidatorTests.cs b/Govor.API.Tests/UnitTests/Infrastructure/Validators/InvitationValidatorTests.cs index 7a2a9b4..68e71e5 100644 --- a/Govor.API.Tests/UnitTests/Infrastructure/Validators/InvitationValidatorTests.cs +++ b/Govor.API.Tests/UnitTests/Infrastructure/Validators/InvitationValidatorTests.cs @@ -89,4 +89,16 @@ public class InvitationValidatorTests Assert.Throws>( () => _messageValidator.Validate(invitation)); Assert.That(_messageValidator.TryValidate(invitation), Is.False); } + + [Test] + public void Given_InvalidCode_When_Exist_Then_Returns_False() + { + // Arrange + var invitation = _fixture.Create(); + invitation.Code = string.Empty; + + // Act & Assert + Assert.Throws>( () => _messageValidator.Validate(invitation)); + Assert.That(_messageValidator.TryValidate(invitation), Is.False); + } } \ No newline at end of file diff --git a/Govor.API/Services/Authentication/AuthService.cs b/Govor.API/Services/Authentication/AuthService.cs index 2c662dc..9ea80a5 100644 --- a/Govor.API/Services/Authentication/AuthService.cs +++ b/Govor.API/Services/Authentication/AuthService.cs @@ -38,7 +38,7 @@ public class AuthService : IAccountService throw new UserAlreadyExistException(name); // 2. Проверка валидности инвайта - var invite = await _invitesRepository.GetByCodeAsync(inviteCode); + var invite = await _invitesRepository.FindByCodeAsync(inviteCode); // 3. Генерация пароля var passwordHash = _passwordHasher.Hash(password); diff --git a/Govor.API/Services/Authentication/JwtService.cs b/Govor.API/Services/Authentication/JwtService.cs index f827790..9a62426 100644 --- a/Govor.API/Services/Authentication/JwtService.cs +++ b/Govor.API/Services/Authentication/JwtService.cs @@ -22,7 +22,7 @@ public class JwtService : IJwtService public string GenerateJwtToken(User user) { - var invite = _invitesRepository.GetByIdAsync(user.InviteId).Result; + var invite = _invitesRepository.FindByIdAsync(user.InviteId).Result; var claims = new[] { diff --git a/Govor.Core/DTOs/RegistrationDto.cs b/Govor.Core/DTOs/RegistrationDto.cs index 4216c80..dc32a91 100644 --- a/Govor.Core/DTOs/RegistrationDto.cs +++ b/Govor.Core/DTOs/RegistrationDto.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Govor.Core.Infrastructure.Validators; +using Govor.Core.Models; namespace Govor.Core.DTOs; @@ -13,6 +14,6 @@ public record RegistrationDto [Required] [MinLength(8)] public string Password { get; init; } - [MinLength(8)] + [MinLength(InvitationValidator.MIN_INVITATION_LENGTH)] public string InviteLink { get; init; } } \ No newline at end of file diff --git a/Govor.Core/Infrastructure/Validators/InvitationValidator.cs b/Govor.Core/Infrastructure/Validators/InvitationValidator.cs index 813da99..069fe1d 100644 --- a/Govor.Core/Infrastructure/Validators/InvitationValidator.cs +++ b/Govor.Core/Infrastructure/Validators/InvitationValidator.cs @@ -4,6 +4,7 @@ namespace Govor.Core.Infrastructure.Validators; public class InvitationValidator : IObjectValidator { + public const int MIN_INVITATION_LENGTH = 10; public void Validate(Invitation inv) { try @@ -18,6 +19,8 @@ public class InvitationValidator : IObjectValidator throw new ArgumentException("EndDate cannot be less than StartDate", nameof(inv.EndDate)); if(inv.MaxParticipants <= 0) throw new ArgumentException("MaxParticipants cannot be less than 0", nameof(inv.MaxParticipants)); + if(inv.Code == string.Empty || inv.Code.Length < MIN_INVITATION_LENGTH) + throw new ArgumentException($"Code cannot be empty or less then {MIN_INVITATION_LENGTH}", nameof(inv.Code)); } catch (Exception ex) { diff --git a/Govor.Core/Models/Invitation.cs b/Govor.Core/Models/Invitation.cs index 5e3bf01..a53c29e 100644 --- a/Govor.Core/Models/Invitation.cs +++ b/Govor.Core/Models/Invitation.cs @@ -4,9 +4,22 @@ public class Invitation { public Guid Id { get; set; } public bool IsAdmin { get; set; } + public string Code { get; set; } public string Description { get; set; } public DateTime DateCreated { get; set; } public DateTime EndDate { get; set; } public int MaxParticipants { get; set; } public List Users { get; set; } = new List(); + + public override bool Equals(object? obj) + { + var invitation = obj as Invitation ?? throw new InvalidCastException(); + + return Id == invitation.Id && + IsAdmin == invitation.IsAdmin && + Description == invitation.Description && + DateCreated == invitation.DateCreated && + EndDate == invitation.EndDate && + MaxParticipants == invitation.MaxParticipants; + } } \ No newline at end of file diff --git a/Govor.Core/Repositories/Admins/IAdminsWriter.cs b/Govor.Core/Repositories/Admins/IAdminsWriter.cs index 2355013..d34d582 100644 --- a/Govor.Core/Repositories/Admins/IAdminsWriter.cs +++ b/Govor.Core/Repositories/Admins/IAdminsWriter.cs @@ -6,5 +6,5 @@ public interface IAdminsWriter { Task AddAsync(Admin admin); Task UpdateAsync(Admin admin); - Task DeleteAsync(Guid admin); + Task RemoveAsync(Guid admin); } \ No newline at end of file diff --git a/Govor.Core/Repositories/Invaites/IInvitesReader.cs b/Govor.Core/Repositories/Invaites/IInvitesReader.cs index 94e9493..85cb34d 100644 --- a/Govor.Core/Repositories/Invaites/IInvitesReader.cs +++ b/Govor.Core/Repositories/Invaites/IInvitesReader.cs @@ -5,7 +5,7 @@ namespace Govor.Core.Repositories.Invaites; public interface IInvitesReader { Task> GetAllAsync(); - Task GetByIdAsync(Guid id); - Task GetByCodeAsync(string code); - Task> GetAdminsInvitesAsync(); + Task FindByIdAsync(Guid id); + Task FindByCodeAsync(string code); + Task> FindAdminsInvitesAsync(); } \ No newline at end of file diff --git a/Govor.Data/Repositories/AdminsRepository.cs b/Govor.Data/Repositories/AdminsRepository.cs index 0482504..0e5783b 100644 --- a/Govor.Data/Repositories/AdminsRepository.cs +++ b/Govor.Data/Repositories/AdminsRepository.cs @@ -72,7 +72,7 @@ public class AdminsRepository(GovorDbContext context, IObjectValidator va } } - public async Task DeleteAsync(Guid admin) + public async Task RemoveAsync(Guid admin) { try { diff --git a/Govor.Data/Repositories/InvitesRepository.cs b/Govor.Data/Repositories/InvitesRepository.cs index 7b18448..374e697 100644 --- a/Govor.Data/Repositories/InvitesRepository.cs +++ b/Govor.Data/Repositories/InvitesRepository.cs @@ -1,6 +1,9 @@ +using Govor.Core.Infrastructure.Extensions; using Govor.Core.Infrastructure.Validators; using Govor.Core.Models; using Govor.Core.Repositories.Invaites; +using Govor.Data.Repositories.Exceptions; +using Microsoft.EntityFrameworkCore; namespace Govor.Data.Repositories; @@ -17,46 +20,124 @@ public class InvitesRepository : IInvitesRepository public async Task> GetAllAsync() { - throw new NotImplementedException(); + return await _context.Invitations + .AsNoTracking() + .Include(i => i.Users) + .ToListOrThrowIfEmpty(new NotFoundException("Database is empty")); } - public Task GetByIdAsync(Guid id) + public async Task FindByIdAsync(Guid id) { - throw new NotImplementedException(); + return await _context.Invitations + .AsNoTracking() + .Include(i => i.Users) + .FirstOrDefaultAsync(i => i.Id == id) + ?? throw new NotFoundByKeyException(id, "Invitation with given id does not exist"); } - public Task GetByCodeAsync(string code) + public async Task FindByCodeAsync(string code) { - throw new NotImplementedException(); + return await _context.Invitations + .AsNoTracking() + .Include(i => i.Users) + .FirstOrDefaultAsync(i => i.Code == code) + ?? throw new NotFoundByKeyException(code, "Invitation with given code does not exist"); } - public Task> GetAdminsInvitesAsync() + public async Task> FindAdminsInvitesAsync() { - throw new NotImplementedException(); + return await _context.Invitations + .AsNoTracking() + .Include(i => i.Users) + .Where(i => i.IsAdmin) + .ToListOrThrowIfEmpty(new NotFoundByKeyException(true, "Admins invites do not exist")); } - public Task AddAsync(Invitation invitation) + public async Task AddAsync(Invitation invitation) { - throw new NotImplementedException(); + try + { + _validator.Validate(invitation); + + _context.Invitations.Add(invitation); + await _context.SaveChangesAsync(); + } + catch (InvalidObjectException ex) + { + throw new AdditionException("Invitation with given data invalid", ex); + } + catch (Exception ex) + { + throw new AdditionException("Cannot add invitation", ex); + }; } - public Task UpdateAsync(Invitation invitation) + public async Task UpdateAsync(Invitation invitation) { - throw new NotImplementedException(); + try + { + _validator.Validate(invitation); + + var rowsAffected = await _context.Invitations + .Where(a => a.Id == invitation.Id) + .ExecuteUpdateAsync(u => u + .SetProperty(i => i.IsAdmin, invitation.IsAdmin) + .SetProperty(i => i.Code, invitation.Code) + .SetProperty(i => i.Description, invitation.Description) + .SetProperty(i => i.DateCreated, invitation.DateCreated) + .SetProperty(i => i.EndDate, invitation.EndDate) + .SetProperty(i => i.MaxParticipants, invitation.MaxParticipants) + .SetProperty(i => i.Users, invitation.Users) + ); + + if (rowsAffected == 0) + throw new NotFoundByKeyException(invitation.Id, "Invitation with given data invalid"); + } + catch (NotFoundByKeyException ex) + { + throw new UpdateException($"Not found invitation by given id {invitation.Id}", ex); + } + catch (Exception ex) + { + throw new UpdateException($"Error when updating the invitation {invitation.Id}", ex); + } } - public Task RemoveAsync(Invitation invitation) + public async Task RemoveAsync(Invitation invitation) { - throw new NotImplementedException(); + try + { + var result = await FindByIdAsync(invitation.Id); + + _context.Invitations.Remove(result); + await _context.SaveChangesAsync(); + } + catch (NotFoundByKeyException ex) + { + throw new RemoveException($"Not found invitation by given id {invitation.Id}", ex); + } + catch (Exception ex) + { + throw new RemoveException("Error when removing the invitation", ex); + } } public bool Exist(Invitation invitation) { - throw new NotImplementedException(); + _validator.Validate(invitation); + return _context.Invitations.Any(i => i.Id == invitation.Id && + i.Code == invitation.Code && + i.IsAdmin == invitation.IsAdmin && + i.DateCreated == invitation.DateCreated && + i.EndDate == invitation.EndDate && + i.Description == invitation.Description && + i.MaxParticipants == invitation.MaxParticipants + + ); } public bool Exist(Guid guid) { - throw new NotImplementedException(); + return _context.Invitations.Any(i => i.Id == guid); } } \ No newline at end of file