From 2cb6a93060dd38e58d354aa1f67899dc1d5129df Mon Sep 17 00:00:00 2001 From: Artemy <109195690+stalcker2288969@users.noreply.github.com> Date: Mon, 7 Jul 2025 13:55:29 +0700 Subject: [PATCH] Add private chat support and group model enhancements Introduces repositories, validators, and integration tests for private chats. Refactors group-related models to support invitations and memberships, updates group repository interfaces and implementations, and enhances message service logic to handle private chat creation and retrieval. Also registers new services and validators in DI, and updates related tests. --- .../PrivateChatsRepositoryTests.cs | 7 + .../IntegrationTests/Hubs/ChatsHubTests.cs | 35 ++++ .../Validators/PrivateChatValidatorTests.cs | 7 + .../UnitTests/Services/MessageServiceTests.cs | 15 +- .../ConfigurationProgramExtensions.cs | 3 + .../Interfaces/Messages/IMessageService.cs | 2 +- Govor.Application/Services/MessageService.cs | 46 +++++- .../Validators/PrivateChatValidator.cs | 38 +++++ Govor.Core/Models/ChatGroup.cs | 6 +- Govor.Core/Models/GroupInvitation.cs | 15 ++ Govor.Core/Models/GroupMembership.cs | 1 + .../Groups/IGroupMessagesReader.cs | 8 + .../Repositories/Groups/IGroupsReader.cs | 14 +- .../Repositories/Groups/IGroupsWriter.cs | 8 +- .../PrivateChats/IPrivateChatsExist.cs | 7 + .../PrivateChats/IPrivateChatsReader.cs | 10 ++ .../PrivateChats/IPrivateChatsRepository.cs | 6 + .../PrivateChats/IPrivateChatsWriter.cs | 10 ++ Govor.Data/GovorDbContext.cs | 1 + Govor.Data/Repositories/GroupRepository.cs | 154 +++++++++++++++--- .../Repositories/PrivateChatsRepository.cs | 115 +++++++++++++ 21 files changed, 454 insertions(+), 54 deletions(-) create mode 100644 Govor.API.Tests/IntegrationTests/EF/Repositories/PrivateChatsRepositoryTests.cs create mode 100644 Govor.API.Tests/IntegrationTests/Hubs/ChatsHubTests.cs create mode 100644 Govor.API.Tests/UnitTests/Infrastructure/Validators/PrivateChatValidatorTests.cs create mode 100644 Govor.Core/Infrastructure/Validators/PrivateChatValidator.cs create mode 100644 Govor.Core/Models/GroupInvitation.cs create mode 100644 Govor.Core/Repositories/Groups/IGroupMessagesReader.cs create mode 100644 Govor.Core/Repositories/PrivateChats/IPrivateChatsExist.cs create mode 100644 Govor.Core/Repositories/PrivateChats/IPrivateChatsReader.cs create mode 100644 Govor.Core/Repositories/PrivateChats/IPrivateChatsRepository.cs create mode 100644 Govor.Core/Repositories/PrivateChats/IPrivateChatsWriter.cs create mode 100644 Govor.Data/Repositories/PrivateChatsRepository.cs diff --git a/Govor.API.Tests/IntegrationTests/EF/Repositories/PrivateChatsRepositoryTests.cs b/Govor.API.Tests/IntegrationTests/EF/Repositories/PrivateChatsRepositoryTests.cs new file mode 100644 index 0000000..41467ef --- /dev/null +++ b/Govor.API.Tests/IntegrationTests/EF/Repositories/PrivateChatsRepositoryTests.cs @@ -0,0 +1,7 @@ +namespace Govor.API.Tests.IntegrationTests.EF.Repositories; + +[TestFixture] +public class PrivateChatsRepositoryTests +{ + +} \ No newline at end of file diff --git a/Govor.API.Tests/IntegrationTests/Hubs/ChatsHubTests.cs b/Govor.API.Tests/IntegrationTests/Hubs/ChatsHubTests.cs new file mode 100644 index 0000000..4f2ebe1 --- /dev/null +++ b/Govor.API.Tests/IntegrationTests/Hubs/ChatsHubTests.cs @@ -0,0 +1,35 @@ +using AutoFixture; +using Govor.API.Hubs; +using Govor.Application.Interfaces.Messages; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Govor.API.Tests.IntegrationTests.Hubs; + +[TestFixture] +public class ChatsHubTests +{ + private Mock> _loggerMock; + private Mock _messageServiceMock; + private Fixture _fixture; + private ChatsHub _chatsHub; + + [SetUp] + public void SetUp() + { + _fixture = new Fixture(); + _fixture.Behaviors.OfType().ToList().ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _messageServiceMock = new Mock(); + _loggerMock = new Mock>(); + + _chatsHub = new ChatsHub( + _loggerMock.Object, + _messageServiceMock.Object + ); + } + + // Test for Send action + +} \ No newline at end of file diff --git a/Govor.API.Tests/UnitTests/Infrastructure/Validators/PrivateChatValidatorTests.cs b/Govor.API.Tests/UnitTests/Infrastructure/Validators/PrivateChatValidatorTests.cs new file mode 100644 index 0000000..8abd1fd --- /dev/null +++ b/Govor.API.Tests/UnitTests/Infrastructure/Validators/PrivateChatValidatorTests.cs @@ -0,0 +1,7 @@ +namespace Govor.API.Tests.UnitTests.Infrastructure.Validators; + +[TestFixture] +public class PrivateChatValidatorTests +{ + +} \ No newline at end of file diff --git a/Govor.API.Tests/UnitTests/Services/MessageServiceTests.cs b/Govor.API.Tests/UnitTests/Services/MessageServiceTests.cs index d789064..3260d5b 100644 --- a/Govor.API.Tests/UnitTests/Services/MessageServiceTests.cs +++ b/Govor.API.Tests/UnitTests/Services/MessageServiceTests.cs @@ -5,6 +5,7 @@ using Govor.Application.Services; using Govor.Core.Models; using Govor.Core.Repositories.Groups; using Govor.Core.Repositories.Messages; +using Govor.Core.Repositories.PrivateChats; using Govor.Core.Repositories.Users; using Govor.Data.Repositories.Exceptions; using Microsoft.Extensions.Logging; @@ -19,6 +20,7 @@ public class MessageServiceTests private Mock _mockUsersRepo; private Mock _mockGroupsRepo; private Mock _mockVerifyFriendship; + private Mock _mockPrivateChats; private Mock> _mockLogger; private MessageService _messageService; @@ -29,6 +31,7 @@ public class MessageServiceTests _mockUsersRepo = new Mock(); _mockGroupsRepo = new Mock(); _mockVerifyFriendship = new Mock(); + _mockPrivateChats = new Mock(); _mockLogger = new Mock>(); _messageService = new MessageService( @@ -36,6 +39,7 @@ public class MessageServiceTests _mockUsersRepo.Object, _mockGroupsRepo.Object, _mockVerifyFriendship.Object, + _mockPrivateChats.Object, _mockLogger.Object); } @@ -59,7 +63,8 @@ public class MessageServiceTests _mockUsersRepo.Setup(r => r.ExistsByIdAsync(recipientId)).ReturnsAsync(true); _mockVerifyFriendship.Setup(v => v.VerifyAsync(senderId, recipientId)).Returns(Task.CompletedTask); _mockMessagesRepo.Setup(r => r.AddAsync(It.IsAny())).Returns(Task.CompletedTask); - + _mockPrivateChats.Setup(c => c.Exist(senderId, recipientId)).Returns(true); + _mockPrivateChats.Setup(c => c.GetByMembersAsync(senderId, recipientId)).ReturnsAsync(new PrivateChat(){Id = recipientId}); // Act var result = await _messageService.SendMessageAsync(sendMessageParams); // Assert @@ -90,7 +95,7 @@ public class MessageServiceTests new List()); _mockGroupsRepo.Setup(r => r.Exists(groupId)).Returns(true); - _mockGroupsRepo.Setup(r => r.IsUserMemberOfGroupAsync(senderId, groupId)).Returns(true); + _mockGroupsRepo.Setup(r => r.IsUserMemberOfGroupAsync(senderId, groupId)).ReturnsAsync(true); _mockMessagesRepo.Setup(r => r.AddAsync(It.IsAny())).Returns(Task.CompletedTask); // Act @@ -130,7 +135,7 @@ public class MessageServiceTests Assert.That(result.IsSuccess, Is.EqualTo(false)); Assert.That(result.Exception, Is.Not.Null); Assert.That(result.Exception,Is.TypeOf()); - Assert.That(result.MessageId, Is.EqualTo(Guid.Empty)); + Assert.That(result.Message, Is.Null); } [Test] @@ -160,9 +165,11 @@ public class MessageServiceTests Assert.That(result.IsSuccess, Is.EqualTo(false)); Assert.That(result.Exception, Is.Not.Null); Assert.That(result.Exception,Is.TypeOf()); - Assert.That(result.MessageId, Is.EqualTo(Guid.Empty)); + Assert.That(result.Message, Is.Null); } + + // Test for EditMessageAsync action [Test] public async Task EditMessageAsync_Success() diff --git a/Govor.API/Extensions/ConfigurationProgramExtensions.cs b/Govor.API/Extensions/ConfigurationProgramExtensions.cs index 44d19da..7e45361 100644 --- a/Govor.API/Extensions/ConfigurationProgramExtensions.cs +++ b/Govor.API/Extensions/ConfigurationProgramExtensions.cs @@ -17,6 +17,7 @@ using Govor.Core.Repositories.Groups; using Govor.Core.Repositories.Invaites; using Govor.Core.Repositories.MediasAttachments; using Govor.Core.Repositories.Messages; +using Govor.Core.Repositories.PrivateChats; using Govor.Core.Repositories.Users; using Govor.Data; using Govor.Data.Repositories; @@ -61,6 +62,7 @@ public static class ConfigurationProgramExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); } @@ -72,6 +74,7 @@ public static class ConfigurationProgramExtensions services.AddScoped, AdminValidator>(); services.AddScoped, InvitationValidator>(); services.AddScoped, FriendshipValidator>(); + services.AddScoped, PrivateChatValidator>(); } public static void AddGovorDbContext(this IServiceCollection services, IConfiguration configuration) diff --git a/Govor.Application/Interfaces/Messages/IMessageService.cs b/Govor.Application/Interfaces/Messages/IMessageService.cs index 5493bbf..f4c269e 100644 --- a/Govor.Application/Interfaces/Messages/IMessageService.cs +++ b/Govor.Application/Interfaces/Messages/IMessageService.cs @@ -18,7 +18,7 @@ public interface IMessageService // Define specific result types for clarity, including original message for notifications if needed public record SendMessageResult(bool IsSuccess, Exception? Exception, Message Message) - : Result(IsSuccess, Exception, Message.Id); + : Result(IsSuccess, Exception, Message?.Id ?? Guid.Empty); public record EditMessageResult(bool IsSuccess, Exception? Exception, Message? OriginalMessage) : Result(IsSuccess, Exception, OriginalMessage?.Id ?? Guid.Empty) diff --git a/Govor.Application/Services/MessageService.cs b/Govor.Application/Services/MessageService.cs index cf4469e..f894666 100644 --- a/Govor.Application/Services/MessageService.cs +++ b/Govor.Application/Services/MessageService.cs @@ -4,6 +4,7 @@ using Govor.Application.Interfaces.Messages.Parameters; using Govor.Core.Models; using Govor.Core.Repositories.Groups; using Govor.Core.Repositories.Messages; +using Govor.Core.Repositories.PrivateChats; using Govor.Core.Repositories.Users; using Microsoft.Extensions.Logging; @@ -14,6 +15,7 @@ public class MessageService : IMessageService private readonly IMessagesRepository _messagesRepository; private readonly IUsersRepository _usersRepository; // For validating user recipients private readonly IGroupsRepository _groupsRepository; // For validating group recipients and fetching members + private readonly IPrivateChatsRepository _privateChats; private readonly IVerifyFriendship _verifyFriendship; // For private messages private readonly ILogger _logger; @@ -22,17 +24,20 @@ public class MessageService : IMessageService IUsersRepository usersRepository, IGroupsRepository groupsRepository, IVerifyFriendship verifyFriendship, + IPrivateChatsRepository privateChats, ILogger logger) { _messagesRepository = messagesRepository; _usersRepository = usersRepository; _groupsRepository = groupsRepository; _verifyFriendship = verifyFriendship; + _privateChats = privateChats; _logger = logger; } public async Task SendMessageAsync(SendMessage sendParams) { + Guid recipientId; try { // Validate recipient @@ -45,6 +50,7 @@ public class MessageService : IMessageService } // Verify friendship for private messages await _verifyFriendship.VerifyAsync(sendParams.FromUserId, sendParams.RecipientId); + recipientId = await GetPrivateChatsIdAsync(sendParams.FromUserId, sendParams.RecipientId); } else if (sendParams.RecipientType == RecipientType.Group) { @@ -54,25 +60,28 @@ public class MessageService : IMessageService return new SendMessageResult(false, new KeyNotFoundException($"Recipient group {sendParams.RecipientId} not found."), default); } // TODO: Optionally, verify if sender is a member of the group - // bool isMember = await _groupsRepository.IsUserMemberOfGroupAsync(sendParams.FromUserId, sendParams.RecipientId); - // if (!isMember) - // { - // _logger.LogWarning("User {UserId} attempted to send message to group {GroupId} but is not a member", sendParams.FromUserId, sendParams.RecipientId); - // return new SendMessageResult(false, new UnauthorizedAccessException("Sender is not a member of the group."), Guid.Empty); - // } + bool isMember = await _groupsRepository.IsUserMemberOfGroupAsync(sendParams.FromUserId, sendParams.RecipientId); + if (!isMember) + { + _logger.LogWarning("User {UserId} attempted to send message to group {GroupId} but is not a member", sendParams.FromUserId, sendParams.RecipientId); + return new SendMessageResult(false, new UnauthorizedAccessException("Sender is not a member of the group."), default); + } + + recipientId = sendParams.RecipientId; } else { _logger.LogError("Invalid recipient type specified: {RecipientType}", sendParams.RecipientType); return new SendMessageResult(false, new ArgumentException("Invalid recipient type."), default); } - + + var messageId = Guid.NewGuid(); var message = new Message { Id = messageId, SenderId = sendParams.FromUserId, - RecipientId = sendParams.RecipientId, + RecipientId = recipientId, RecipientType = sendParams.RecipientType, EncryptedContent = sendParams.EncryptContent, SentAt = sendParams.SendAt, @@ -97,6 +106,24 @@ public class MessageService : IMessageService } } + private async Task GetPrivateChatsIdAsync(Guid userA, Guid userB) + { + Guid recipientId; + // Makes new PrivateChat if not exist + if (!_privateChats.Exist(userA, userB)) + { + recipientId = Guid.NewGuid(); + await _privateChats.AddAsync(new PrivateChat() + { Id = recipientId, UserAId = userA, UserBId = userB }); + } + else + { + recipientId = (await _privateChats.GetByMembersAsync(userA, userB)).Id; + } + + return recipientId; + } + public async Task EditMessageAsync(EditMessage editParams) { try @@ -109,7 +136,8 @@ public class MessageService : IMessageService return new EditMessageResult(false, new UnauthorizedAccessException("User is not authorized to edit this message."), null); } - // TODO: Add a time limit for editing messages? e.g., if (message.SentAt < DateTime.UtcNow.AddMinutes(-15)) throw new Exception("Edit time limit exceeded"); + /*if (message.SentAt < DateTime.UtcNow.AddMinutes(-15)) + throw new Exception("Edit time limit exceeded");*/ var originalMessageForNotification = new Message { diff --git a/Govor.Core/Infrastructure/Validators/PrivateChatValidator.cs b/Govor.Core/Infrastructure/Validators/PrivateChatValidator.cs new file mode 100644 index 0000000..184d442 --- /dev/null +++ b/Govor.Core/Infrastructure/Validators/PrivateChatValidator.cs @@ -0,0 +1,38 @@ +using Govor.Core.Models; + +namespace Govor.Core.Infrastructure.Validators; + +public class PrivateChatValidator : IObjectValidator +{ + public void Validate(PrivateChat chat) + { + try + { + if(chat is null) + throw new ArgumentNullException(nameof(chat)); + if(chat.Id == Guid.Empty) + throw new ArgumentException("Id cannot be empty", nameof(chat)); + if(chat.UserAId == Guid.Empty) + throw new ArgumentException("UserAId cannot be empty", nameof(chat.UserAId)); + if(chat.UserBId == Guid.Empty) + throw new ArgumentException("UserBId cannot be empty", nameof(chat.UserBId)); + } + catch (Exception ex) + { + throw new InvalidObjectException(ex); + } + } + + public bool TryValidate(PrivateChat chat) + { + try + { + Validate(chat); + return true; + } + catch (Exception ex) + { + return false; + } + } +} \ No newline at end of file diff --git a/Govor.Core/Models/ChatGroup.cs b/Govor.Core/Models/ChatGroup.cs index b1ae3c2..7954c97 100644 --- a/Govor.Core/Models/ChatGroup.cs +++ b/Govor.Core/Models/ChatGroup.cs @@ -6,9 +6,9 @@ public class ChatGroup public string Name { get; set; } public string Description { get; set; } public Guid ImageId { get; set; } - - public List InviteCode { get; set; } public bool IsChannel { get; set; } public bool IsPrivate { get; set; } - public List Admins { get; set; } = new(); + public List Admins { get; set; } = new(); + public List Members { get; set; } = new(); + public List InviteCodes { get; set; } = new(); } \ No newline at end of file diff --git a/Govor.Core/Models/GroupInvitation.cs b/Govor.Core/Models/GroupInvitation.cs new file mode 100644 index 0000000..57f44c9 --- /dev/null +++ b/Govor.Core/Models/GroupInvitation.cs @@ -0,0 +1,15 @@ +namespace Govor.Core.Models; + +public class GroupInvitation +{ + public Guid Id { get; set; } + public Guid GroupId { get; set; } + public Guid UserMakerId { get; set; } + public string InvitationCode { get; set; } + public string Description {get; set;} + public DateTime EndDate { get; set; } + public DateTime CreatedAt { get; set; } + public int MaxParticipants { get; set; } + public List GroupMemberships { get; set; } = new(); + public User? UserMaker { get; set; } +} \ No newline at end of file diff --git a/Govor.Core/Models/GroupMembership.cs b/Govor.Core/Models/GroupMembership.cs index d33f40d..d70341e 100644 --- a/Govor.Core/Models/GroupMembership.cs +++ b/Govor.Core/Models/GroupMembership.cs @@ -5,5 +5,6 @@ public class GroupMembership public Guid Id { get; set; } public Guid GroupId { get; set; } public Guid UserId { get; set; } + public Guid InvitationId { get; set; } public bool IsBanned { get; set; } } \ No newline at end of file diff --git a/Govor.Core/Repositories/Groups/IGroupMessagesReader.cs b/Govor.Core/Repositories/Groups/IGroupMessagesReader.cs new file mode 100644 index 0000000..3f5d9ab --- /dev/null +++ b/Govor.Core/Repositories/Groups/IGroupMessagesReader.cs @@ -0,0 +1,8 @@ +using Govor.Core.Models; + +namespace Govor.Core.Repositories.Groups; + +public interface IGroupMessagesReader +{ + public Task> GetMessages(Guid chatId, Guid? startMessageId, int pageSize = 20); +} \ No newline at end of file diff --git a/Govor.Core/Repositories/Groups/IGroupsReader.cs b/Govor.Core/Repositories/Groups/IGroupsReader.cs index 752817d..26829d0 100644 --- a/Govor.Core/Repositories/Groups/IGroupsReader.cs +++ b/Govor.Core/Repositories/Groups/IGroupsReader.cs @@ -1,13 +1,13 @@ -using System.Text.RegularExpressions; +using Govor.Core.Models; namespace Govor.Core.Repositories.Groups; public interface IGroupsReader { - public Task> GetAllAsync(); - public Task GetByIdAsync(Guid id); - public Task> FindByNameAsync(string name); - public Task> GetByAdminIdAsync(Guid adminId); - public Task> GetByUserIdAsync(Guid adminId); - public bool IsUserMemberOfGroupAsync(Guid userId, Guid groupId); + public Task> GetAllAsync(); + public Task GetByIdAsync(Guid id); + public Task> SearchByNameAsync(string name); + public Task> GetByAdminIdAsync(Guid userId); + public Task> GetByUserIdAsync(Guid userId); + public Task IsUserMemberOfGroupAsync(Guid userId, Guid groupId); } \ No newline at end of file diff --git a/Govor.Core/Repositories/Groups/IGroupsWriter.cs b/Govor.Core/Repositories/Groups/IGroupsWriter.cs index dbb2ce4..4cc38f4 100644 --- a/Govor.Core/Repositories/Groups/IGroupsWriter.cs +++ b/Govor.Core/Repositories/Groups/IGroupsWriter.cs @@ -1,10 +1,10 @@ -using System.Text.RegularExpressions; +using Govor.Core.Models; namespace Govor.Core.Repositories.Groups; public interface IGroupsWriter { - Task Add(Group group); - Task Update(Group group); - Task Remove(Guid groupId); + Task AddAsync(ChatGroup group); + Task UpdateAsync(ChatGroup group); + Task RemoveAsync(Guid groupId); } \ No newline at end of file diff --git a/Govor.Core/Repositories/PrivateChats/IPrivateChatsExist.cs b/Govor.Core/Repositories/PrivateChats/IPrivateChatsExist.cs new file mode 100644 index 0000000..af72f01 --- /dev/null +++ b/Govor.Core/Repositories/PrivateChats/IPrivateChatsExist.cs @@ -0,0 +1,7 @@ +namespace Govor.Core.Repositories.PrivateChats; + +public interface IPrivateChatsExist +{ + bool Exist(Guid chatId); + bool Exist(Guid userAId, Guid userBId); +} \ No newline at end of file diff --git a/Govor.Core/Repositories/PrivateChats/IPrivateChatsReader.cs b/Govor.Core/Repositories/PrivateChats/IPrivateChatsReader.cs new file mode 100644 index 0000000..bcc2f83 --- /dev/null +++ b/Govor.Core/Repositories/PrivateChats/IPrivateChatsReader.cs @@ -0,0 +1,10 @@ +using Govor.Core.Models; + +namespace Govor.Core.Repositories.PrivateChats; + +public interface IPrivateChatsReader +{ + Task> GetAllAsync(); + Task GetByIdAsync(Guid id); + Task GetByMembersAsync(Guid memberAId, Guid memberBId); +} \ No newline at end of file diff --git a/Govor.Core/Repositories/PrivateChats/IPrivateChatsRepository.cs b/Govor.Core/Repositories/PrivateChats/IPrivateChatsRepository.cs new file mode 100644 index 0000000..de632d6 --- /dev/null +++ b/Govor.Core/Repositories/PrivateChats/IPrivateChatsRepository.cs @@ -0,0 +1,6 @@ +namespace Govor.Core.Repositories.PrivateChats; + +public interface IPrivateChatsRepository : IPrivateChatsReader, IPrivateChatsWriter, IPrivateChatsExist +{ + +} \ No newline at end of file diff --git a/Govor.Core/Repositories/PrivateChats/IPrivateChatsWriter.cs b/Govor.Core/Repositories/PrivateChats/IPrivateChatsWriter.cs new file mode 100644 index 0000000..d2afa33 --- /dev/null +++ b/Govor.Core/Repositories/PrivateChats/IPrivateChatsWriter.cs @@ -0,0 +1,10 @@ +using Govor.Core.Models; + +namespace Govor.Core.Repositories.PrivateChats; + +public interface IPrivateChatsWriter +{ + Task AddAsync(PrivateChat chat); + Task UpdateAsync(PrivateChat chat); + Task RemoveAsync(Guid chatId); +} \ No newline at end of file diff --git a/Govor.Data/GovorDbContext.cs b/Govor.Data/GovorDbContext.cs index c07d72a..b49c3e8 100644 --- a/Govor.Data/GovorDbContext.cs +++ b/Govor.Data/GovorDbContext.cs @@ -21,6 +21,7 @@ public class GovorDbContext(DbContextOptions options) : DbContex public virtual DbSet MediaFiles { get; set; } public virtual DbSet ChatGroups { get; set; } + public virtual DbSet GroupInvitations { get; set; } public virtual DbSet GroupMemberships { get; set; } public virtual DbSet GroupAdmins { get; set; } diff --git a/Govor.Data/Repositories/GroupRepository.cs b/Govor.Data/Repositories/GroupRepository.cs index cfbc592..daaf32a 100644 --- a/Govor.Data/Repositories/GroupRepository.cs +++ b/Govor.Data/Repositories/GroupRepository.cs @@ -1,62 +1,164 @@ using System.Text.RegularExpressions; +using Govor.Core.Infrastructure.Extensions; +using Govor.Core.Infrastructure.Validators; using Govor.Core.Models; using Govor.Core.Repositories.Groups; +using Govor.Data.Repositories.Exceptions; +using Microsoft.EntityFrameworkCore; namespace Govor.Data.Repositories; public class GroupRepository : IGroupsRepository { - public Task> GetAllAsync() + private GovorDbContext _context; + private IObjectValidator _validator; + + public GroupRepository(GovorDbContext context, IObjectValidator validator) { - throw new NotImplementedException(); - } - - public Task GetByIdAsync(Guid id) - { - throw new NotImplementedException(); - } - - public Task> FindByNameAsync(string name) - { - throw new NotImplementedException(); + _context = context; + _validator = validator; } - public Task> GetByAdminIdAsync(Guid adminId) + public async Task> GetAllAsync() { - throw new NotImplementedException(); + return await _context.ChatGroups + .AsNoTracking() + .Include(g => g.Admins) + .Include(g => g.Members) + .Include(g => g.InviteCodes) + .AsSplitQuery() + .ToListOrThrowIfEmpty(new NotFoundException("Database is empty")); } - public Task> GetByUserIdAsync(Guid adminId) + public async Task GetByIdAsync(Guid id) { - throw new NotImplementedException(); + return await _context.ChatGroups + .AsNoTracking() + .Include(g => g.Admins) + .Include(g => g.Members) + .Include(g => g.InviteCodes) + .AsSplitQuery() + .FirstOrDefaultAsync(g => g.Id == id) + ?? throw new NotFoundByKeyException(id, "Group with given id doesn't exist"); } - public Task Add(Group group) + public async Task> SearchByNameAsync(string name) { - throw new NotImplementedException(); + return await _context.ChatGroups + .AsNoTracking() + .Include(g => g.Admins) + .Include(g => g.Members) + .Include(g => g.InviteCodes) + .AsSplitQuery() + .Where(g => g.Name.ToLower().Contains(name.ToLower())) + .Take(20) + .ToListOrThrowIfEmpty(new NotFoundByKeyException(name, "Group with name doesn't exist")); + } + + public async Task> GetByAdminIdAsync(Guid userId) + { + return await _context.ChatGroups + .AsNoTracking() + .Include(g => g.Admins) + .Include(g => g.Members) + .Include(g => g.InviteCodes) + .AsSplitQuery() + .Where(g => g.Admins.Any(a => a.UserId == userId)) + .ToListOrThrowIfEmpty(new NotFoundByKeyException(userId, "Admin with given id doesn't exist")); } - public Task Update(Group group) + public async Task> GetByUserIdAsync(Guid userId) { - throw new NotImplementedException(); + return await _context.ChatGroups + .AsNoTracking() + .Include(g => g.Admins) + .Include(g => g.Members) + .Include(g => g.InviteCodes) + .AsSplitQuery() + .Where(g => g.Members.Any(a => a.UserId == userId)) + .ToListOrThrowIfEmpty(new NotFoundByKeyException(userId, "Member with given id doesn't exist")); } - public Task Remove(Guid groupId) + public async Task AddAsync(ChatGroup group) { - throw new NotImplementedException(); + try + { + _validator.Validate(group); + + _context.ChatGroups.Add(group); + await _context.SaveChangesAsync(); + } + catch (InvalidObjectException ex) + { + throw new AdditionException("Group with given data invalid", ex); + } + catch (Exception ex) + { + throw new AdditionException("Cannot add group", ex); + } } + + public async Task UpdateAsync(ChatGroup group) + { + try + { + _validator.Validate(group); + + var rowsAffected = await _context.ChatGroups + .Where(u => u.Id == group.Id) + .ExecuteUpdateAsync(g => g + .SetProperty(g => g.Name, group.Name) + .SetProperty(g => g.Description, group.Description) + .SetProperty(g => g.ImageId, group.ImageId) + .SetProperty(g => g.IsChannel, group.IsChannel) + .SetProperty(g => g.IsPrivate, group.IsPrivate) + .SetProperty(g => g.InviteCodes, group.InviteCodes) + ); + + if (rowsAffected == 0) + throw new UpdateException($"Not found group by given id {group.Id}"); + } + catch (Exception ex) + { + throw new UpdateException($"Error when updating the group {group.Id}", ex); + } + } + + public async Task RemoveAsync(Guid groupId) + { + try + { + var result = await GetByIdAsync(groupId); + + _context.ChatGroups.Remove(result); + await _context.SaveChangesAsync(); + } + catch (InvalidObjectException ex) + { + throw new RemoveException("User with given data invalid", ex); + } + } + public bool Exists(Guid groupId) { - throw new NotImplementedException(); + return _context.ChatGroups.Any(g => g.Id == groupId); } public bool Exists(ChatGroup chatGroup) { - throw new NotImplementedException(); + return _context.ChatGroups.Any(g => g.Id == chatGroup.Id && + g.IsChannel == chatGroup.IsChannel && + g.Description == chatGroup.Description && + g.IsPrivate == chatGroup.IsPrivate && + g.Name == chatGroup.Name && + g.ImageId == chatGroup.ImageId); } - public bool IsUserMemberOfGroupAsync(Guid userId, Guid groupId) + public async Task IsUserMemberOfGroupAsync(Guid userId, Guid groupId) { - throw new NotImplementedException(); + return await _context.ChatGroups + .AsNoTracking() + .Include(g => g.Members) + .AnyAsync(g => g.Members.Any(a => a.UserId == userId && a.GroupId == groupId)); } } diff --git a/Govor.Data/Repositories/PrivateChatsRepository.cs b/Govor.Data/Repositories/PrivateChatsRepository.cs new file mode 100644 index 0000000..07836ec --- /dev/null +++ b/Govor.Data/Repositories/PrivateChatsRepository.cs @@ -0,0 +1,115 @@ +using Govor.Core.Infrastructure.Extensions; +using Govor.Core.Infrastructure.Validators; +using Govor.Core.Models; +using Govor.Core.Repositories.PrivateChats; +using Govor.Data.Repositories.Exceptions; +using Microsoft.EntityFrameworkCore; + +namespace Govor.Data.Repositories; + +public class PrivateChatsRepository : IPrivateChatsRepository +{ + private GovorDbContext _context; + private IObjectValidator _validator; + + public PrivateChatsRepository(GovorDbContext context, IObjectValidator validator) + { + _context = context; + _validator = validator; + } + + public async Task> GetAllAsync() + { + return await _context.PrivateChats + .AsNoTracking() + .ToListOrThrowIfEmpty(new NotFoundException("Database is empty")); + } + + public async Task GetByIdAsync(Guid id) + { + return await _context.PrivateChats + .AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == id) + ?? throw new NotFoundByKeyException(id, "Private Chat with given Id does not exist"); + } + + public async Task GetByMembersAsync(Guid memberAId, Guid memberBId) + { + return await _context.PrivateChats + .AsNoTracking() + .FirstOrDefaultAsync(f => (f.UserAId == memberAId && f.UserBId == memberBId) || (f.UserAId == memberBId && f.UserBId == memberAId)) + ?? throw new NotFoundByKeyException<(Guid, Guid)>((memberAId, memberBId), "Private Chat with given members Id's does not exist"); + } + + public async Task AddAsync(PrivateChat chat) + { + try + { + _validator.Validate(chat); + + _context.PrivateChats.Add(chat); + await _context.SaveChangesAsync(); + } + catch (InvalidObjectException ex) + { + throw new AdditionException("Private chat with given data invalid", ex); + } + catch (Exception ex) + { + throw new AdditionException("Cannot add private chat", ex); + } + } + + public async Task UpdateAsync(PrivateChat chat) + { + try + { + _validator.Validate(chat); + + var rowsAffected = await _context.PrivateChats + .Where(m => m.Id == chat.Id) + .ExecuteUpdateAsync(c => c + .SetProperty(c => c.UserAId, chat.UserAId) + .SetProperty(c => c.UserBId, chat.UserBId) + .SetProperty(c => c.Messages, chat.Messages) + ); + + if (rowsAffected == 0) + throw new UpdateException($"Not found private chat by given id {chat.Id}"); + } + catch (Exception ex) + { + throw new UpdateException($"Error when updating the private chat {chat.Id}", ex); + } + } + + public async Task RemoveAsync(Guid chatId) + { + try + { + var result = await GetByIdAsync(chatId); + + _context.PrivateChats.Remove(result); + await _context.SaveChangesAsync(); + } + catch (NotFoundByKeyException ex) + { + throw new RemoveException($"Not found private chat by given id {chatId}", ex); + } + catch (Exception ex) + { + throw new RemoveException("Error when removing the private chat", ex); + } + } + + public bool Exist(Guid chatId) + { + return _context.PrivateChats.Any(c => c.Id == chatId); + } + + public bool Exist(Guid userAId, Guid userBId) + { + return _context.PrivateChats.Any(f => (f.UserAId == userAId && f.UserBId == userBId) + || (f.UserAId == userBId && f.UserBId == userAId)); + } +} \ No newline at end of file