diff --git a/Govor.API.Tests/IntegrationTests/Controllers/ChatLoadControllerTests.cs b/Govor.API.Tests/IntegrationTests/Controllers/ChatLoadControllerTests.cs new file mode 100644 index 0000000..00a0377 --- /dev/null +++ b/Govor.API.Tests/IntegrationTests/Controllers/ChatLoadControllerTests.cs @@ -0,0 +1,107 @@ +using AutoFixture; +using AutoMapper; +using Govor.API.Controllers; +using Govor.Application.Interfaces; +using Govor.Application.Interfaces.Infrastructure.Extensions; +using Govor.Contracts.Requests; +using Govor.Contracts.Responses; +using Govor.Core.Models.Messages; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Govor.API.Tests.IntegrationTests.Controllers; + +[TestFixture] +public class ChatLoadControllerTests +{ + private Mock _currentUserServiceMock; + private Mock> _loggerMock; + private Mock _messagesLoaderMock; + private Mock _mapperMock; + private Fixture _fixture; + private ChatLoadController _controller; + + private Guid _userId = Guid.NewGuid(); + private Guid _chatId = Guid.NewGuid(); + private Guid _currentId = Guid.NewGuid(); + + + [SetUp] + public void SetUp() + { + _fixture = new Fixture(); + _fixture.Behaviors.OfType().ToList().ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _currentUserServiceMock = new Mock(); + _loggerMock = new Mock>(); + _messagesLoaderMock = new Mock(); + _mapperMock = new Mock(); + + _currentUserServiceMock.Setup(u => u.GetCurrentUserId()).Returns(_currentId); + + _controller = new ChatLoadController( + _loggerMock.Object, + _messagesLoaderMock.Object, + _currentUserServiceMock.Object, + _mapperMock.Object); + } + + // Tests for GetChatMessages action + [Test] + public async Task GetChatMessages_ValidRequest_ReturnsOkResult() + { + // Arrange + var random = new Random(); + var messages = _fixture.CreateMany(random.Next(3, 10)).ToList(); + var before = random.Next(1, 10); + var after = random.Next(1, 10); + + var query = new MessageQuery() + { + Before = before, + After = after, + StartMessageId = null + }; + + _messagesLoaderMock.Setup(m => + m.LoadMessagesInChatGroup(_chatId, _currentId, null, before, after)) + .ReturnsAsync(messages); + + var messagesResponse = messages.Select(m => new MessageResponse() + { + Id = m.Id, + SenderId = m.SenderId, + RecipientId = m.RecipientId, + RecipientType = m.RecipientType, + }).ToList(); + + _mapperMock.Setup(f => f.Map>(messages)). + Returns(messagesResponse); + + // Act + var result = await _controller.GetGroupMessages(_chatId, query); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + + var okResult = (OkObjectResult)result; + var values = okResult.Value as List; + + Assert.That(values, Is.Not.Null); + + Assert.That(values.Select(v => v.Id), + Is.EquivalentTo(messagesResponse.Select(m => m.Id))); + + Assert.That(values.Select(v => v.SenderId), + Is.EquivalentTo(messagesResponse.Select(m => m.SenderId))); + + Assert.That(values.Select(v => v.RecipientId), + Is.EquivalentTo(messagesResponse.Select(m => m.RecipientId))); + + Assert.That(values.Select(v => v.RecipientType), + Is.EquivalentTo(messagesResponse.Select(m => m.RecipientType))); + } +} \ No newline at end of file diff --git a/Govor.API/Controllers/ChatLoadController.cs b/Govor.API/Controllers/ChatLoadController.cs index 36fd42d..fcda9ca 100644 --- a/Govor.API/Controllers/ChatLoadController.cs +++ b/Govor.API/Controllers/ChatLoadController.cs @@ -1,7 +1,9 @@ using AutoMapper; using Govor.Application.Interfaces; using Govor.Application.Interfaces.Infrastructure.Extensions; +using Govor.Contracts.Requests; using Govor.Contracts.Responses; +using Govor.Data.Repositories.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -16,6 +18,7 @@ public class ChatLoadController : Controller private readonly ILogger _logger; private readonly IMessagesLoader _messagesLoader; private readonly IMapper _mapper; + public ChatLoadController( ILogger logger, IMessagesLoader messagesLoader, @@ -29,18 +32,21 @@ public class ChatLoadController : Controller } [HttpGet("group-messages")] - public async Task GetChatMessages( - [FromQuery] Guid chatId, - [FromQuery] Guid? startMessageId, - [FromQuery] int pageSize) + public async Task GetGroupMessages( + Guid chatId, + [FromQuery] MessageQuery query) { try { - var result = await _messagesLoader.LoadLastMessagesInChatGroup( + if (query.Before < 0 || query.After < 0 || query.After + query.Before > 100) + return BadRequest("Values must be non-negative and total must not exceed 100."); + + var result = await _messagesLoader.LoadMessagesInChatGroup( chatId, _currentUser.GetCurrentUserId(), - startMessageId, - pageSize); + query.StartMessageId, + query.Before, + query.After); var response = _mapper.Map>(result); @@ -51,6 +57,16 @@ public class ChatLoadController : Controller _logger.LogWarning(ex.Message); return Forbid(ex.Message); } + catch (NotFoundException ex) + { + _logger.LogWarning(ex, ex.Message); + return BadRequest(ex.Message); + } + catch (ArgumentException ex) + { + _logger.LogWarning(ex, ex.Message); + return BadRequest(ex.Message); + } catch (Exception ex) { _logger.LogError(ex, ex.Message); @@ -60,17 +76,20 @@ public class ChatLoadController : Controller [HttpGet("user-messages")] public async Task GetUserMessages( - [FromQuery] Guid userId, - [FromQuery] Guid? startMessageId, - [FromQuery] int pageSize = 20) + Guid userId, + [FromQuery] MessageQuery query) { try { - var result = await _messagesLoader.LoadLastMessagesInUserChat( + if (query.Before < 0 || query.After < 0 || query.After + query.Before > 100) + return BadRequest("Values must be non-negative and total must not exceed 100."); + + var result = await _messagesLoader.LoadMessagesInUserChat( userId, _currentUser.GetCurrentUserId(), - startMessageId, - pageSize); + query.StartMessageId, + query.Before, + query.After); var response = _mapper.Map>(result); @@ -86,6 +105,16 @@ public class ChatLoadController : Controller _logger.LogWarning(ex.Message); return Forbid(ex.Message); } + catch (NotFoundException ex) + { + _logger.LogWarning(ex, ex.Message); + return BadRequest(ex.Message); + } + catch (ArgumentException ex) + { + _logger.LogWarning(ex, ex.Message); + return BadRequest(ex.Message); + } catch (Exception ex) { _logger.LogError(ex, ex.Message); diff --git a/Govor.API/Controllers/OnlinePingingController.cs b/Govor.API/Controllers/OnlinePingingController.cs index 871e960..3f3a026 100644 --- a/Govor.API/Controllers/OnlinePingingController.cs +++ b/Govor.API/Controllers/OnlinePingingController.cs @@ -13,6 +13,7 @@ public class OnlinePingingController : Controller { private readonly ILogger _logger; private readonly IPingHandlerService _ping; + private readonly IUserPresenceService _presenceService; private readonly ICurrentUserService _currentUserService; public OnlinePingingController(ILogger logger, @@ -51,13 +52,21 @@ public class OnlinePingingController : Controller catch (Exception e) { _logger.LogError(e, e.Message); - return StatusCode(500, new { error = "Failed to send friend request." }); + return StatusCode(500, "Failed to ping."); } } - [HttpGet("is-online")] - public async Task IsOnline(Guid userId) + [HttpGet("status/{userId}")] + public IActionResult GetStatus(Guid userId) { - return BadRequest(); + try + { + return Ok(_presenceService.WhenUserWasOnline(userId)); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + return StatusCode(500, "Internal server error."); + } } } \ No newline at end of file diff --git a/Govor.Application.Tests/Services/Messages/MessagesLoaderTests.cs b/Govor.Application.Tests/Services/Messages/MessagesLoaderTests.cs index 3c8f76d..4bdf983 100644 --- a/Govor.Application.Tests/Services/Messages/MessagesLoaderTests.cs +++ b/Govor.Application.Tests/Services/Messages/MessagesLoaderTests.cs @@ -56,7 +56,7 @@ public class MessagesLoaderTests await _dbContext.SaveChangesAsync(); // Act - var result = await _loader.LoadLastMessagesInUserChat(_currentUserId, _otherUserId, null); + var result = await _loader.LoadMessagesInUserChat(_currentUserId, _otherUserId, null); // Assert Assert.That(result, Has.Count.EqualTo(1)); @@ -70,7 +70,7 @@ public class MessagesLoaderTests _privateChatsRepoMock.Setup(r => r.GetByMembersAsync(_currentUserId, _otherUserId)) .ReturnsAsync(new PrivateChat { Id = Guid.NewGuid() }); // Act - var result = await _loader.LoadLastMessagesInUserChat(_currentUserId, _otherUserId, null); + var result = await _loader.LoadMessagesInUserChat(_currentUserId, _otherUserId, null); // Assert Assert.That(result, Is.Empty); @@ -81,7 +81,7 @@ public class MessagesLoaderTests { // Act & Assert var ex = Assert.ThrowsAsync(async () => - await _loader.LoadLastMessagesInUserChat(Guid.Empty, _currentUserId, null)); + await _loader.LoadMessagesInUserChat(Guid.Empty, _currentUserId, null)); Assert.That(ex.Message, Does.Contain("User id cannot be empty")); } @@ -94,7 +94,7 @@ public class MessagesLoaderTests // Act & Assert var ex = Assert.ThrowsAsync(async () => - await _loader.LoadLastMessagesInUserChat(_currentUserId, _otherUserId, null)); + await _loader.LoadMessagesInUserChat(_currentUserId, _otherUserId, null)); Assert.That(ex.Message, Is.EqualTo("Private chat not found")); } @@ -116,7 +116,7 @@ public class MessagesLoaderTests await _dbContext.SaveChangesAsync(); // Act - var result = await _loader.LoadLastMessagesInChatGroup(_groupChatId, _currentUserId, null); + var result = await _loader.LoadMessagesInChatGroup(_groupChatId, _currentUserId, null); // Assert Assert.That(result, Has.Count.EqualTo(1)); @@ -127,9 +127,9 @@ public class MessagesLoaderTests { // Act & Assert var ex = Assert.ThrowsAsync(async () => - await _loader.LoadLastMessagesInChatGroup(Guid.Empty, _currentUserId, null)); + await _loader.LoadMessagesInChatGroup(Guid.Empty, _currentUserId, null)); - Assert.That(ex.Message, Does.Contain("Chat id cannot be empty")); + Assert.That(ex.Message, Is.EqualTo("Chat id cannot be empty")); } [Test] @@ -141,9 +141,9 @@ public class MessagesLoaderTests // Act & Assert var ex = Assert.ThrowsAsync(async () => - await _loader.LoadLastMessagesInChatGroup(_groupChatId, _currentUserId, null)); + await _loader.LoadMessagesInChatGroup(_groupChatId, _currentUserId, null)); - Assert.That(ex.Message, Is.EqualTo("You are not in a group.")); + Assert.That(ex.Message, Is.EqualTo("You are not a member of this group.")); } [Test] @@ -154,7 +154,7 @@ public class MessagesLoaderTests .ReturnsAsync(true); // Act - var result = await _loader.LoadLastMessagesInChatGroup(_groupChatId, _currentUserId, null); + var result = await _loader.LoadMessagesInChatGroup(_groupChatId, _currentUserId, null); // Assert Assert.That(result, Is.Empty); diff --git a/Govor.Application/Interfaces/IMessagesLoader.cs b/Govor.Application/Interfaces/IMessagesLoader.cs index cb8880d..54fff2f 100644 --- a/Govor.Application/Interfaces/IMessagesLoader.cs +++ b/Govor.Application/Interfaces/IMessagesLoader.cs @@ -4,6 +4,6 @@ namespace Govor.Application.Interfaces; public interface IMessagesLoader { - Task> LoadLastMessagesInUserChat(Guid userId,Guid currentId, Guid? startMessageId, int pageSize = 20); - Task> LoadLastMessagesInChatGroup(Guid chatId,Guid currentId, Guid? startMessageId, int pageSize = 20); + Task> LoadMessagesInUserChat(Guid userId,Guid currentId, Guid? startMessageId, int before = 20, int after = 2); + Task> LoadMessagesInChatGroup(Guid chatId,Guid currentId, Guid? startMessageId, int before = 20, int after = 2); } \ No newline at end of file diff --git a/Govor.Application/Interfaces/IUserPresenceService.cs b/Govor.Application/Interfaces/IUserPresenceService.cs new file mode 100644 index 0000000..38eea19 --- /dev/null +++ b/Govor.Application/Interfaces/IUserPresenceService.cs @@ -0,0 +1,6 @@ +namespace Govor.Application.Interfaces; + +public interface IUserPresenceService +{ + DateTime WhenUserWasOnline(Guid userId); +} \ No newline at end of file diff --git a/Govor.Application/Services/Messages/MessagesLoader.cs b/Govor.Application/Services/Messages/MessagesLoader.cs index 541a400..872084b 100644 --- a/Govor.Application/Services/Messages/MessagesLoader.cs +++ b/Govor.Application/Services/Messages/MessagesLoader.cs @@ -1,8 +1,6 @@ using Govor.Application.Interfaces; -using Govor.Core.Infrastructure.Extensions; using Govor.Core.Models.Messages; using Govor.Core.Repositories.Groups; -using Govor.Core.Repositories.Messages; using Govor.Core.Repositories.PrivateChats; using Govor.Data; using Govor.Data.Repositories.Exceptions; @@ -26,57 +24,121 @@ public class MessagesLoader : IMessagesLoader _dbContext = dbContext; } - public async Task> LoadLastMessagesInUserChat(Guid userId, Guid currentUser, Guid? startMessageId, int pageSize = 20) + public async Task> LoadMessagesInUserChat( + Guid userId, + Guid currentUser, + Guid? startMessageId, + int before = 20, + int after = 2) { - if(userId == Guid.Empty) + if (userId == Guid.Empty) throw new ArgumentException("User id cannot be empty"); - - if(!_privateChatsRepository.Exist(userId, currentUser)) + + if (!_privateChatsRepository.Exist(userId, currentUser)) throw new InvalidOperationException("Private chat not found"); - - try + + var chat = await _privateChatsRepository.GetByMembersAsync(userId, currentUser); + + var query = _dbContext.Messages + .AsNoTracking() + .Include(m => m.MediaAttachments) + .ThenInclude(m => m.MediaFile) + .Where(m => m.RecipientType == RecipientType.User && + m.RecipientId == chat.Id); + + if (startMessageId is null) { - var chat = await _privateChatsRepository.GetByMembersAsync(userId, currentUser); - - return await _dbContext.Messages - .AsNoTracking() - .Include(m => m.MediaAttachments) - .ThenInclude(m => m.MediaFile) - .AsSplitQuery() - .Where(m => m.RecipientType == RecipientType.User && - m.RecipientId == chat.Id) - .Take(pageSize) - .ToListOrThrowIfEmpty(new NotFoundException("Messages not found")); - } - catch (NotFoundException ex) - { - return new List(); + return await query + .OrderByDescending(m => m.SentAt) + .Take(before) + .ToListAsync(); } + + var startMessage = await _dbContext.Messages.FindAsync(startMessageId.Value); + if (startMessage == null) + throw new NotFoundException("Start message not found"); + + var beforeMessages = await query + .Where(m => m.SentAt < startMessage.SentAt) + .OrderByDescending(m => m.SentAt) + .Take(before) + .ToListAsync(); + + var afterMessages = await query + .Where(m => m.SentAt > startMessage.SentAt) + .OrderBy(m => m.SentAt) + .Take(after) + .ToListAsync(); + + // older -> start -> newer + var result = beforeMessages + .OrderBy(m => m.SentAt) + .Concat(new[] { startMessage }) + .Concat(afterMessages) + .ToList(); + + return result; } - public async Task> LoadLastMessagesInChatGroup(Guid chatId, Guid currentUser, Guid? startMessageId, int pageSize = 20) + + public async Task> LoadMessagesInChatGroup( + Guid chatId, + Guid currentUser, + Guid? startMessageId, + int before = 20, + int after = 2) { - if(chatId == Guid.Empty) + if (chatId == Guid.Empty) throw new ArgumentException("Chat id cannot be empty"); - - if(!await _groupsRepository.IsUserMemberOfGroupAsync(currentUser, chatId)) - throw new UnauthorizedAccessException("You are not in a group."); - - try + + var isMember = await _groupsRepository.IsUserMemberOfGroupAsync(currentUser, chatId); + if (!isMember) + throw new UnauthorizedAccessException("You are not a member of this group."); + + var baseQuery = _dbContext.Messages + .AsNoTracking() + .Include(m => m.MediaAttachments) + .ThenInclude(m => m.MediaFile) + .AsSplitQuery() + .Where(m => m.RecipientType == RecipientType.Group && m.RecipientId == chatId); + + if (startMessageId is null) { - return await _dbContext.Messages - .AsNoTracking() - .Include(m => m.MediaAttachments) - .ThenInclude(m => m.MediaFile) - .AsSplitQuery() - .Where(m => m.RecipientType == RecipientType.Group && - m.RecipientId == chatId) - .Take(pageSize) - .ToListOrThrowIfEmpty(new NotFoundException("Messages not found")); - } - catch (NotFoundException ex) - { - return new List(); + return await baseQuery + .OrderByDescending(m => m.SentAt) + .Take(before) + .OrderBy(m => m.SentAt) + .ToListAsync(); } + + var startMessage = await _dbContext.Messages + .AsNoTracking() + .FirstOrDefaultAsync(m => m.Id == startMessageId.Value && + m.RecipientType == RecipientType.Group && + m.RecipientId == chatId); + + if (startMessage == null) + throw new NotFoundException("Start message not found in this group."); + + var beforeMessages = await baseQuery + .Where(m => m.SentAt < startMessage.SentAt) + .OrderByDescending(m => m.SentAt) + .Take(before) + .ToListAsync(); + + var afterMessages = await baseQuery + .Where(m => m.SentAt > startMessage.SentAt) + .OrderBy(m => m.SentAt) + .Take(after) + .ToListAsync(); + + var result = beforeMessages + .OrderBy(m => m.SentAt) + .Concat(new[] { startMessage }) + .Concat(afterMessages) + .ToList(); + + return result; } + } \ No newline at end of file diff --git a/Govor.ConsoleClient/Commands/HelpCommand.cs b/Govor.ConsoleClient/Commands/HelpCommand.cs index b8cd16f..f0fa612 100644 --- a/Govor.ConsoleClient/Commands/HelpCommand.cs +++ b/Govor.ConsoleClient/Commands/HelpCommand.cs @@ -39,16 +39,14 @@ public class HelpCommand : ICommand else { _logger.Info("Чтобы получить подробную информацию, напишите /help {command}"); - } - - foreach (var command in commands) - { - var name = command.GetType().GetCustomAttribute()?.Path.Replace("/", "").ToLower() - ?? command.GetType().Name.Replace("Command", "").ToLower(); + foreach (var command in commands) + { + var name = command.GetType().GetCustomAttribute()?.Path.Replace("/", "").ToLower() + ?? command.GetType().Name.Replace("Command", "").ToLower(); - _logger.Log($"{name} - {command.ShortHelp()}"); + _logger.Log($"{name} - {command.ShortHelp()}"); + } } - return Task.CompletedTask; } diff --git a/Govor.ConsoleClient/Commands/SendMessageCommand.cs b/Govor.ConsoleClient/Commands/SendMessageCommand.cs index 8aa96db..56d4528 100644 --- a/Govor.ConsoleClient/Commands/SendMessageCommand.cs +++ b/Govor.ConsoleClient/Commands/SendMessageCommand.cs @@ -34,7 +34,7 @@ public class SendMessageCommand : IInteractiveCommand public string LongHelp() { - throw new NotImplementedException(); + return "Отпарвка тестовых сообщений не существующему юзеру 2"; } public string ShortHelp() diff --git a/Govor.Contracts/Requests/MessageQuery.cs b/Govor.Contracts/Requests/MessageQuery.cs new file mode 100644 index 0000000..5dd24f7 --- /dev/null +++ b/Govor.Contracts/Requests/MessageQuery.cs @@ -0,0 +1,8 @@ +namespace Govor.Contracts.Requests; + +public class MessageQuery +{ + public Guid? StartMessageId { get; set; } + public int Before { get; set; } = 20; + public int After { get; set; } = 2; +}