diff --git a/MinimalChat.API.sln b/MinimalChat.API.sln index 87a0e77..c16f776 100644 --- a/MinimalChat.API.sln +++ b/MinimalChat.API.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinmalChat.Data", "MinmalCh EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalChat.Domain", "MinimalChat.Domain\MinimalChat.Domain.csproj", "{02D64341-3011-405F-B842-992193E94896}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalChat.Tests", "MinimalChat.Tests\MinimalChat.Tests.csproj", "{CB0E12C8-B21B-450F-A327-1F71361F4DA3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {02D64341-3011-405F-B842-992193E94896}.Debug|Any CPU.Build.0 = Debug|Any CPU {02D64341-3011-405F-B842-992193E94896}.Release|Any CPU.ActiveCfg = Release|Any CPU {02D64341-3011-405F-B842-992193E94896}.Release|Any CPU.Build.0 = Release|Any CPU + {CB0E12C8-B21B-450F-A327-1F71361F4DA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB0E12C8-B21B-450F-A327-1F71361F4DA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB0E12C8-B21B-450F-A327-1F71361F4DA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB0E12C8-B21B-450F-A327-1F71361F4DA3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MinimalChat.Tests/ChatHubTests.cs b/MinimalChat.Tests/ChatHubTests.cs new file mode 100644 index 0000000..f32608a --- /dev/null +++ b/MinimalChat.Tests/ChatHubTests.cs @@ -0,0 +1,311 @@ +using FluentAssertions; +using Microsoft.AspNetCore.SignalR; +using MinimalChat.API.Hubs; +using MinimalChat.Domain.DTOs; +using MinimalChat.Domain.Interfaces; +using Moq; +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Xunit; + +namespace MinimalChat.Tests +{ + // Define IChatClient and GroupMessageDto for testing purposes + // These should ideally mirror what's in ChatHub.cs or a shared contract + public interface IChatClient + { + Task ReceiveMessage(MessageDto message); + Task ReceiveGroupMessage(GroupMessageDto message); + Task JoinedGroup(string groupName); + Task LeftGroup(string groupName); + Task UserOnline(string userId); + Task UserOffline(string userId); + Task Error(string message); // Added for error notifications + } + + public class GroupMessageDto // Simplified for testing + { + public Guid GroupId { get; set; } + public string SenderId { get; set; } + public string SenderName { get; set; } // Assuming sender name might be useful + public string Content { get; set; } + public DateTime Timestamp { get; set; } + } + + + public class ChatHubTests + { + private readonly Mock _mockMessageService; + private readonly Mock _mockGroupService; + private readonly Mock> _mockClients; + private readonly Mock _mockClientProxy; // Client proxy for specific users/groups/caller + private readonly Mock _mockGroups; + private readonly Mock _mockContext; + private readonly ChatHub _hub; + + public ChatHubTests() + { + _mockMessageService = new Mock(); + _mockGroupService = new Mock(); + _mockClients = new Mock>(); + _mockClientProxy = new Mock(); + _mockGroups = new Mock(); + _mockContext = new Mock(); + + _hub = new ChatHub(_mockMessageService.Object, _mockGroupService.Object) + { + Clients = _mockClients.Object, + Groups = _mockGroups.Object, + Context = _mockContext.Object + }; + } + + private void SetupHubContext(string userId, string connectionId = "test-connection-id") + { + var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId) }; + var identity = new ClaimsIdentity(claims, "TestAuthentication"); + var user = new ClaimsPrincipal(identity); + + _mockContext.Setup(c => c.User).Returns(user); + _mockContext.Setup(c => c.UserIdentifier).Returns(userId); + _mockContext.Setup(c => c.ConnectionId).Returns(connectionId); + } + + [Fact] + public async Task SendMessageToUser_ValidMessage_CallsServiceAndSendsToReceiver() + { + // Arrange + var senderId = Guid.NewGuid().ToString(); + var receiverId = Guid.NewGuid().ToString(); + var messageContent = "Hello there!"; + var connectionId = "sender-conn-id"; + SetupHubContext(senderId, connectionId); + + var messageDtoFromService = new ResponseMessageDto // This is what MessageService returns + { + Id = 1, + SenderId = Guid.Parse(senderId), + ReceiverId = Guid.Parse(receiverId), + Content = messageContent, + Timestamp = DateTime.UtcNow, + SenderName = "Sender", + ReceiverName = "Receiver" + }; + + // This is what ChatHub maps to for IChatClient.ReceiveMessage + var expectedMessageDtoForClient = new MessageDto + { + SenderId = senderId, + ReceiverId = receiverId, + Content = messageContent, + Timestamp = messageDtoFromService.Timestamp // Assuming timestamp is passed through + }; + + + _mockMessageService.Setup(s => s.SendMessageAsync(It.Is(dto => + dto.SenderId == senderId && + dto.ReceiverId == receiverId && + dto.Content == messageContent))) + .ReturnsAsync(messageDtoFromService); + + _mockClients.Setup(c => c.User(receiverId)).Returns(_mockClientProxy.Object); + _mockClients.Setup(c => c.Client(connectionId)).Returns(_mockClientProxy.Object); // For sender confirmation + + // Act + await _hub.SendMessageToUser(receiverId, messageContent); + + // Assert + _mockMessageService.Verify(s => s.SendMessageAsync(It.Is(dto => + dto.SenderId == senderId && + dto.ReceiverId == receiverId && + dto.Content == messageContent)), Times.Once); + + // Verify message sent to receiver + _mockClientProxy.Verify(c => c.ReceiveMessage(It.Is(m => + m.SenderId == senderId && + m.ReceiverId == receiverId && + m.Content == messageContent && + m.Timestamp == expectedMessageDtoForClient.Timestamp + )), Times.Once); + + // Verify message also sent back to the sender (caller) + _mockClientProxy.Verify(c => c.ReceiveMessage(It.Is(m => + m.SenderId == senderId && + m.ReceiverId == receiverId && // Self-message still has receiverId + m.Content == messageContent && + m.Timestamp == expectedMessageDtoForClient.Timestamp + )), Times.Exactly(2)); // Once for receiver, once for sender + } + + [Fact] + public async Task SendMessageToUser_ServiceThrowsException_DoesNotSendMessage() + { + // Arrange + var senderId = Guid.NewGuid().ToString(); + var receiverId = Guid.NewGuid().ToString(); + var messageContent = "Test message"; + SetupHubContext(senderId); + + _mockMessageService.Setup(s => s.SendMessageAsync(It.IsAny())) + .ThrowsAsync(new Exception("Service error")); + + _mockClients.Setup(c => c.User(receiverId)).Returns(_mockClientProxy.Object); + _mockClients.Setup(c => c.Caller).Returns(_mockClientProxy.Object); + + + // Act + await Assert.ThrowsAsync(() => _hub.SendMessageToUser(receiverId, messageContent)); + + // Assert + _mockMessageService.Verify(s => s.SendMessageAsync(It.IsAny()), Times.Once); + _mockClientProxy.Verify(c => c.ReceiveMessage(It.IsAny()), Times.Never); + // Optional: Verify error sent to caller if implemented + // _mockClientProxy.Verify(c => c.Error(It.IsAny()), Times.Once); + } + + [Fact] + public async Task JoinGroup_ValidRequest_CallsServiceAndAddsToGroup() + { + // Arrange + var memberId = Guid.NewGuid().ToString(); + var connectionId = "test-conn-id"; + var groupId = Guid.NewGuid(); + var groupName = "Test Group"; + SetupHubContext(memberId, connectionId); + + _mockGroupService.Setup(s => s.AddMemberToGroupAsync(groupId, + It.Is(dto => dto.UserId == memberId), + memberId)) // currentUserId is the memberId itself + .ReturnsAsync(true); + + _mockClients.Setup(c => c.Caller).Returns(_mockClientProxy.Object); + + // Act + await _hub.JoinGroup(groupId.ToString(), groupName); + + // Assert + _mockGroupService.Verify(s => s.AddMemberToGroupAsync(groupId, + It.Is(dto => dto.UserId == memberId), + memberId), Times.Once); + _mockGroups.Verify(g => g.AddToGroupAsync(connectionId, groupName, default), Times.Once); + _mockClientProxy.Verify(c => c.JoinedGroup(groupName), Times.Once); + } + + [Fact] + public async Task JoinGroup_ServiceFails_DoesNotAddToGroupOrNotify() + { + // Arrange + var memberId = Guid.NewGuid().ToString(); + var connectionId = "test-conn-id"; + var groupId = Guid.NewGuid(); + var groupName = "Test Group"; + SetupHubContext(memberId, connectionId); + + _mockGroupService.Setup(s => s.AddMemberToGroupAsync(groupId, It.Is(dto => dto.UserId == memberId), memberId)) + .ReturnsAsync(false); // Service indicates failure + + _mockClients.Setup(c => c.Caller).Returns(_mockClientProxy.Object); + + // Act + await _hub.JoinGroup(groupId.ToString(), groupName); // Should not throw, but handle gracefully + + // Assert + _mockGroupService.Verify(s => s.AddMemberToGroupAsync(groupId, It.Is(dto => dto.UserId == memberId), memberId), Times.Once); + _mockGroups.Verify(g => g.AddToGroupAsync(It.IsAny(), It.IsAny(), default), Times.Never); + _mockClientProxy.Verify(c => c.JoinedGroup(It.IsAny()), Times.Never); + // Verify error notification to caller + _mockClientProxy.Verify(c => c.Error($"Failed to join group {groupName}."), Times.Once); + } + + [Fact] + public async Task SendMessageToGroup_ValidMessage_SendsToGroup() + { + // Arrange + var senderId = Guid.NewGuid().ToString(); + var senderName = "Test Sender"; // Assume hub fetches this or it's passed + var groupId = Guid.NewGuid(); + var groupName = "Test Chat Group"; + var messageContent = "Hello group!"; + SetupHubContext(senderId); + + _mockContext.Setup(c => c.User.Identity.Name).Returns(senderName); // If hub uses User.Identity.Name + + var expectedGroupMessage = new GroupMessageDto + { + GroupId = groupId, + SenderId = senderId, + SenderName = senderName, + Content = messageContent, + Timestamp = It.IsAny() // Timestamp is generated in hub + }; + + // For group messages, we don't expect MessageService to be called based on ChatHub's current implementation + // If it were, we'd mock _mockMessageService.SendMessageAsync here. + + _mockClients.Setup(c => c.Group(groupName)).Returns(_mockClientProxy.Object); + + // Act + await _hub.SendMessageToGroup(groupId.ToString(), groupName, messageContent); + + // Assert + _mockClientProxy.Verify(c => c.ReceiveGroupMessage(It.Is(dto => + dto.GroupId == groupId && + dto.SenderId == senderId && + dto.SenderName == senderName && // Or mock User.Identity.Name if used + dto.Content == messageContent + )), Times.Once); + } + + + [Fact] + public async Task LeaveGroup_ValidRequest_CallsServiceAndRemovesFromGroup() + { + // Arrange + var memberId = Guid.NewGuid().ToString(); + var connectionId = "test-conn-id"; + var groupId = Guid.NewGuid(); + var groupName = "Test Group"; + SetupHubContext(memberId, connectionId); + + _mockGroupService.Setup(s => s.RemoveMemberFromGroupAsync(groupId, memberId, memberId)) + .ReturnsAsync(true); + + _mockClients.Setup(c => c.Caller).Returns(_mockClientProxy.Object); + + // Act + await _hub.LeaveGroup(groupId.ToString(), groupName); + + // Assert + _mockGroupService.Verify(s => s.RemoveMemberFromGroupAsync(groupId, memberId, memberId), Times.Once); + _mockGroups.Verify(g => g.RemoveFromGroupAsync(connectionId, groupName, default), Times.Once); + _mockClientProxy.Verify(c => c.LeftGroup(groupName), Times.Once); + } + + [Fact] + public async Task LeaveGroup_ServiceFails_DoesNotRemoveFromGroupOrNotify() + { + // Arrange + var memberId = Guid.NewGuid().ToString(); + var connectionId = "test-conn-id"; + var groupId = Guid.NewGuid(); + var groupName = "Test Group"; + SetupHubContext(memberId, connectionId); + + _mockGroupService.Setup(s => s.RemoveMemberFromGroupAsync(groupId, memberId, memberId)) + .ReturnsAsync(false); // Service indicates failure + + _mockClients.Setup(c => c.Caller).Returns(_mockClientProxy.Object); + + // Act + await _hub.LeaveGroup(groupId.ToString(), groupName); + + // Assert + _mockGroupService.Verify(s => s.RemoveMemberFromGroupAsync(groupId, memberId, memberId), Times.Once); + _mockGroups.Verify(g => g.RemoveFromGroupAsync(It.IsAny(), It.IsAny(), default), Times.Never); + _mockClientProxy.Verify(c => c.LeftGroup(It.IsAny()), Times.Never); + // Verify error notification to caller + _mockClientProxy.Verify(c => c.Error($"Failed to leave group {groupName}."), Times.Once); + } + } +} diff --git a/MinimalChat.Tests/GlobalUsings.cs b/MinimalChat.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/MinimalChat.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/MinimalChat.Tests/GroupChatControllerTests.cs b/MinimalChat.Tests/GroupChatControllerTests.cs new file mode 100644 index 0000000..30a1d77 --- /dev/null +++ b/MinimalChat.Tests/GroupChatControllerTests.cs @@ -0,0 +1,505 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using MinimalChat.API.Controllers; +using MinimalChat.Domain.DTOs; +using MinimalChat.Domain.Interfaces; +using Moq; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Xunit; + +namespace MinimalChat.Tests +{ + public class GroupChatControllerTests + { + private readonly Mock _mockGroupService; + private readonly Mock> _mockLogger; + private readonly GroupChatController _controller; + + public GroupChatControllerTests() + { + _mockGroupService = new Mock(); + _mockLogger = new Mock>(); + _controller = new GroupChatController(_mockGroupService.Object, _mockLogger.Object); + } + + private void SetupUserContext(string userId) + { + var claims = new List { new Claim(ClaimTypes.NameIdentifier, userId) }; + var identity = new ClaimsIdentity(claims, "TestAuthType"); + var claimsPrincipal = new ClaimsPrincipal(identity); + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = claimsPrincipal } + }; + } + + // 1. CreateGroup_ValidModel_ReturnsOk + [Fact] + public async Task CreateGroup_ValidModel_ReturnsOk() + { + // Arrange + var creatorUserId = Guid.NewGuid().ToString(); + SetupUserContext(creatorUserId); + var groupDto = new GroupDto { Name = "Test Group", Description = "A cool group" }; + var expectedResponse = new ResponseGroupDto { Id = Guid.NewGuid(), Name = groupDto.Name, Description = groupDto.Description, CreatedBy = Guid.Parse(creatorUserId) }; + _mockGroupService.Setup(s => s.CreateGroupAsync(groupDto, creatorUserId)).ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.CreateGroup(groupDto); + + // Assert + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(expectedResponse); + _mockGroupService.Verify(s => s.CreateGroupAsync(groupDto, creatorUserId), Times.Once); + } + + // 2. CreateGroup_InvalidModel_ReturnsBadRequest + [Fact] + public async Task CreateGroup_InvalidModel_ReturnsBadRequest() + { + // Arrange + var creatorUserId = Guid.NewGuid().ToString(); + SetupUserContext(creatorUserId); + _controller.ModelState.AddModelError("Name", "Name is required"); + var groupDto = new GroupDto { Description = "A group without a name" }; // Invalid + + // Act + var result = await _controller.CreateGroup(groupDto); + + // Assert + result.Should().BeOfType(); + _mockGroupService.Verify(s => s.CreateGroupAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + // 3. CreateGroup_CreatorNotFound_ReturnsNotFound + [Fact] + public async Task CreateGroup_CreatorNotFound_ReturnsNotFound() + { + // Arrange + var creatorUserId = Guid.NewGuid().ToString(); + SetupUserContext(creatorUserId); + var groupDto = new GroupDto { Name = "Test Group" }; + var exceptionMessage = $"Creator with ID {creatorUserId} not found."; + _mockGroupService.Setup(s => s.CreateGroupAsync(groupDto, creatorUserId)).ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.CreateGroup(groupDto); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.StatusCode.Should().Be(404); + notFoundResult.Value.Should().Be(exceptionMessage); + } + + // 4. UpdateGroup_ValidModel_ReturnsOk + [Fact] + public async Task UpdateGroup_ValidModel_ReturnsOk() + { + // Arrange + var updaterUserId = Guid.NewGuid().ToString(); + SetupUserContext(updaterUserId); + var groupId = Guid.NewGuid(); + var groupDto = new GroupDto { Name = "Updated Group", Description = "Updated description" }; + var expectedResponse = new ResponseGroupDto { Id = groupId, Name = groupDto.Name, Description = groupDto.Description, CreatedBy = Guid.Parse(updaterUserId) }; + _mockGroupService.Setup(s => s.UpdateGroupAsync(groupId, groupDto, updaterUserId)).ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.UpdateGroup(groupId, groupDto); + + // Assert + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(expectedResponse); + } + + // 5. UpdateGroup_InvalidModel_ReturnsBadRequest + [Fact] + public async Task UpdateGroup_InvalidModel_ReturnsBadRequest() + { + // Arrange + var updaterUserId = Guid.NewGuid().ToString(); + SetupUserContext(updaterUserId); + var groupId = Guid.NewGuid(); + _controller.ModelState.AddModelError("Name", "Name is required"); + var groupDto = new GroupDto { Description = "Updated description" }; // Invalid + + // Act + var result = await _controller.UpdateGroup(groupId, groupDto); + + // Assert + result.Should().BeOfType(); + _mockGroupService.Verify(s => s.UpdateGroupAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + // 6. UpdateGroup_GroupNotFound_ReturnsNotFound + [Fact] + public async Task UpdateGroup_GroupNotFound_ReturnsNotFound() + { + // Arrange + var updaterUserId = Guid.NewGuid().ToString(); + SetupUserContext(updaterUserId); + var groupId = Guid.NewGuid(); + var groupDto = new GroupDto { Name = "Updated Group" }; + var exceptionMessage = $"Group with ID {groupId} not found."; + _mockGroupService.Setup(s => s.UpdateGroupAsync(groupId, groupDto, updaterUserId)).ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.UpdateGroup(groupId, groupDto); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.StatusCode.Should().Be(404); + notFoundResult.Value.Should().Be(exceptionMessage); + } + + // 7. UpdateGroup_Unauthorized_ReturnsUnauthorized + [Fact] + public async Task UpdateGroup_Unauthorized_ReturnsUnauthorized() + { + // Arrange + var updaterUserId = Guid.NewGuid().ToString(); + SetupUserContext(updaterUserId); + var groupId = Guid.NewGuid(); + var groupDto = new GroupDto { Name = "Updated Group" }; + var exceptionMessage = "You are not authorized to update this group."; + _mockGroupService.Setup(s => s.UpdateGroupAsync(groupId, groupDto, updaterUserId)).ThrowsAsync(new UnauthorizedAccessException(exceptionMessage)); + + // Act + var result = await _controller.UpdateGroup(groupId, groupDto); + + // Assert + var unauthorizedResult = result.Should().BeOfType().Subject; + unauthorizedResult.StatusCode.Should().Be(401); + unauthorizedResult.Value.Should().Be(exceptionMessage); + } + + // 8. DeleteGroup_ValidRequest_ReturnsNoContent + [Fact] + public async Task DeleteGroup_ValidRequest_ReturnsNoContent() + { + // Arrange + var deleterUserId = Guid.NewGuid().ToString(); + SetupUserContext(deleterUserId); + var groupId = Guid.NewGuid(); + _mockGroupService.Setup(s => s.DeleteGroupAsync(groupId, deleterUserId)).ReturnsAsync(true); + + // Act + var result = await _controller.DeleteGroup(groupId); + + // Assert + result.Should().BeOfType(); + } + + // 9. DeleteGroup_GroupNotFound_ReturnsNotFound + [Fact] + public async Task DeleteGroup_GroupNotFound_ReturnsNotFound() + { + // Arrange + var deleterUserId = Guid.NewGuid().ToString(); + SetupUserContext(deleterUserId); + var groupId = Guid.NewGuid(); + var exceptionMessage = $"Group with ID {groupId} not found."; + _mockGroupService.Setup(s => s.DeleteGroupAsync(groupId, deleterUserId)).ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.DeleteGroup(groupId); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.StatusCode.Should().Be(404); + notFoundResult.Value.Should().Be(exceptionMessage); + } + + // 10. DeleteGroup_Unauthorized_ReturnsUnauthorized + [Fact] + public async Task DeleteGroup_Unauthorized_ReturnsUnauthorized() + { + // Arrange + var deleterUserId = Guid.NewGuid().ToString(); + SetupUserContext(deleterUserId); + var groupId = Guid.NewGuid(); + var exceptionMessage = "You are not authorized to delete this group."; + _mockGroupService.Setup(s => s.DeleteGroupAsync(groupId, deleterUserId)).ThrowsAsync(new UnauthorizedAccessException(exceptionMessage)); + + // Act + var result = await _controller.DeleteGroup(groupId); + + // Assert + var unauthorizedResult = result.Should().BeOfType().Subject; + unauthorizedResult.StatusCode.Should().Be(401); + unauthorizedResult.Value.Should().Be(exceptionMessage); + } + + // 11. DeleteGroup_ServiceReturnsFalse_ReturnsBadRequest + [Fact] + public async Task DeleteGroup_ServiceReturnsFalse_ReturnsBadRequest() + { + // Arrange + var deleterUserId = Guid.NewGuid().ToString(); + SetupUserContext(deleterUserId); + var groupId = Guid.NewGuid(); + _mockGroupService.Setup(s => s.DeleteGroupAsync(groupId, deleterUserId)).ReturnsAsync(false); + + // Act + var result = await _controller.DeleteGroup(groupId); + + // Assert + var badRequestResult = result.Should().BeOfType().Subject; + badRequestResult.Value.Should().Be("Failed to delete group."); + } + + // 12. AddMemberToGroup_ValidRequest_ReturnsOk + [Fact] + public async Task AddMemberToGroup_ValidRequest_ReturnsOk() + { + // Arrange + var adminUserId = Guid.NewGuid().ToString(); + SetupUserContext(adminUserId); + var groupId = Guid.NewGuid(); + var memberDto = new AddGroupMemberDto { UserId = Guid.NewGuid().ToString() }; + _mockGroupService.Setup(s => s.AddMemberToGroupAsync(groupId, memberDto, adminUserId)).ReturnsAsync(true); + + // Act + var result = await _controller.AddMemberToGroup(groupId, memberDto); + + // Assert + var okResult = result.Should().BeOfType().Subject; + okResult.Value.Should().Be("Member added successfully."); + } + + // 13. AddMemberToGroup_InvalidModel_ReturnsBadRequest + [Fact] + public async Task AddMemberToGroup_InvalidModel_ReturnsBadRequest() + { + // Arrange + var adminUserId = Guid.NewGuid().ToString(); + SetupUserContext(adminUserId); + var groupId = Guid.NewGuid(); + _controller.ModelState.AddModelError("UserId", "UserId is required"); + var memberDto = new AddGroupMemberDto { }; // Invalid + + // Act + var result = await _controller.AddMemberToGroup(groupId, memberDto); + + // Assert + result.Should().BeOfType(); + } + + // 14. AddMemberToGroup_GroupOrUserNotFound_ReturnsNotFound + [Fact] + public async Task AddMemberToGroup_GroupOrUserNotFound_ReturnsNotFound() + { + // Arrange + var adminUserId = Guid.NewGuid().ToString(); + SetupUserContext(adminUserId); + var groupId = Guid.NewGuid(); + var memberDto = new AddGroupMemberDto { UserId = Guid.NewGuid().ToString() }; + var exceptionMessage = "Group or User not found."; + _mockGroupService.Setup(s => s.AddMemberToGroupAsync(groupId, memberDto, adminUserId)).ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.AddMemberToGroup(groupId, memberDto); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.Value.Should().Be(exceptionMessage); + } + + // 15. AddMemberToGroup_UserAlreadyMember_ReturnsConflict + [Fact] + public async Task AddMemberToGroup_UserAlreadyMember_ReturnsConflict() + { + // Arrange + var adminUserId = Guid.NewGuid().ToString(); + SetupUserContext(adminUserId); + var groupId = Guid.NewGuid(); + var memberDto = new AddGroupMemberDto { UserId = Guid.NewGuid().ToString() }; + var exceptionMessage = "User is already a member."; + _mockGroupService.Setup(s => s.AddMemberToGroupAsync(groupId, memberDto, adminUserId)).ThrowsAsync(new InvalidOperationException(exceptionMessage)); + + // Act + var result = await _controller.AddMemberToGroup(groupId, memberDto); + + // Assert + var conflictResult = result.Should().BeOfType().Subject; + conflictResult.Value.Should().Be(exceptionMessage); + } + + // 16. AddMemberToGroup_Unauthorized_ReturnsUnauthorized + [Fact] + public async Task AddMemberToGroup_Unauthorized_ReturnsUnauthorized() + { + // Arrange + var nonAdminUserId = Guid.NewGuid().ToString(); + SetupUserContext(nonAdminUserId); + var groupId = Guid.NewGuid(); + var memberDto = new AddGroupMemberDto { UserId = Guid.NewGuid().ToString() }; + var exceptionMessage = "Not authorized to add members."; + _mockGroupService.Setup(s => s.AddMemberToGroupAsync(groupId, memberDto, nonAdminUserId)).ThrowsAsync(new UnauthorizedAccessException(exceptionMessage)); + + // Act + var result = await _controller.AddMemberToGroup(groupId, memberDto); + + // Assert + var unauthorizedResult = result.Should().BeOfType().Subject; + unauthorizedResult.Value.Should().Be(exceptionMessage); + } + + // 17. RemoveMemberFromGroup_ValidRequest_ReturnsOk + [Fact] + public async Task RemoveMemberFromGroup_ValidRequest_ReturnsOk() + { + // Arrange + var adminOrMemberUserId = Guid.NewGuid().ToString(); + SetupUserContext(adminOrMemberUserId); + var groupId = Guid.NewGuid(); + var memberId = Guid.NewGuid().ToString(); + _mockGroupService.Setup(s => s.RemoveMemberFromGroupAsync(groupId, memberId, adminOrMemberUserId)).ReturnsAsync(true); + + // Act + var result = await _controller.RemoveMemberFromGroup(groupId, memberId); + + // Assert + var okResult = result.Should().BeOfType().Subject; + okResult.Value.Should().Be("Member removed successfully."); + } + + // 18. RemoveMemberFromGroup_GroupOrMemberNotFound_ReturnsNotFound + [Fact] + public async Task RemoveMemberFromGroup_GroupOrMemberNotFound_ReturnsNotFound() + { + // Arrange + var adminOrMemberUserId = Guid.NewGuid().ToString(); + SetupUserContext(adminOrMemberUserId); + var groupId = Guid.NewGuid(); + var memberId = Guid.NewGuid().ToString(); + var exceptionMessage = "Group or Member not found."; + _mockGroupService.Setup(s => s.RemoveMemberFromGroupAsync(groupId, memberId, adminOrMemberUserId)).ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.RemoveMemberFromGroup(groupId, memberId); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.Value.Should().Be(exceptionMessage); + } + + // 19. RemoveMemberFromGroup_Unauthorized_ReturnsUnauthorized + [Fact] + public async Task RemoveMemberFromGroup_Unauthorized_ReturnsUnauthorized() + { + // Arrange + var nonAdminNonMemberUserId = Guid.NewGuid().ToString(); + SetupUserContext(nonAdminNonMemberUserId); + var groupId = Guid.NewGuid(); + var memberId = Guid.NewGuid().ToString(); + var exceptionMessage = "Not authorized to remove member."; + _mockGroupService.Setup(s => s.RemoveMemberFromGroupAsync(groupId, memberId, nonAdminNonMemberUserId)).ThrowsAsync(new UnauthorizedAccessException(exceptionMessage)); + + // Act + var result = await _controller.RemoveMemberFromGroup(groupId, memberId); + + // Assert + var unauthorizedResult = result.Should().BeOfType().Subject; + unauthorizedResult.Value.Should().Be(exceptionMessage); + } + + // 20. GetGroupById_GroupExists_ReturnsOk + [Fact] + public async Task GetGroupById_GroupExists_ReturnsOk() + { + // Arrange + var groupId = Guid.NewGuid(); + var expectedResponse = new ResponseGroupDto { Id = groupId, Name = "Test Group" }; + _mockGroupService.Setup(s => s.GetGroupByIdAsync(groupId)).ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.GetGroupById(groupId); + + // Assert + var okResult = result.Should().BeOfType().Subject; + okResult.Value.Should().BeEquivalentTo(expectedResponse); + } + + // 21. GetGroupById_GroupNotFound_ReturnsNotFound + [Fact] + public async Task GetGroupById_GroupNotFound_ReturnsNotFound() + { + // Arrange + var groupId = Guid.NewGuid(); + var exceptionMessage = "Group not found."; + _mockGroupService.Setup(s => s.GetGroupByIdAsync(groupId)).ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.GetGroupById(groupId); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.Value.Should().Be(exceptionMessage); + } + + // 22. GetAllGroups_ReturnsOkWithGroupList + [Fact] + public async Task GetAllGroups_ReturnsOkWithGroupList() + { + // Arrange + var expectedResponse = new List + { + new ResponseGroupDto { Id = Guid.NewGuid(), Name = "Group 1" }, + new ResponseGroupDto { Id = Guid.NewGuid(), Name = "Group 2" } + }; + _mockGroupService.Setup(s => s.GetAllGroupsAsync()).ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.GetAllGroups(); + + // Assert + var okResult = result.Should().BeOfType().Subject; + okResult.Value.Should().BeEquivalentTo(expectedResponse); + } + + // 23. GetGroupMembers_GroupExists_ReturnsOkWithMemberList + [Fact] + public async Task GetGroupMembers_GroupExists_ReturnsOkWithMemberList() + { + // Arrange + var groupId = Guid.NewGuid(); + var expectedResponse = new List + { + new UserDto { Id = Guid.NewGuid(), Name = "Member 1" }, + new UserDto { Id = Guid.NewGuid(), Name = "Member 2" } + }; + _mockGroupService.Setup(s => s.GetGroupMembersAsync(groupId)).ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.GetGroupMembers(groupId); + + // Assert + var okResult = result.Should().BeOfType().Subject; + okResult.Value.Should().BeEquivalentTo(expectedResponse); + } + + // 24. GetGroupMembers_GroupNotFound_ReturnsNotFound + [Fact] + public async Task GetGroupMembers_GroupNotFound_ReturnsNotFound() + { + // Arrange + var groupId = Guid.NewGuid(); + var exceptionMessage = "Group not found, so cannot retrieve members."; + _mockGroupService.Setup(s => s.GetGroupMembersAsync(groupId)).ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.GetGroupMembers(groupId); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.Value.Should().Be(exceptionMessage); + } + } +} diff --git a/MinimalChat.Tests/GroupServiceTests.cs b/MinimalChat.Tests/GroupServiceTests.cs new file mode 100644 index 0000000..cc6d71a --- /dev/null +++ b/MinimalChat.Tests/GroupServiceTests.cs @@ -0,0 +1,520 @@ +using AutoMapper; +using FluentAssertions; +using MinimalChat.Domain.DTOs; +using MinimalChat.Domain.Interfaces; +using MinimalChat.Domain.Models; +using MinmalChat.Data.Services; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Xunit; + +namespace MinimalChat.Tests +{ + public class GroupServiceTests + { + private readonly Mock> _mockGroupRepository; + private readonly Mock> _mockUserRepository; + private readonly Mock> _mockGroupMemberRepository; + private readonly Mock _mockMapper; + private readonly GroupService _groupService; + + public GroupServiceTests() + { + _mockGroupRepository = new Mock>(); + _mockUserRepository = new Mock>(); + _mockGroupMemberRepository = new Mock>(); + _mockMapper = new Mock(); + _groupService = new GroupService( + _mockGroupRepository.Object, + _mockUserRepository.Object, + _mockGroupMemberRepository.Object, + _mockMapper.Object + ); + } + + [Fact] + public async Task CreateGroupAsync_Success() + { + // Arrange + var creatorId = Guid.NewGuid().ToString(); + var groupDto = new GroupDto { Name = "Test Group", Description = "Test Description" }; + var creator = new User { Id = Guid.Parse(creatorId), Name = "Creator" }; + var group = new Group { Id = Guid.NewGuid(), Name = groupDto.Name, Description = groupDto.Description, CreatedBy = Guid.Parse(creatorId) }; + var responseGroupDto = new ResponseGroupDto { Id = group.Id, Name = group.Name, Description = group.Description, CreatedBy = group.CreatedBy }; + + _mockUserRepository.Setup(repo => repo.GetByIdAsync(Guid.Parse(creatorId))).ReturnsAsync(creator); + _mockMapper.Setup(m => m.Map(groupDto)).Returns(group); + _mockGroupRepository.Setup(repo => repo.AddAsync(It.IsAny())).Returns(Task.CompletedTask); + _mockGroupMemberRepository.Setup(repo => repo.AddAsync(It.IsAny())).Returns(Task.CompletedTask); + _mockMapper.Setup(m => m.Map(It.IsAny())).Returns(responseGroupDto); + + // Act + var result = await _groupService.CreateGroupAsync(groupDto, creatorId); + + // Assert + _mockUserRepository.Verify(repo => repo.GetByIdAsync(Guid.Parse(creatorId)), Times.Once); + _mockGroupRepository.Verify(repo => repo.AddAsync(It.Is(g => g.Name == groupDto.Name && g.CreatedBy == Guid.Parse(creatorId))), Times.Once); + _mockGroupMemberRepository.Verify(repo => repo.AddAsync(It.Is(gm => gm.GroupId == group.Id && gm.UserId == Guid.Parse(creatorId))), Times.Once); + result.Should().NotBeNull(); + result.Should().BeEquivalentTo(responseGroupDto); + } + + [Fact] + public async Task CreateGroupAsync_CreatorNotFound() + { + // Arrange + var creatorId = Guid.NewGuid().ToString(); + var groupDto = new GroupDto { Name = "Test Group" }; + _mockUserRepository.Setup(repo => repo.GetByIdAsync(Guid.Parse(creatorId))).ReturnsAsync((User)null); + + // Act + Func act = async () => await _groupService.CreateGroupAsync(groupDto, creatorId); + + // Assert + await act.Should().ThrowAsync().WithMessage($"Creator with ID {creatorId} not found."); + } + + [Fact] + public async Task UpdateGroupAsync_Success() + { + // Arrange + var groupId = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); + var groupDto = new GroupDto { Name = "Updated Group Name", Description = "Updated Description" }; + var existingGroup = new Group { Id = groupId, Name = "Old Name", Description = "Old Desc", CreatedBy = Guid.Parse(currentUserId) }; + var updatedGroup = new Group { Id = groupId, Name = groupDto.Name, Description = groupDto.Description, CreatedBy = Guid.Parse(currentUserId) }; + var responseGroupDto = new ResponseGroupDto { Id = groupId, Name = updatedGroup.Name, Description = updatedGroup.Description, CreatedBy = Guid.Parse(currentUserId) }; + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(existingGroup); + _mockMapper.Setup(m => m.Map(groupDto, existingGroup)).Returns(updatedGroup); // Simulate AutoMapper updating existingGroup + _mockGroupRepository.Setup(repo => repo.UpdateAsync(It.IsAny())).Returns(Task.CompletedTask); + _mockMapper.Setup(m => m.Map(It.IsAny())).Returns(responseGroupDto); + + + // Act + var result = await _groupService.UpdateGroupAsync(groupId, groupDto, currentUserId); + + // Assert + _mockGroupRepository.Verify(repo => repo.GetByIdAsync(groupId), Times.Once); + _mockGroupRepository.Verify(repo => repo.UpdateAsync(It.Is(g => g.Name == groupDto.Name && g.Description == groupDto.Description)), Times.Once); + result.Should().NotBeNull(); + result.Should().BeEquivalentTo(responseGroupDto); + } + + [Fact] + public async Task UpdateGroupAsync_GroupNotFound() + { + // Arrange + var groupId = Guid.NewGuid(); + var groupDto = new GroupDto { Name = "Updated Name" }; + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync((Group)null); + + // Act + Func act = async () => await _groupService.UpdateGroupAsync(groupId, groupDto, Guid.NewGuid().ToString()); + + // Assert + await act.Should().ThrowAsync().WithMessage($"Group with ID {groupId} not found."); + } + + [Fact] + public async Task UpdateGroupAsync_Unauthorized() + { + // Arrange + var groupId = Guid.NewGuid(); + var ownerUserId = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); // Different user + var groupDto = new GroupDto { Name = "Updated Name" }; + var existingGroup = new Group { Id = groupId, Name = "Old Name", CreatedBy = ownerUserId }; + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(existingGroup); + + // Act + Func act = async () => await _groupService.UpdateGroupAsync(groupId, groupDto, currentUserId); + + // Assert + await act.Should().ThrowAsync().WithMessage("You are not authorized to update this group."); + } + + [Fact] + public async Task DeleteGroupAsync_Success() + { + // Arrange + var groupId = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); + var group = new Group { Id = groupId, CreatedBy = Guid.Parse(currentUserId) }; + var groupMembers = new List { new GroupMember { Id = Guid.NewGuid(), GroupId = groupId, UserId = Guid.NewGuid() } }; + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(group); + _mockGroupMemberRepository.Setup(repo => repo.GetAllAsync(It.IsAny>>())).ReturnsAsync(groupMembers); + _mockGroupMemberRepository.Setup(repo => repo.DeleteAsync(It.IsAny())).Returns(Task.CompletedTask); + _mockGroupRepository.Setup(repo => repo.DeleteAsync(group)).Returns(Task.CompletedTask); + + // Act + var result = await _groupService.DeleteGroupAsync(groupId, currentUserId); + + // Assert + _mockGroupRepository.Verify(repo => repo.GetByIdAsync(groupId), Times.Once); + _mockGroupMemberRepository.Verify(repo => repo.GetAllAsync(It.IsAny>>()), Times.Once); + _mockGroupMemberRepository.Verify(repo => repo.DeleteAsync(It.IsAny()), Times.Exactly(groupMembers.Count)); + _mockGroupRepository.Verify(repo => repo.DeleteAsync(group), Times.Once); + result.Should().BeTrue(); + } + + [Fact] + public async Task DeleteGroupAsync_GroupNotFound() + { + // Arrange + var groupId = Guid.NewGuid(); + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync((Group)null); + + // Act + Func act = async () => await _groupService.DeleteGroupAsync(groupId, Guid.NewGuid().ToString()); + + // Assert + await act.Should().ThrowAsync().WithMessage($"Group with ID {groupId} not found."); + } + + [Fact] + public async Task DeleteGroupAsync_Unauthorized() + { + // Arrange + var groupId = Guid.NewGuid(); + var ownerUserId = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); // Different user + var group = new Group { Id = groupId, CreatedBy = ownerUserId }; + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(group); + + // Act + Func act = async () => await _groupService.DeleteGroupAsync(groupId, currentUserId); + + // Assert + await act.Should().ThrowAsync().WithMessage("You are not authorized to delete this group."); + } + + [Fact] + public async Task AddMemberToGroupAsync_Success() + { + // Arrange + var groupId = Guid.NewGuid(); + var userIdToAdd = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); // Admin + var addGroupMemberDto = new AddGroupMemberDto { UserId = userIdToAdd.ToString() }; + var group = new Group { Id = groupId, CreatedBy = Guid.Parse(currentUserId) }; + var userToAdd = new User { Id = userIdToAdd }; + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(group); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(userIdToAdd)).ReturnsAsync(userToAdd); + _mockGroupMemberRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())).ReturnsAsync((GroupMember)null); // Not already a member + _mockGroupMemberRepository.Setup(repo => repo.AddAsync(It.IsAny())).Returns(Task.CompletedTask); + + // Act + var result = await _groupService.AddMemberToGroupAsync(groupId, addGroupMemberDto, currentUserId); + + // Assert + _mockGroupRepository.Verify(repo => repo.GetByIdAsync(groupId), Times.Once); + _mockUserRepository.Verify(repo => repo.GetByIdAsync(userIdToAdd), Times.Once); + _mockGroupMemberRepository.Verify(repo => repo.FirstOrDefaultAsync(It.IsAny>>()), Times.Once); + _mockGroupMemberRepository.Verify(repo => repo.AddAsync(It.Is(gm => gm.GroupId == groupId && gm.UserId == userIdToAdd)), Times.Once); + result.Should().BeTrue(); + } + + [Fact] + public async Task AddMemberToGroupAsync_GroupNotFound() + { + // Arrange + var groupId = Guid.NewGuid(); + var addGroupMemberDto = new AddGroupMemberDto { UserId = Guid.NewGuid().ToString() }; + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync((Group)null); + + // Act + Func act = async () => await _groupService.AddMemberToGroupAsync(groupId, addGroupMemberDto, Guid.NewGuid().ToString()); + + // Assert + await act.Should().ThrowAsync().WithMessage($"Group with ID {groupId} not found."); + } + + [Fact] + public async Task AddMemberToGroupAsync_UserToAddNotFound() + { + // Arrange + var groupId = Guid.NewGuid(); + var userIdToAdd = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); + var addGroupMemberDto = new AddGroupMemberDto { UserId = userIdToAdd.ToString() }; + var group = new Group { Id = groupId, CreatedBy = Guid.Parse(currentUserId) }; + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(group); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(userIdToAdd)).ReturnsAsync((User)null); + + // Act + Func act = async () => await _groupService.AddMemberToGroupAsync(groupId, addGroupMemberDto, currentUserId); + + // Assert + await act.Should().ThrowAsync().WithMessage($"User with ID {userIdToAdd} not found."); + } + + [Fact] + public async Task AddMemberToGroupAsync_UserAlreadyMember() + { + // Arrange + var groupId = Guid.NewGuid(); + var userIdToAdd = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); + var addGroupMemberDto = new AddGroupMemberDto { UserId = userIdToAdd.ToString() }; + var group = new Group { Id = groupId, CreatedBy = Guid.Parse(currentUserId) }; + var userToAdd = new User { Id = userIdToAdd }; + var existingMember = new GroupMember { GroupId = groupId, UserId = userIdToAdd }; + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(group); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(userIdToAdd)).ReturnsAsync(userToAdd); + _mockGroupMemberRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())).ReturnsAsync(existingMember); + + // Act + Func act = async () => await _groupService.AddMemberToGroupAsync(groupId, addGroupMemberDto, currentUserId); + + // Assert + await act.Should().ThrowAsync().WithMessage($"User {userIdToAdd} is already a member of group {groupId}."); + } + + [Fact] + public async Task AddMemberToGroupAsync_UnauthorizedNotAdmin() + { + // Arrange + var groupId = Guid.NewGuid(); + var userIdToAdd = Guid.NewGuid(); + var groupAdminId = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); // Not admin + var addGroupMemberDto = new AddGroupMemberDto { UserId = userIdToAdd.ToString() }; + var group = new Group { Id = groupId, CreatedBy = groupAdminId }; + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(group); + + // Act + Func act = async () => await _groupService.AddMemberToGroupAsync(groupId, addGroupMemberDto, currentUserId); + + // Assert + await act.Should().ThrowAsync().WithMessage("You are not authorized to add members to this group."); + } + + [Fact] + public async Task RemoveMemberFromGroupAsync_Success_AdminRemoves() + { + // Arrange + var groupId = Guid.NewGuid(); + var memberIdToRemove = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); // Admin + var group = new Group { Id = groupId, CreatedBy = Guid.Parse(currentUserId) }; + var userToRemove = new User { Id = memberIdToRemove }; + var groupMemberEntry = new GroupMember { Id = Guid.NewGuid(), GroupId = groupId, UserId = memberIdToRemove }; + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(group); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(memberIdToRemove)).ReturnsAsync(userToRemove); + _mockGroupMemberRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())).ReturnsAsync(groupMemberEntry); + _mockGroupMemberRepository.Setup(repo => repo.DeleteAsync(groupMemberEntry)).Returns(Task.CompletedTask); + + // Act + var result = await _groupService.RemoveMemberFromGroupAsync(groupId, memberIdToRemove.ToString(), currentUserId); + + // Assert + _mockGroupRepository.Verify(repo => repo.GetByIdAsync(groupId), Times.Once); + _mockUserRepository.Verify(repo => repo.GetByIdAsync(memberIdToRemove), Times.Once); + _mockGroupMemberRepository.Verify(repo => repo.FirstOrDefaultAsync(It.IsAny>>()), Times.Once); + _mockGroupMemberRepository.Verify(repo => repo.DeleteAsync(groupMemberEntry), Times.Once); + result.Should().BeTrue(); + } + + [Fact] + public async Task RemoveMemberFromGroupAsync_Success_MemberRemovesSelf() + { + // Arrange + var groupId = Guid.NewGuid(); + var memberIdToRemove = Guid.NewGuid(); + var currentUserId = memberIdToRemove.ToString(); // Member removing self + var groupAdminId = Guid.NewGuid(); // Different from current user + var group = new Group { Id = groupId, CreatedBy = groupAdminId }; + var userToRemove = new User { Id = memberIdToRemove }; + var groupMemberEntry = new GroupMember { Id = Guid.NewGuid(), GroupId = groupId, UserId = memberIdToRemove }; + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(group); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(memberIdToRemove)).ReturnsAsync(userToRemove); + _mockGroupMemberRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())).ReturnsAsync(groupMemberEntry); + _mockGroupMemberRepository.Setup(repo => repo.DeleteAsync(groupMemberEntry)).Returns(Task.CompletedTask); + + // Act + var result = await _groupService.RemoveMemberFromGroupAsync(groupId, memberIdToRemove.ToString(), currentUserId); + + // Assert + result.Should().BeTrue(); + } + + + [Fact] + public async Task RemoveMemberFromGroupAsync_GroupNotFound() + { + // Arrange + var groupId = Guid.NewGuid(); + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync((Group)null); + + // Act + Func act = async () => await _groupService.RemoveMemberFromGroupAsync(groupId, Guid.NewGuid().ToString(), Guid.NewGuid().ToString()); + + // Assert + await act.Should().ThrowAsync().WithMessage($"Group with ID {groupId} not found."); + } + + [Fact] + public async Task RemoveMemberFromGroupAsync_MemberNotFoundInGroup() + { + // Arrange + var groupId = Guid.NewGuid(); + var memberIdToRemove = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); + var group = new Group { Id = groupId, CreatedBy = Guid.Parse(currentUserId) }; // Current user is admin + var userToRemove = new User { Id = memberIdToRemove }; + + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(group); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(memberIdToRemove)).ReturnsAsync(userToRemove); + _mockGroupMemberRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())).ReturnsAsync((GroupMember)null); // Member not in group + + // Act + Func act = async () => await _groupService.RemoveMemberFromGroupAsync(groupId, memberIdToRemove.ToString(), currentUserId); + + // Assert + await act.Should().ThrowAsync().WithMessage($"Member with ID {memberIdToRemove} not found in group {groupId}."); + } + + [Fact] + public async Task RemoveMemberFromGroupAsync_Unauthorized() + { + // Arrange + var groupId = Guid.NewGuid(); + var memberIdToRemove = Guid.NewGuid(); + var groupAdminId = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); // Neither admin nor member being removed + var group = new Group { Id = groupId, CreatedBy = groupAdminId }; + var userToRemove = new User { Id = memberIdToRemove }; + var groupMemberEntry = new GroupMember { GroupId = groupId, UserId = memberIdToRemove }; + + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(group); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(memberIdToRemove)).ReturnsAsync(userToRemove); + _mockGroupMemberRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())).ReturnsAsync(groupMemberEntry); + + // Act + Func act = async () => await _groupService.RemoveMemberFromGroupAsync(groupId, memberIdToRemove.ToString(), currentUserId); + + // Assert + await act.Should().ThrowAsync().WithMessage("You are not authorized to remove this member."); + } + + [Fact] + public async Task GetGroupByIdAsync_GroupFound() + { + // Arrange + var groupId = Guid.NewGuid(); + var group = new Group { Id = groupId, Name = "Test Group" }; + var responseGroupDto = new ResponseGroupDto { Id = groupId, Name = "Test Group" }; + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(group); + _mockMapper.Setup(m => m.Map(group)).Returns(responseGroupDto); + + // Act + var result = await _groupService.GetGroupByIdAsync(groupId); + + // Assert + _mockGroupRepository.Verify(repo => repo.GetByIdAsync(groupId), Times.Once); + result.Should().NotBeNull(); + result.Should().BeEquivalentTo(responseGroupDto); + } + + [Fact] + public async Task GetGroupByIdAsync_GroupNotFound() + { + // Arrange + var groupId = Guid.NewGuid(); + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync((Group)null); + + // Act + Func act = async () => await _groupService.GetGroupByIdAsync(groupId); + + // Assert + await act.Should().ThrowAsync().WithMessage($"Group with ID {groupId} not found."); + } + + [Fact] + public async Task GetAllGroupsAsync_ReturnsGroups() + { + // Arrange + var groups = new List + { + new Group { Id = Guid.NewGuid(), Name = "Group 1" }, + new Group { Id = Guid.NewGuid(), Name = "Group 2" } + }; + var responseGroupDtos = groups.Select(g => new ResponseGroupDto { Id = g.Id, Name = g.Name }).ToList(); + + _mockGroupRepository.Setup(repo => repo.GetAllAsync()).ReturnsAsync(groups); + _mockMapper.Setup(m => m.Map>(groups)).Returns(responseGroupDtos); + + // Act + var result = await _groupService.GetAllGroupsAsync(); + + // Assert + _mockGroupRepository.Verify(repo => repo.GetAllAsync(), Times.Once); + result.Should().NotBeNull(); + result.Should().HaveCount(groups.Count); + result.Should().BeEquivalentTo(responseGroupDtos); + } + + [Fact] + public async Task GetGroupMembersAsync_GroupFound_ReturnsMembers() + { + // Arrange + var groupId = Guid.NewGuid(); + var group = new Group { Id = groupId, Name = "Test Group" }; + var memberUserIds = new List { Guid.NewGuid(), Guid.NewGuid() }; + var groupMembers = memberUserIds.Select(uid => new GroupMember { GroupId = groupId, UserId = uid }).ToList(); + var users = memberUserIds.Select(uid => new User { Id = uid, Name = $"User {uid}" }).ToList(); + var userDtos = users.Select(u => new UserDto { Id = u.Id, Name = u.Name }).ToList(); + + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync(group); + _mockGroupMemberRepository.Setup(repo => repo.GetAllAsync(It.IsAny>>())) + .ReturnsAsync(groupMembers); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(It.IsAny())) + .ReturnsAsync((Guid id) => users.FirstOrDefault(u => u.Id == id)); + _mockMapper.Setup(m => m.Map(It.IsAny())) + .Returns((User u) => userDtos.FirstOrDefault(dto => dto.Id == u.Id)); + + + // Act + var result = await _groupService.GetGroupMembersAsync(groupId); + + // Assert + _mockGroupRepository.Verify(repo => repo.GetByIdAsync(groupId), Times.Once); + _mockGroupMemberRepository.Verify(repo => repo.GetAllAsync(It.IsAny>>()), Times.Once); + _mockUserRepository.Verify(repo => repo.GetByIdAsync(It.IsAny()), Times.Exactly(memberUserIds.Count)); + result.Should().NotBeNull(); + result.Should().HaveCount(memberUserIds.Count); + result.Should().BeEquivalentTo(userDtos); + } + + [Fact] + public async Task GetGroupMembersAsync_GroupNotFound() + { + // Arrange + var groupId = Guid.NewGuid(); + _mockGroupRepository.Setup(repo => repo.GetByIdAsync(groupId)).ReturnsAsync((Group)null); + + // Act + Func act = async () => await _groupService.GetGroupMembersAsync(groupId); + + // Assert + await act.Should().ThrowAsync().WithMessage($"Group with ID {groupId} not found."); + } + } +} diff --git a/MinimalChat.Tests/LogServiceTests.cs b/MinimalChat.Tests/LogServiceTests.cs new file mode 100644 index 0000000..620be59 --- /dev/null +++ b/MinimalChat.Tests/LogServiceTests.cs @@ -0,0 +1,201 @@ +using AutoMapper; +using FluentAssertions; +using MinimalChat.Domain.DTOs; +using MinimalChat.Domain.Interfaces; +using MinimalChat.Domain.Models; +using MinmalChat.Data.Services; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Xunit; + +namespace MinimalChat.Tests +{ + public class LogServiceTests + { + private readonly Mock> _mockLogRepository; + private readonly Mock _mockMapper; + private readonly LogService _logService; + + public LogServiceTests() + { + _mockLogRepository = new Mock>(); + _mockMapper = new Mock(); + _logService = new LogService(_mockLogRepository.Object, _mockMapper.Object); + } + + [Fact] + public async Task LogRequestAsync_Success() + { + // Arrange + var requestLog = new RequestLog + { + Id = Guid.NewGuid(), + Path = "/api/test", + Method = "GET", + Timestamp = DateTime.UtcNow, + RequestBody = "{}", + StatusCode = 200 + }; + + _mockLogRepository.Setup(repo => repo.AddAsync(It.IsAny())).Returns(Task.CompletedTask); + + // Act + await _logService.LogRequestAsync(requestLog); + + // Assert + _mockLogRepository.Verify(repo => repo.AddAsync(requestLog), Times.Once); + } + + [Fact] + public async Task GetLogsAsync_NoDates_ReturnsAllLogs() + { + // Arrange + var logsFromRepo = new List + { + new RequestLog { Id = Guid.NewGuid(), Path = "/api/test1", Timestamp = DateTime.UtcNow.AddHours(-1) }, + new RequestLog { Id = Guid.NewGuid(), Path = "/api/test2", Timestamp = DateTime.UtcNow } + }; + var expectedLogDtos = logsFromRepo.Select(log => new LogDto { Id = log.Id, Path = log.Path, Timestamp = log.Timestamp }).ToList(); + + _mockLogRepository.Setup(repo => repo.GetAllAsync(It.IsAny>>())) + .ReturnsAsync(logsFromRepo); // Predicate will be null, so all are returned + _mockMapper.Setup(m => m.Map>(logsFromRepo)).Returns(expectedLogDtos); + + // Act + var result = await _logService.GetLogsAsync(null, null); + + // Assert + _mockLogRepository.Verify(repo => repo.GetAllAsync(It.IsAny>>()), Times.Once); + _mockMapper.Verify(m => m.Map>(logsFromRepo), Times.Once); + result.Should().NotBeNull(); + result.Should().BeEquivalentTo(expectedLogDtos); + } + + [Fact] + public async Task GetLogsAsync_WithStartDate_ReturnsLogsFromStartDate() + { + // Arrange + var startDate = DateTime.UtcNow.AddHours(-1); + var allLogs = new List + { + new RequestLog { Id = Guid.NewGuid(), Path = "/api/before", Timestamp = startDate.AddHours(-1) }, + new RequestLog { Id = Guid.NewGuid(), Path = "/api/on_start", Timestamp = startDate }, + new RequestLog { Id = Guid.NewGuid(), Path = "/api/after", Timestamp = startDate.AddHours(1) } + }; + var filteredLogsFromRepo = allLogs.Where(log => log.Timestamp >= startDate).ToList(); + var expectedLogDtos = filteredLogsFromRepo.Select(log => new LogDto { Id = log.Id, Path = log.Path, Timestamp = log.Timestamp }).ToList(); + + _mockLogRepository.Setup(repo => repo.GetAllAsync(It.IsAny>>())) + .ReturnsAsync((Expression> predicate) => allLogs.Where(predicate.Compile()).ToList()); + _mockMapper.Setup(m => m.Map>(It.Is>(logs => logs.SequenceEqual(filteredLogsFromRepo)))) + .Returns(expectedLogDtos); + + // Act + var result = await _logService.GetLogsAsync(startDate, null); + + // Assert + _mockLogRepository.Verify(repo => repo.GetAllAsync(It.IsAny>>()), Times.Once); + _mockMapper.Verify(m => m.Map>(filteredLogsFromRepo), Times.Once); + result.Should().NotBeNull(); + result.Should().BeEquivalentTo(expectedLogDtos); + result.All(log => log.Timestamp >= startDate).Should().BeTrue(); + } + + [Fact] + public async Task GetLogsAsync_WithEndDate_ReturnsLogsUntilEndDate() + { + // Arrange + var endDate = DateTime.UtcNow.AddHours(1); + var allLogs = new List + { + new RequestLog { Id = Guid.NewGuid(), Path = "/api/before", Timestamp = endDate.AddHours(-1) }, + new RequestLog { Id = Guid.NewGuid(), Path = "/api/on_end", Timestamp = endDate }, + new RequestLog { Id = Guid.NewGuid(), Path = "/api/after", Timestamp = endDate.AddHours(1) } + }; + var filteredLogsFromRepo = allLogs.Where(log => log.Timestamp <= endDate).ToList(); + var expectedLogDtos = filteredLogsFromRepo.Select(log => new LogDto { Id = log.Id, Path = log.Path, Timestamp = log.Timestamp }).ToList(); + + _mockLogRepository.Setup(repo => repo.GetAllAsync(It.IsAny>>())) + .ReturnsAsync((Expression> predicate) => allLogs.Where(predicate.Compile()).ToList()); + _mockMapper.Setup(m => m.Map>(It.Is>(logs => logs.SequenceEqual(filteredLogsFromRepo)))) + .Returns(expectedLogDtos); + + // Act + var result = await _logService.GetLogsAsync(null, endDate); + + // Assert + _mockLogRepository.Verify(repo => repo.GetAllAsync(It.IsAny>>()), Times.Once); + _mockMapper.Verify(m => m.Map>(filteredLogsFromRepo), Times.Once); + result.Should().NotBeNull(); + result.Should().BeEquivalentTo(expectedLogDtos); + result.All(log => log.Timestamp <= endDate).Should().BeTrue(); + } + + [Fact] + public async Task GetLogsAsync_WithBothDates_ReturnsLogsInDateRange() + { + // Arrange + var startDate = DateTime.UtcNow.AddDays(-1); + var endDate = DateTime.UtcNow; + var allLogs = new List + { + new RequestLog { Id = Guid.NewGuid(), Path = "/api/too_early", Timestamp = startDate.AddDays(-1) }, + new RequestLog { Id = Guid.NewGuid(), Path = "/api/on_start", Timestamp = startDate }, + new RequestLog { Id = Guid.NewGuid(), Path = "/api/in_range", Timestamp = startDate.AddHours(12) }, + new RequestLog { Id = Guid.NewGuid(), Path = "/api/on_end", Timestamp = endDate }, + new RequestLog { Id = Guid.NewGuid(), Path = "/api/too_late", Timestamp = endDate.AddDays(1) } + }; + var filteredLogsFromRepo = allLogs.Where(log => log.Timestamp >= startDate && log.Timestamp <= endDate).ToList(); + var expectedLogDtos = filteredLogsFromRepo.Select(log => new LogDto { Id = log.Id, Path = log.Path, Timestamp = log.Timestamp }).ToList(); + + _mockLogRepository.Setup(repo => repo.GetAllAsync(It.IsAny>>())) + .ReturnsAsync((Expression> predicate) => allLogs.Where(predicate.Compile()).ToList()); + _mockMapper.Setup(m => m.Map>(It.Is>(logs => logs.SequenceEqual(filteredLogsFromRepo)))) + .Returns(expectedLogDtos); + + // Act + var result = await _logService.GetLogsAsync(startDate, endDate); + + // Assert + _mockLogRepository.Verify(repo => repo.GetAllAsync(It.IsAny>>()), Times.Once); + _mockMapper.Verify(m => m.Map>(filteredLogsFromRepo), Times.Once); + result.Should().NotBeNull(); + result.Should().BeEquivalentTo(expectedLogDtos); + result.All(log => log.Timestamp >= startDate && log.Timestamp <= endDate).Should().BeTrue(); + } + + [Fact] + public async Task GetLogsAsync_NoLogsMatch_ReturnsEmptyList() + { + // Arrange + var startDate = DateTime.UtcNow.AddDays(1); // Future start date to ensure no logs match + var endDate = DateTime.UtcNow.AddDays(2); + var allLogs = new List // Logs from the past + { + new RequestLog { Id = Guid.NewGuid(), Path = "/api/past_log1", Timestamp = DateTime.UtcNow.AddDays(-1) }, + new RequestLog { Id = Guid.NewGuid(), Path = "/api/past_log2", Timestamp = DateTime.UtcNow.AddDays(-2) } + }; + // The predicate will filter allLogs to an empty list + var filteredLogsFromRepo = new List(); + var expectedLogDtos = new List(); // Empty list + + _mockLogRepository.Setup(repo => repo.GetAllAsync(It.IsAny>>())) + .ReturnsAsync((Expression> predicate) => allLogs.Where(predicate.Compile()).ToList()); + _mockMapper.Setup(m => m.Map>(It.Is>(logs => !logs.Any()))) + .Returns(expectedLogDtos); + + // Act + var result = await _logService.GetLogsAsync(startDate, endDate); + + // Assert + _mockLogRepository.Verify(repo => repo.GetAllAsync(It.IsAny>>()), Times.Once); + _mockMapper.Verify(m => m.Map>(filteredLogsFromRepo), Times.Once); + result.Should().NotBeNull(); + result.Should().BeEmpty(); + } + } +} diff --git a/MinimalChat.Tests/LogsControllerTests.cs b/MinimalChat.Tests/LogsControllerTests.cs new file mode 100644 index 0000000..0439d96 --- /dev/null +++ b/MinimalChat.Tests/LogsControllerTests.cs @@ -0,0 +1,146 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using MinimalChat.API.Controllers; +using MinimalChat.Domain.DTOs; +using MinimalChat.Domain.Interfaces; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace MinimalChat.Tests +{ + public class LogsControllerTests + { + private readonly Mock _mockLogService; + private readonly Mock> _mockLogger; + private readonly LogsController _controller; + + public LogsControllerTests() + { + _mockLogService = new Mock(); + _mockLogger = new Mock>(); + _controller = new LogsController(_mockLogService.Object, _mockLogger.Object); + } + + [Fact] + public async Task GetLogs_NoDates_ReturnsOkWithAllLogs() + { + // Arrange + var expectedLogs = new List + { + new LogDto { Path = "/api/test1", Timestamp = DateTime.UtcNow.AddHours(-1) }, + new LogDto { Path = "/api/test2", Timestamp = DateTime.UtcNow } + }; + _mockLogService.Setup(s => s.GetLogsAsync(null, null)).ReturnsAsync(expectedLogs); + + // Act + var result = await _controller.GetLogs(null, null); + + // Assert + _mockLogService.Verify(s => s.GetLogsAsync(null, null), Times.Once); + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(expectedLogs); + } + + [Fact] + public async Task GetLogs_WithStartDate_ReturnsOkWithFilteredLogs() + { + // Arrange + var startDate = DateTime.UtcNow.Date; + var expectedLogs = new List + { + new LogDto { Path = "/api/test_start", Timestamp = startDate.AddHours(1) } + }; + _mockLogService.Setup(s => s.GetLogsAsync(startDate, null)).ReturnsAsync(expectedLogs); + + // Act + var result = await _controller.GetLogs(startDate, null); + + // Assert + _mockLogService.Verify(s => s.GetLogsAsync(startDate, null), Times.Once); + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(expectedLogs); + } + + [Fact] + public async Task GetLogs_WithEndDate_ReturnsOkWithFilteredLogs() + { + // Arrange + var endDate = DateTime.UtcNow.Date.AddDays(1).AddTicks(-1); // End of the day + var expectedLogs = new List + { + new LogDto { Path = "/api/test_end", Timestamp = endDate.AddHours(-1) } + }; + _mockLogService.Setup(s => s.GetLogsAsync(null, endDate)).ReturnsAsync(expectedLogs); + + // Act + var result = await _controller.GetLogs(null, endDate); + + // Assert + _mockLogService.Verify(s => s.GetLogsAsync(null, endDate), Times.Once); + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(expectedLogs); + } + + [Fact] + public async Task GetLogs_WithBothDates_ReturnsOkWithFilteredLogs() + { + // Arrange + var startDate = DateTime.UtcNow.Date; + var endDate = DateTime.UtcNow.Date.AddDays(1).AddTicks(-1); + var expectedLogs = new List + { + new LogDto { Path = "/api/test_both", Timestamp = startDate.AddHours(5) } + }; + _mockLogService.Setup(s => s.GetLogsAsync(startDate, endDate)).ReturnsAsync(expectedLogs); + + // Act + var result = await _controller.GetLogs(startDate, endDate); + + // Assert + _mockLogService.Verify(s => s.GetLogsAsync(startDate, endDate), Times.Once); + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(expectedLogs); + } + + [Fact] + public async Task GetLogs_ServiceThrowsException_ReturnsInternalServerError() + { + // Arrange + var exceptionMessage = "Service layer error"; + _mockLogService.Setup(s => s.GetLogsAsync(null, null)).ThrowsAsync(new Exception(exceptionMessage)); + + // Act + var result = await _controller.GetLogs(null, null); + + // Assert + var objectResult = result.Should().BeOfType().Subject; + objectResult.StatusCode.Should().Be(500); + objectResult.Value.Should().Be($"An error occurred while fetching logs: {exceptionMessage}"); + } + + [Fact] + public async Task GetLogs_InvalidDateRange_StartDateAfterEndDate_ReturnsBadRequest() + { + // Arrange + var startDate = DateTime.UtcNow.Date.AddDays(1); + var endDate = DateTime.UtcNow.Date; // Start date is after end date + + // Act + var result = await _controller.GetLogs(startDate, endDate); + + // Assert + _mockLogService.Verify(s => s.GetLogsAsync(It.IsAny(), It.IsAny()), Times.Never); + var badRequestResult = result.Should().BeOfType().Subject; + badRequestResult.StatusCode.Should().Be(400); + badRequestResult.Value.Should().Be("Start date cannot be after end date."); + } + } +} diff --git a/MinimalChat.Tests/MessageServiceTests.cs b/MinimalChat.Tests/MessageServiceTests.cs new file mode 100644 index 0000000..9167c8d --- /dev/null +++ b/MinimalChat.Tests/MessageServiceTests.cs @@ -0,0 +1,436 @@ +using AutoMapper; +using FluentAssertions; +using MinimalChat.Domain.DTOs; +using MinimalChat.Domain.Interfaces; +using MinimalChat.Domain.Models; +using MinmalChat.Data.Services; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Xunit; + +namespace MinimalChat.Tests +{ + public class MessageServiceTests + { + private readonly Mock> _mockMessageRepository; + private readonly Mock> _mockUserRepository; + private readonly Mock _mockMapper; + private readonly MessageService _messageService; + + public MessageServiceTests() + { + _mockMessageRepository = new Mock>(); + _mockUserRepository = new Mock>(); + _mockMapper = new Mock(); + _messageService = new MessageService(_mockMessageRepository.Object, _mockUserRepository.Object, _mockMapper.Object); + } + + [Fact] + public async Task SendMessageAsync_Success() + { + // Arrange + var messageDto = new MessageDto { SenderId = Guid.NewGuid().ToString(), ReceiverId = Guid.NewGuid().ToString(), Content = "Hello!" }; + var sender = new User { Id = Guid.Parse(messageDto.SenderId), Name = "Sender" }; + var receiver = new User { Id = Guid.Parse(messageDto.ReceiverId), Name = "Receiver" }; + var message = new Message { Id = 1, SenderId = sender.Id, ReceiverId = receiver.Id, Content = messageDto.Content, Timestamp = DateTime.UtcNow }; + var responseMessageDto = new ResponseMessageDto { Id = message.Id, SenderId = message.SenderId, ReceiverId = message.ReceiverId, Content = message.Content, Timestamp = message.Timestamp, SenderName = sender.Name, ReceiverName = receiver.Name }; + + _mockUserRepository.Setup(repo => repo.GetByIdAsync(sender.Id)).ReturnsAsync(sender); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(receiver.Id)).ReturnsAsync(receiver); + _mockMapper.Setup(m => m.Map(messageDto)).Returns(message); + _mockMessageRepository.Setup(repo => repo.AddAsync(It.IsAny())).Returns(Task.CompletedTask); + _mockMapper.Setup(m => m.Map(It.IsAny())).Returns(responseMessageDto); + _mockMapper.Setup(m => m.Map(It.IsAny(), It.IsAny())) + .Callback((src, dest) => { + if (src.Id == sender.Id) dest.SenderName = src.Name; + if (src.Id == receiver.Id) dest.ReceiverName = src.Name; + }); + + + // Act + var result = await _messageService.SendMessageAsync(messageDto); + + // Assert + _mockUserRepository.Verify(repo => repo.GetByIdAsync(sender.Id), Times.Once); + _mockUserRepository.Verify(repo => repo.GetByIdAsync(receiver.Id), Times.Once); + _mockMessageRepository.Verify(repo => repo.AddAsync(It.Is(m => m.Content == messageDto.Content)), Times.Once); + result.Should().NotBeNull(); + result.Should().BeEquivalentTo(responseMessageDto); + result.SenderName.Should().Be(sender.Name); + result.ReceiverName.Should().Be(receiver.Name); + } + + [Fact] + public async Task SendMessageAsync_SenderNotFound() + { + // Arrange + var messageDto = new MessageDto { SenderId = Guid.NewGuid().ToString(), ReceiverId = Guid.NewGuid().ToString(), Content = "Hello!" }; + _mockUserRepository.Setup(repo => repo.GetByIdAsync(Guid.Parse(messageDto.SenderId))).ReturnsAsync((User)null); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(Guid.Parse(messageDto.ReceiverId))).ReturnsAsync(new User()); + + + // Act + Func act = async () => await _messageService.SendMessageAsync(messageDto); + + // Assert + await act.Should().ThrowAsync().WithMessage($"Sender with ID {messageDto.SenderId} not found."); + } + + [Fact] + public async Task SendMessageAsync_ReceiverNotFound() + { + // Arrange + var messageDto = new MessageDto { SenderId = Guid.NewGuid().ToString(), ReceiverId = Guid.NewGuid().ToString(), Content = "Hello!" }; + _mockUserRepository.Setup(repo => repo.GetByIdAsync(Guid.Parse(messageDto.SenderId))).ReturnsAsync(new User()); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(Guid.Parse(messageDto.ReceiverId))).ReturnsAsync((User)null); + + // Act + Func act = async () => await _messageService.SendMessageAsync(messageDto); + + // Assert + await act.Should().ThrowAsync().WithMessage($"Receiver with ID {messageDto.ReceiverId} not found."); + } + + [Fact] + public async Task GetConversationHistoryAsync_ValidUsers_ReturnsHistory() + { + // Arrange + var user1Id = Guid.NewGuid(); + var user2Id = Guid.NewGuid(); + var conversationDto = new ConversationHistoryDto { UserId1 = user1Id, UserId2 = user2Id, Count = 5, SortOrder = "asc" }; + var user1 = new User { Id = user1Id }; + var user2 = new User { Id = user2Id }; + + var messages = new List + { + new Message { Id = 1, SenderId = user1Id, ReceiverId = user2Id, Content = "Msg1", Timestamp = DateTime.UtcNow.AddMinutes(-5) }, + new Message { Id = 2, SenderId = user2Id, ReceiverId = user1Id, Content = "Msg2", Timestamp = DateTime.UtcNow.AddMinutes(-4) }, + new Message { Id = 3, SenderId = user1Id, ReceiverId = user2Id, Content = "Msg3", Timestamp = DateTime.UtcNow.AddMinutes(-3) }, + new Message { Id = 4, SenderId = user2Id, ReceiverId = user1Id, Content = "Msg4", Timestamp = DateTime.UtcNow.AddMinutes(-2) }, + new Message { Id = 5, SenderId = user1Id, ReceiverId = user2Id, Content = "Msg5", Timestamp = DateTime.UtcNow.AddMinutes(-1) }, + new Message { Id = 6, SenderId = user1Id, ReceiverId = user2Id, Content = "Msg6-Older", Timestamp = DateTime.UtcNow.AddMinutes(-10) }, // Should be filtered by count if 'Before' not used + }; + var responseMessageDtos = messages.Where(m=> m.Id <=5).Select(m => new ResponseMessageDto { Id = m.Id, Content = m.Content, Timestamp = m.Timestamp, SenderId = m.SenderId, ReceiverId = m.ReceiverId }).ToList(); + + _mockUserRepository.Setup(repo => repo.GetByIdAsync(user1Id)).ReturnsAsync(user1); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(user2Id)).ReturnsAsync(user2); + // Let the service logic handle the filtering and sorting + _mockMessageRepository.Setup(repo => repo.GetAllAsync(It.IsAny>>())) + .ReturnsAsync((Expression> predicate) => messages.Where(predicate.Compile()).ToList()); + _mockMapper.Setup(m => m.Map>(It.IsAny>())).Returns(responseMessageDtos); + + + // Act + var result = await _messageService.GetConversationHistoryAsync(conversationDto); + + // Assert + _mockUserRepository.Verify(repo => repo.GetByIdAsync(user1Id), Times.Once); + _mockUserRepository.Verify(repo => repo.GetByIdAsync(user2Id), Times.Once); + _mockMessageRepository.Verify(repo => repo.GetAllAsync(It.IsAny>>()), Times.Once); + result.Should().NotBeNull(); + result.Should().HaveCount(conversationDto.Count); + result.Should().BeInAscendingOrder(r => r.Timestamp); // due to sortOrder = "asc" + result.All(r => (r.SenderId == user1Id && r.ReceiverId == user2Id) || (r.SenderId == user2Id && r.ReceiverId == user1Id)).Should().BeTrue(); + } + + [Fact] + public async Task GetConversationHistoryAsync_WithBefore_ReturnsCorrectHistory() + { + // Arrange + var user1Id = Guid.NewGuid(); + var user2Id = Guid.NewGuid(); + var beforeTimestamp = DateTime.UtcNow.AddMinutes(-2.5); // Before Msg3 + var conversationDto = new ConversationHistoryDto { UserId1 = user1Id, UserId2 = user2Id, Before = beforeTimestamp, Count = 2, SortOrder = "desc" }; + var user1 = new User { Id = user1Id }; + var user2 = new User { Id = user2Id }; + + var messages = new List + { + new Message { Id = 1, SenderId = user1Id, ReceiverId = user2Id, Content = "Msg1", Timestamp = DateTime.UtcNow.AddMinutes(-5) }, // Should be included + new Message { Id = 2, SenderId = user2Id, ReceiverId = user1Id, Content = "Msg2", Timestamp = DateTime.UtcNow.AddMinutes(-4) }, // Should be included + new Message { Id = 3, SenderId = user1Id, ReceiverId = user2Id, Content = "Msg3", Timestamp = DateTime.UtcNow.AddMinutes(-3) }, // Should be included + new Message { Id = 4, SenderId = user2Id, ReceiverId = user1Id, Content = "Msg4", Timestamp = DateTime.UtcNow.AddMinutes(-2) }, // After 'Before' + new Message { Id = 5, SenderId = user1Id, ReceiverId = user2Id, Content = "Msg5", Timestamp = DateTime.UtcNow.AddMinutes(-1) }, // After 'Before' + }; + // Expected: Msg3, Msg2 (Count = 2, Descending, Before Msg3's timestamp which is -3) + var expectedMessages = new List { messages[2], messages[1] }; + var responseMessageDtos = expectedMessages.Select(m => new ResponseMessageDto { Id = m.Id, Content = m.Content, Timestamp = m.Timestamp, SenderId = m.SenderId, ReceiverId = m.ReceiverId }).ToList(); + + + _mockUserRepository.Setup(repo => repo.GetByIdAsync(user1Id)).ReturnsAsync(user1); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(user2Id)).ReturnsAsync(user2); + _mockMessageRepository.Setup(repo => repo.GetAllAsync(It.IsAny>>())) + .ReturnsAsync((Expression> predicate) => messages.Where(predicate.Compile()).ToList()); + _mockMapper.Setup(m => m.Map>(It.IsAny>())).Returns(responseMessageDtos); + + + // Act + var result = await _messageService.GetConversationHistoryAsync(conversationDto); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(conversationDto.Count); + result.Should().BeInDescendingOrder(r => r.Timestamp); + result.Select(r => r.Id).Should().ContainInOrder(3, 2); // Msg3, then Msg2 + result.All(r => r.Timestamp < beforeTimestamp).Should().BeTrue(); + } + + [Fact] + public async Task GetConversationHistoryAsync_User1NotFound() + { + // Arrange + var conversationDto = new ConversationHistoryDto { UserId1 = Guid.NewGuid(), UserId2 = Guid.NewGuid() }; + _mockUserRepository.Setup(repo => repo.GetByIdAsync(conversationDto.UserId1)).ReturnsAsync((User)null); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(conversationDto.UserId2)).ReturnsAsync(new User()); + + + // Act + Func act = async () => await _messageService.GetConversationHistoryAsync(conversationDto); + + // Assert + await act.Should().ThrowAsync().WithMessage($"User with ID {conversationDto.UserId1} not found."); + } + + [Fact] + public async Task GetConversationHistoryAsync_User2NotFound() + { + // Arrange + var conversationDto = new ConversationHistoryDto { UserId1 = Guid.NewGuid(), UserId2 = Guid.NewGuid() }; + _mockUserRepository.Setup(repo => repo.GetByIdAsync(conversationDto.UserId1)).ReturnsAsync(new User()); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(conversationDto.UserId2)).ReturnsAsync((User)null); + + // Act + Func act = async () => await _messageService.GetConversationHistoryAsync(conversationDto); + + // Assert + await act.Should().ThrowAsync().WithMessage($"User with ID {conversationDto.UserId2} not found."); + } + + [Fact] + public async Task GetConversationHistoryAsync_NoMessages_ReturnsEmpty() + { + // Arrange + var conversationDto = new ConversationHistoryDto { UserId1 = Guid.NewGuid(), UserId2 = Guid.NewGuid(), Count = 10 }; + _mockUserRepository.Setup(repo => repo.GetByIdAsync(conversationDto.UserId1)).ReturnsAsync(new User()); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(conversationDto.UserId2)).ReturnsAsync(new User()); + _mockMessageRepository.Setup(repo => repo.GetAllAsync(It.IsAny>>())).ReturnsAsync(new List()); + _mockMapper.Setup(m => m.Map>(It.IsAny>())).Returns(new List()); + + // Act + var result = await _messageService.GetConversationHistoryAsync(conversationDto); + + // Assert + result.Should().NotBeNull(); + result.Should().BeEmpty(); + } + + [Fact] + public async Task EditMessageAsync_Success() + { + // Arrange + var messageId = 1; + var currentUserId = Guid.NewGuid().ToString(); + var editMessageDto = new EditMessageDto { Content = "Updated content" }; + var message = new Message { Id = messageId, SenderId = Guid.Parse(currentUserId), Content = "Original content", Timestamp = DateTime.UtcNow }; + var updatedMessage = new Message { Id = messageId, SenderId = Guid.Parse(currentUserId), Content = editMessageDto.Content, Timestamp = message.Timestamp }; // Timestamp shouldn't change + var responseMessageDto = new ResponseMessageDto { Id = messageId, SenderId = Guid.Parse(currentUserId), Content = editMessageDto.Content, Timestamp = message.Timestamp }; + + _mockMessageRepository.Setup(repo => repo.GetByIdAsync(messageId)).ReturnsAsync(message); + _mockMessageRepository.Setup(repo => repo.UpdateAsync(It.Is(m => m.Id == messageId && m.Content == editMessageDto.Content))).Returns(Task.CompletedTask); + _mockMapper.Setup(m => m.Map(It.Is(m => m.Content == editMessageDto.Content))).Returns(responseMessageDto); + _mockMapper.Setup(m => m.Map(editMessageDto, message)).Callback((src, dest) => { + dest.Content = src.Content; + }); + + + // Act + var result = await _messageService.EditMessageAsync(messageId, editMessageDto, currentUserId); + + // Assert + _mockMessageRepository.Verify(repo => repo.GetByIdAsync(messageId), Times.Once); + _mockMessageRepository.Verify(repo => repo.UpdateAsync(It.Is(m => m.Content == editMessageDto.Content)), Times.Once); + result.Should().NotBeNull(); + result.Content.Should().Be(editMessageDto.Content); + } + + [Fact] + public async Task EditMessageAsync_MessageNotFound() + { + // Arrange + var messageId = 1; + var editMessageDto = new EditMessageDto { Content = "Updated content" }; + _mockMessageRepository.Setup(repo => repo.GetByIdAsync(messageId)).ReturnsAsync((Message)null); + + // Act + Func act = async () => await _messageService.EditMessageAsync(messageId, editMessageDto, Guid.NewGuid().ToString()); + + // Assert + await act.Should().ThrowAsync().WithMessage($"Message with ID {messageId} not found."); + } + + [Fact] + public async Task EditMessageAsync_UnauthorizedEdit() + { + // Arrange + var messageId = 1; + var ownerUserId = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); // Different user + var editMessageDto = new EditMessageDto { Content = "Updated content" }; + var message = new Message { Id = messageId, SenderId = ownerUserId, Content = "Original content" }; + + _mockMessageRepository.Setup(repo => repo.GetByIdAsync(messageId)).ReturnsAsync(message); + + // Act + Func act = async () => await _messageService.EditMessageAsync(messageId, editMessageDto, currentUserId); + + // Assert + await act.Should().ThrowAsync().WithMessage("You are not authorized to edit this message."); + } + + [Fact] + public async Task DeleteMessageAsync_Success() + { + // Arrange + var messageId = 1; + var currentUserId = Guid.NewGuid().ToString(); + var message = new Message { Id = messageId, SenderId = Guid.Parse(currentUserId) }; + + _mockMessageRepository.Setup(repo => repo.GetByIdAsync(messageId)).ReturnsAsync(message); + _mockMessageRepository.Setup(repo => repo.DeleteAsync(message)).Returns(Task.CompletedTask); + + // Act + var result = await _messageService.DeleteMessageAsync(messageId, currentUserId); + + // Assert + _mockMessageRepository.Verify(repo => repo.GetByIdAsync(messageId), Times.Once); + _mockMessageRepository.Verify(repo => repo.DeleteAsync(message), Times.Once); + result.Should().BeTrue(); + } + + [Fact] + public async Task DeleteMessageAsync_MessageNotFound() + { + // Arrange + var messageId = 1; + _mockMessageRepository.Setup(repo => repo.GetByIdAsync(messageId)).ReturnsAsync((Message)null); + + // Act + Func act = async () => await _messageService.DeleteMessageAsync(messageId, Guid.NewGuid().ToString()); + + // Assert + await act.Should().ThrowAsync().WithMessage($"Message with ID {messageId} not found."); + } + + [Fact] + public async Task DeleteMessageAsync_UnauthorizedDelete() + { + // Arrange + var messageId = 1; + var ownerUserId = Guid.NewGuid(); + var currentUserId = Guid.NewGuid().ToString(); // Different user + var message = new Message { Id = messageId, SenderId = ownerUserId }; + + _mockMessageRepository.Setup(repo => repo.GetByIdAsync(messageId)).ReturnsAsync(message); + + // Act + Func act = async () => await _messageService.DeleteMessageAsync(messageId, currentUserId); + + // Assert + await act.Should().ThrowAsync().WithMessage("You are not authorized to delete this message."); + } + + [Fact] + public async Task GetMessagesAsync_DefaultParameters_ReturnsMessages() + { + // Arrange + var getMessagesDto = new GetMessagesDto(); // Defaults: Page = 0, PageSize = 20, SortOrder = "desc" + var messages = new List(); + for (int i = 1; i <= 25; i++) // More than default PageSize + { + messages.Add(new Message { Id = i, Content = $"Message {i}", Timestamp = DateTime.UtcNow.AddMinutes(-i) }); + } + var expectedMessages = messages.OrderByDescending(m => m.Timestamp).Take(20) + .Select(m => new ResponseMessageDto { Id = m.Id, Content = m.Content, Timestamp = m.Timestamp }).ToList(); + + _mockMessageRepository.Setup(repo => repo.GetAllAsync(null)).ReturnsAsync(messages); // No filter expression for this overload + _mockMapper.Setup(m => m.Map>(It.IsAny>())) + .Returns((IEnumerable src) => src.Select(m => new ResponseMessageDto { Id = m.Id, Content = m.Content, Timestamp = m.Timestamp })); + + + // Act + var result = await _messageService.GetMessagesAsync(getMessagesDto); + + // Assert + _mockMessageRepository.Verify(repo => repo.GetAllAsync(null), Times.Once); + result.Should().NotBeNull(); + result.Should().HaveCount(20); // Default PageSize + result.Should().BeInDescendingOrder(r => r.Timestamp); // Default SortOrder + result.First().Content.Should().Be("Message 1"); // Newest + } + + [Fact] + public async Task GetMessagesAsync_WithQuery_ReturnsFilteredMessages() + { + // Arrange + var query = "Special"; + var getMessagesDto = new GetMessagesDto { Query = query }; + var messages = new List + { + new Message { Id = 1, Content = "This is a Special message", Timestamp = DateTime.UtcNow.AddMinutes(-1) }, + new Message { Id = 2, Content = "Another message", Timestamp = DateTime.UtcNow.AddMinutes(-2) }, + new Message { Id = 3, Content = "Yet another Special one", Timestamp = DateTime.UtcNow.AddMinutes(-3) } + }; + var filteredMessages = messages.Where(m => m.Content.Contains(query)).ToList(); + var expectedResponseDtos = filteredMessages.Select(m => new ResponseMessageDto { Id = m.Id, Content = m.Content, Timestamp = m.Timestamp }).ToList(); + + _mockMessageRepository.Setup(repo => repo.GetAllAsync(It.IsAny>>())) + .ReturnsAsync((Expression> predicate) => messages.Where(predicate.Compile()).ToList()); + _mockMapper.Setup(m => m.Map>(It.IsAny>())) + .Returns((IEnumerable src) => src.Select(m => new ResponseMessageDto { Id = m.Id, Content = m.Content, Timestamp = m.Timestamp })); + + // Act + var result = await _messageService.GetMessagesAsync(getMessagesDto); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(2); + result.All(r => r.Content.Contains(query)).Should().BeTrue(); + result.Should().BeEquivalentTo(expectedResponseDtos, options => options.ExcludingMissingMembers()); + } + + [Fact] + public async Task GetMessagesAsync_WithCountAndSort_ReturnsCorrectly() + { + // Arrange + var getMessagesDto = new GetMessagesDto { PageSize = 2, SortOrder = "asc" }; + var messages = new List + { + new Message { Id = 1, Content = "Message 1", Timestamp = DateTime.UtcNow.AddMinutes(-3) }, // Oldest + new Message { Id = 2, Content = "Message 2", Timestamp = DateTime.UtcNow.AddMinutes(-2) }, + new Message { Id = 3, Content = "Message 3", Timestamp = DateTime.UtcNow.AddMinutes(-1) } // Newest + }; + var expectedMessages = messages.OrderBy(m => m.Timestamp).Take(2) + .Select(m => new ResponseMessageDto { Id = m.Id, Content = m.Content, Timestamp = m.Timestamp }).ToList(); + + + _mockMessageRepository.Setup(repo => repo.GetAllAsync(null)).ReturnsAsync(messages); + _mockMapper.Setup(m => m.Map>(It.IsAny>())) + .Returns((IEnumerable src) => src.Select(m => new ResponseMessageDto { Id = m.Id, Content = m.Content, Timestamp = m.Timestamp })); + + + // Act + var result = await _messageService.GetMessagesAsync(getMessagesDto); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(2); + result.Should().BeInAscendingOrder(r => r.Timestamp); + result.First().Content.Should().Be("Message 1"); // Oldest because of asc sort + result.Should().BeEquivalentTo(expectedMessages, options => options.ExcludingMissingMembers()); + } + } +} diff --git a/MinimalChat.Tests/MessagesControllerTests.cs b/MinimalChat.Tests/MessagesControllerTests.cs new file mode 100644 index 0000000..5fb7fdb --- /dev/null +++ b/MinimalChat.Tests/MessagesControllerTests.cs @@ -0,0 +1,349 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using MinimalChat.API.Controllers; +using MinimalChat.Domain.DTOs; +using MinimalChat.Domain.Interfaces; +using Moq; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Xunit; + +namespace MinimalChat.Tests +{ + public class MessagesControllerTests + { + private readonly Mock _mockMessageService; + private readonly Mock _mockUserService; // Not directly used by controller methods, but good practice if needed later + private readonly Mock> _mockLogger; + private readonly MessagesController _controller; + + public MessagesControllerTests() + { + _mockMessageService = new Mock(); + _mockUserService = new Mock(); + _mockLogger = new Mock>(); + _controller = new MessagesController(_mockMessageService.Object, _mockUserService.Object, _mockLogger.Object); + } + + private void SetupUserContext(string userId) + { + var claims = new List { new Claim(ClaimTypes.NameIdentifier, userId) }; + var identity = new ClaimsIdentity(claims, "TestAuthType"); + var claimsPrincipal = new ClaimsPrincipal(identity); + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = claimsPrincipal } + }; + } + + [Fact] + public async Task SendMessage_ValidModel_ReturnsOk() + { + // Arrange + var senderUserId = Guid.NewGuid().ToString(); + SetupUserContext(senderUserId); + var messageDto = new MessageDto { ReceiverId = Guid.NewGuid().ToString(), Content = "Hello" }; + var expectedResponse = new ResponseMessageDto { Id = 1, SenderId = Guid.Parse(senderUserId), ReceiverId = Guid.Parse(messageDto.ReceiverId), Content = messageDto.Content }; + + _mockMessageService.Setup(s => s.SendMessageAsync(It.Is(m => m.SenderId == senderUserId && m.ReceiverId == messageDto.ReceiverId))) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.SendMessage(messageDto); + + // Assert + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(expectedResponse); + _mockMessageService.Verify(s => s.SendMessageAsync(It.Is(m => m.SenderId == senderUserId)), Times.Once); + } + + [Fact] + public async Task SendMessage_InvalidModel_ReturnsBadRequest() + { + // Arrange + var senderUserId = Guid.NewGuid().ToString(); + SetupUserContext(senderUserId); + _controller.ModelState.AddModelError("Content", "Content is required"); + var messageDto = new MessageDto { ReceiverId = Guid.NewGuid().ToString() }; // Invalid + + // Act + var result = await _controller.SendMessage(messageDto); + + // Assert + result.Should().BeOfType(); + _mockMessageService.Verify(s => s.SendMessageAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SendMessage_SenderNotFound_ReturnsNotFound() + { + // Arrange + var senderUserId = Guid.NewGuid().ToString(); + SetupUserContext(senderUserId); + var messageDto = new MessageDto { ReceiverId = Guid.NewGuid().ToString(), Content = "Hello" }; + var exceptionMessage = $"Sender with ID {senderUserId} not found."; + _mockMessageService.Setup(s => s.SendMessageAsync(It.Is(m => m.SenderId == senderUserId))) + .ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.SendMessage(messageDto); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.Value.Should().Be(exceptionMessage); + } + + [Fact] + public async Task SendMessage_ReceiverNotFound_ReturnsNotFound() + { + // Arrange + var senderUserId = Guid.NewGuid().ToString(); + var receiverId = Guid.NewGuid().ToString(); + SetupUserContext(senderUserId); + var messageDto = new MessageDto { ReceiverId = receiverId, Content = "Hello" }; + var exceptionMessage = $"Receiver with ID {receiverId} not found."; + _mockMessageService.Setup(s => s.SendMessageAsync(It.Is(m => m.SenderId == senderUserId))) + .ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.SendMessage(messageDto); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.Value.Should().Be(exceptionMessage); + } + + [Fact] + public async Task GetConversationHistory_ValidUsers_ReturnsOk() + { + // Arrange + var requestingUserId = Guid.NewGuid().ToString(); + var contactId = Guid.NewGuid(); + SetupUserContext(requestingUserId); + var conversationDto = new ConversationHistoryDto { UserId1 = Guid.Parse(requestingUserId), UserId2 = contactId, Count = 10 }; + var expectedResponse = new List { new ResponseMessageDto { Content = "Hi" } }; + _mockMessageService.Setup(s => s.GetConversationHistoryAsync(It.Is(d => d.UserId1 == Guid.Parse(requestingUserId) && d.UserId2 == contactId))) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.GetConversationHistory(contactId, null, 10, "asc"); + + // Assert + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(expectedResponse); + _mockMessageService.Verify(s => s.GetConversationHistoryAsync(It.Is( + d => d.UserId1 == Guid.Parse(requestingUserId) && + d.UserId2 == contactId && + d.Before == null && + d.Count == 10 && + d.SortOrder == "asc" + )), Times.Once); + } + + [Fact] + public async Task GetConversationHistory_UserNotFound_ReturnsNotFound() + { + // Arrange + var requestingUserId = Guid.NewGuid().ToString(); + var contactId = Guid.NewGuid(); + SetupUserContext(requestingUserId); + var exceptionMessage = "User not found."; + _mockMessageService.Setup(s => s.GetConversationHistoryAsync(It.Is(d => d.UserId1 == Guid.Parse(requestingUserId) && d.UserId2 == contactId))) + .ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.GetConversationHistory(contactId, null, 10, "asc"); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.Value.Should().Be(exceptionMessage); + } + + [Fact] + public async Task EditMessage_ValidModel_ReturnsOk() + { + // Arrange + var editorUserId = Guid.NewGuid().ToString(); + SetupUserContext(editorUserId); + var messageId = 1; + var editDto = new EditMessageDto { Content = "Updated content" }; + var expectedResponse = new ResponseMessageDto { Id = messageId, Content = editDto.Content, SenderId = Guid.Parse(editorUserId) }; + _mockMessageService.Setup(s => s.EditMessageAsync(messageId, editDto, editorUserId)).ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.EditMessage(messageId, editDto); + + // Assert + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(expectedResponse); + } + + [Fact] + public async Task EditMessage_InvalidModel_ReturnsBadRequest() + { + // Arrange + var editorUserId = Guid.NewGuid().ToString(); + SetupUserContext(editorUserId); + _controller.ModelState.AddModelError("Content", "Content is required"); + var messageId = 1; + var editDto = new EditMessageDto { }; // Invalid + + // Act + var result = await _controller.EditMessage(messageId, editDto); + + // Assert + result.Should().BeOfType(); + _mockMessageService.Verify(s => s.EditMessageAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task EditMessage_MessageNotFound_ReturnsNotFound() + { + // Arrange + var editorUserId = Guid.NewGuid().ToString(); + SetupUserContext(editorUserId); + var messageId = 1; + var editDto = new EditMessageDto { Content = "Updated" }; + var exceptionMessage = "Message not found."; + _mockMessageService.Setup(s => s.EditMessageAsync(messageId, editDto, editorUserId)) + .ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.EditMessage(messageId, editDto); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.Value.Should().Be(exceptionMessage); + } + + [Fact] + public async Task EditMessage_Unauthorized_ReturnsUnauthorized() + { + // Arrange + var editorUserId = Guid.NewGuid().ToString(); + SetupUserContext(editorUserId); + var messageId = 1; + var editDto = new EditMessageDto { Content = "Updated" }; + var exceptionMessage = "Unauthorized to edit."; + _mockMessageService.Setup(s => s.EditMessageAsync(messageId, editDto, editorUserId)) + .ThrowsAsync(new UnauthorizedAccessException(exceptionMessage)); + + // Act + var result = await _controller.EditMessage(messageId, editDto); + + // Assert + var unauthorizedResult = result.Should().BeOfType().Subject; + unauthorizedResult.Value.Should().Be(exceptionMessage); + } + + [Fact] + public async Task DeleteMessage_ValidRequest_ReturnsNoContent() + { + // Arrange + var deleterUserId = Guid.NewGuid().ToString(); + SetupUserContext(deleterUserId); + var messageId = 1; + _mockMessageService.Setup(s => s.DeleteMessageAsync(messageId, deleterUserId)).ReturnsAsync(true); + + // Act + var result = await _controller.DeleteMessage(messageId); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task DeleteMessage_MessageNotFound_ReturnsNotFound() + { + // Arrange + var deleterUserId = Guid.NewGuid().ToString(); + SetupUserContext(deleterUserId); + var messageId = 1; + var exceptionMessage = "Message not found."; + _mockMessageService.Setup(s => s.DeleteMessageAsync(messageId, deleterUserId)) + .ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.DeleteMessage(messageId); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.Value.Should().Be(exceptionMessage); + } + + [Fact] + public async Task DeleteMessage_Unauthorized_ReturnsUnauthorized() + { + // Arrange + var deleterUserId = Guid.NewGuid().ToString(); + SetupUserContext(deleterUserId); + var messageId = 1; + var exceptionMessage = "Unauthorized to delete."; + _mockMessageService.Setup(s => s.DeleteMessageAsync(messageId, deleterUserId)) + .ThrowsAsync(new UnauthorizedAccessException(exceptionMessage)); + + // Act + var result = await _controller.DeleteMessage(messageId); + + // Assert + var unauthorizedResult = result.Should().BeOfType().Subject; + unauthorizedResult.Value.Should().Be(exceptionMessage); + } + + [Fact] + public async Task DeleteMessage_ServiceReturnsFalse_ReturnsBadRequest() + { + // Arrange + var deleterUserId = Guid.NewGuid().ToString(); + SetupUserContext(deleterUserId); + var messageId = 1; + _mockMessageService.Setup(s => s.DeleteMessageAsync(messageId, deleterUserId)).ReturnsAsync(false); + + // Act + var result = await _controller.DeleteMessage(messageId); + + // Assert + var badRequestResult = result.Should().BeOfType().Subject; + badRequestResult.Value.Should().Be("Failed to delete message."); + } + + [Fact] + public async Task GetMessages_ReturnsOkWithMessages() + { + // Arrange + var getMessagesDto = new GetMessagesDto { Query = "test", Page = 1, PageSize = 5, SortOrder = "asc" }; + var expectedResponse = new List { new ResponseMessageDto { Content = "Test Message" } }; + _mockMessageService.Setup(s => s.GetMessagesAsync(getMessagesDto)).ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.GetMessages(getMessagesDto); + + // Assert + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(expectedResponse); + } + + [Fact] + public async Task GetMessages_InvalidModel_ReturnsBadRequest() + { + // Arrange + _controller.ModelState.AddModelError("PageSize", "PageSize must be positive."); + var getMessagesDto = new GetMessagesDto { PageSize = -1 }; // Invalid + + // Act + var result = await _controller.GetMessages(getMessagesDto); + + // Assert + result.Should().BeOfType(); + _mockMessageService.Verify(s => s.GetMessagesAsync(It.IsAny()), Times.Never); + } + } +} diff --git a/MinimalChat.Tests/MinimalChat.Tests.csproj b/MinimalChat.Tests/MinimalChat.Tests.csproj new file mode 100644 index 0000000..679772c --- /dev/null +++ b/MinimalChat.Tests/MinimalChat.Tests.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/MinimalChat.Tests/RequestLoggingMiddlewareTests.cs b/MinimalChat.Tests/RequestLoggingMiddlewareTests.cs new file mode 100644 index 0000000..90aedcd --- /dev/null +++ b/MinimalChat.Tests/RequestLoggingMiddlewareTests.cs @@ -0,0 +1,158 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; // Though not directly used, good for context +using MinimalChat.API.Middleware; +using MinimalChat.Domain.Interfaces; +using MinimalChat.Domain.Models; +using Moq; +using System; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace MinimalChat.Tests +{ + public class RequestLoggingMiddlewareTests + { + private readonly Mock _mockLogService; + private readonly Mock _mockNextDelegate; + private readonly RequestLoggingMiddleware _middleware; + private readonly DefaultHttpContext _httpContext; + private readonly Mock _mockServiceProvider; + + public RequestLoggingMiddlewareTests() + { + _mockLogService = new Mock(); + _mockNextDelegate = new Mock(); + _middleware = new RequestLoggingMiddleware(_mockNextDelegate.Object); + _httpContext = new DefaultHttpContext(); + + // Mock IServiceProvider to provide ILogService + _mockServiceProvider = new Mock(); + _mockServiceProvider + .Setup(sp => sp.GetService(typeof(ILogService))) + .Returns(_mockLogService.Object); + _httpContext.RequestServices = _mockServiceProvider.Object; + + // Default request setup + _httpContext.Request.Scheme = "http"; + _httpContext.Request.Host = new HostString("localhost"); + _httpContext.Connection.RemoteIpAddress = IPAddress.Parse("127.0.0.1"); + } + + [Fact] + public async Task InvokeAsync_LogsRequestAndCallsNext() + { + // Arrange + _httpContext.Request.Path = "/test/path"; + _httpContext.Request.QueryString = new QueryString("?param=value"); + _httpContext.Request.Method = "GET"; + // For RequestBody, if middleware reads it (it does) + var requestBodyContent = "{\"key\":\"value\"}"; + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(requestBodyContent)); + _httpContext.Request.Body = stream; + _httpContext.Request.ContentType = "application/json"; + + + // Act + await _middleware.InvokeAsync(_httpContext); + + // Assert + _mockLogService.Verify(service => service.LogRequestAsync(It.Is(log => + log.Path == "/test/path" && + log.QueryString == "?param=value" && + log.Method == "GET" && + log.Host == "localhost" && + log.ClientIp == "127.0.0.1" && + log.RequestBody.Contains("key") && // Check if body was read + log.RequestBody.Contains("value") && + log.Timestamp <= DateTime.UtcNow && log.Timestamp > DateTime.UtcNow.AddSeconds(-5) // Timestamp is recent + )), Times.Once); + + _mockNextDelegate.Verify(next => next(_httpContext), Times.Once); + } + + [Fact] + public async Task InvokeAsync_LogServiceThrowsException_StillCallsNext() + { + // Arrange + _httpContext.Request.Path = "/test/error"; + _httpContext.Request.Method = "POST"; + var requestBodyContent = "{\"data\":\"important\"}"; + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(requestBodyContent)); + _httpContext.Request.Body = stream; + _httpContext.Request.ContentType = "application/json"; + + + _mockLogService.Setup(s => s.LogRequestAsync(It.IsAny())) + .ThrowsAsync(new Exception("Failed to log to database")); + + // Act + await _middleware.InvokeAsync(_httpContext); + + // Assert + _mockLogService.Verify(service => service.LogRequestAsync(It.IsAny()), Times.Once); + _mockNextDelegate.Verify(next => next(_httpContext), Times.Once); // Crucial: next delegate must still be called + } + + [Fact] + public async Task InvokeAsync_DoesNotLogHealthCheckEndpoint() + { + // Arrange + _httpContext.Request.Path = "/health"; + _httpContext.Request.Method = "GET"; + var requestBodyContent = "{}"; // Health checks usually have no body or simple one + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(requestBodyContent)); + _httpContext.Request.Body = stream; + _httpContext.Request.ContentType = "application/json"; + + + // Act + await _middleware.InvokeAsync(_httpContext); + + // Assert + _mockLogService.Verify(service => service.LogRequestAsync(It.IsAny()), Times.Never()); + _mockNextDelegate.Verify(next => next(_httpContext), Times.Once); + } + + [Fact] + public async Task InvokeAsync_HandlesNullRemoteIpAddressGracefully() + { + // Arrange + _httpContext.Request.Path = "/test/no-ip"; + _httpContext.Request.Method = "GET"; + _httpContext.Connection.RemoteIpAddress = null; // Simulate missing IP + + // Act + await _middleware.InvokeAsync(_httpContext); + + // Assert + _mockLogService.Verify(service => service.LogRequestAsync(It.Is(log => + log.ClientIp == "Unknown" && // Or whatever default/fallback is used + log.Path == "/test/no-ip" + )), Times.Once); + _mockNextDelegate.Verify(next => next(_httpContext), Times.Once); + } + + [Fact] + public async Task InvokeAsync_LogsRequestWithEmptyQueryString() + { + // Arrange + _httpContext.Request.Path = "/test/noquery"; + _httpContext.Request.Method = "GET"; + _httpContext.Request.QueryString = QueryString.Empty; // No query string + + // Act + await _middleware.InvokeAsync(_httpContext); + + // Assert + _mockLogService.Verify(service => service.LogRequestAsync(It.Is(log => + log.Path == "/test/noquery" && + log.QueryString == "" && // Ensure empty string is logged + log.Method == "GET" + )), Times.Once); + _mockNextDelegate.Verify(next => next(_httpContext), Times.Once); + } + } +} diff --git a/MinimalChat.Tests/UnitTest1.cs b/MinimalChat.Tests/UnitTest1.cs new file mode 100644 index 0000000..fdeab48 --- /dev/null +++ b/MinimalChat.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace MinimalChat.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} \ No newline at end of file diff --git a/MinimalChat.Tests/UserServiceTests.cs b/MinimalChat.Tests/UserServiceTests.cs new file mode 100644 index 0000000..6ff4363 --- /dev/null +++ b/MinimalChat.Tests/UserServiceTests.cs @@ -0,0 +1,258 @@ +using AutoMapper; +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using MinimalChat.Domain.DTOs; +using MinimalChat.Domain.Interfaces; +using MinimalChat.Domain.Models; +using MinmalChat.Data.Services; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Xunit; + +namespace MinimalChat.Tests +{ + public class UserServiceTests + { + private readonly Mock> _mockUserRepository; + private readonly Mock _mockMapper; + private readonly Mock _mockConfiguration; + private readonly UserService _userService; + + public UserServiceTests() + { + _mockUserRepository = new Mock>(); + _mockMapper = new Mock(); + _mockConfiguration = new Mock(); + + // Mock IConfiguration for JWT settings + var jwtSettings = new Mock(); + jwtSettings.Setup(s => s.Value).Returns("TestSecretKeyForMinimalChatApp"); // Replace with a test secret + _mockConfiguration.Setup(c => c.GetSection("Jwt:SecretKey")).Returns(jwtSettings.Object); + _mockConfiguration.Setup(c => c.GetSection("Jwt:Issuer")).Returns(new Mock().Object); + _mockConfiguration.Setup(c => c.GetSection("Jwt:Audience")).Returns(new Mock().Object); + + + _userService = new UserService(_mockUserRepository.Object, _mockMapper.Object, _mockConfiguration.Object); + } + + [Fact] + public async Task RegisterUserAsync_Success() + { + // Arrange + var registrationDto = new RegistrationDto { Name = "Test User", Email = "test@example.com", Password = "password" }; + var user = new User { Id = Guid.NewGuid(), Name = registrationDto.Name, Email = registrationDto.Email, PasswordHash = "hashedpassword" }; + var userDto = new UserDto { Id = user.Id, Name = user.Name, Email = user.Email }; + + _mockUserRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())) + .ReturnsAsync((User)null); // Simulate no existing user + _mockUserRepository.Setup(repo => repo.AddAsync(It.IsAny())) + .Returns(Task.CompletedTask); + _mockMapper.Setup(m => m.Map(It.IsAny())) + .Returns(userDto); + _mockMapper.Setup(m => m.Map(It.IsAny())).Returns(user); + + + // Act + var result = await _userService.RegisterUserAsync(registrationDto); + + // Assert + _mockUserRepository.Verify(repo => repo.AddAsync(It.Is(u => u.Email == registrationDto.Email && !string.IsNullOrEmpty(u.PasswordHash))), Times.Once); + result.Should().NotBeNull(); + result.Email.Should().Be(registrationDto.Email); + result.Name.Should().Be(registrationDto.Name); + } + + [Fact] + public async Task RegisterUserAsync_UserAlreadyExists() + { + // Arrange + var registrationDto = new RegistrationDto { Email = "existing@example.com", Password = "password" }; + var existingUser = new User { Email = registrationDto.Email }; + + _mockUserRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())) + .ReturnsAsync(existingUser); // Simulate existing user + + // Act + Func act = async () => await _userService.RegisterUserAsync(registrationDto); + + // Assert + await act.Should().ThrowAsync().WithMessage("User with this email already exists."); + _mockUserRepository.Verify(repo => repo.FirstOrDefaultAsync(It.IsAny>>()), Times.Once); + _mockUserRepository.Verify(repo => repo.AddAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task LoginAsync_Success() + { + // Arrange + var loginDto = new LoginDto { Email = "test@example.com", Password = "password" }; + var user = new User { Id = Guid.NewGuid(), Email = loginDto.Email, PasswordHash = BCrypt.Net.BCrypt.HashPassword(loginDto.Password) }; + var userDto = new UserDto { Id = user.Id, Email = user.Email, Name = "Test User" }; + + _mockUserRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())) + .ReturnsAsync(user); + _mockMapper.Setup(m => m.Map(user)).Returns(userDto); + + // Act + var result = await _userService.LoginAsync(loginDto); + + // Assert + _mockUserRepository.Verify(repo => repo.FirstOrDefaultAsync(It.IsAny>>()), Times.Once); + result.Should().NotBeNull(); + result.Token.Should().NotBeEmpty(); + result.User.Should().NotBeNull(); + result.User.Email.Should().Be(loginDto.Email); + } + + [Fact] + public async Task LoginAsync_UserNotFound() + { + // Arrange + var loginDto = new LoginDto { Email = "nonexistent@example.com", Password = "password" }; + + _mockUserRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())) + .ReturnsAsync((User)null); // Simulate user not found + + // Act + Func act = async () => await _userService.LoginAsync(loginDto); + + // Assert + await act.Should().ThrowAsync().WithMessage("User not found."); + } + + [Fact] + public async Task LoginAsync_InvalidPassword() + { + // Arrange + var loginDto = new LoginDto { Email = "test@example.com", Password = "wrongpassword" }; + var user = new User { Email = loginDto.Email, PasswordHash = BCrypt.Net.BCrypt.HashPassword("correctpassword") }; + + _mockUserRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())) + .ReturnsAsync(user); + + // Act + Func act = async () => await _userService.LoginAsync(loginDto); + + // Assert + await act.Should().ThrowAsync().WithMessage("Invalid credentials."); + } + + [Fact] + public async Task GetUserByIdAsync_UserFound() + { + // Arrange + var userId = Guid.NewGuid(); + var user = new User { Id = userId, Name = "Test User", Email = "test@example.com" }; + var userDto = new UserDto { Id = userId, Name = "Test User", Email = "test@example.com" }; + + _mockUserRepository.Setup(repo => repo.GetByIdAsync(userId)).ReturnsAsync(user); + _mockMapper.Setup(m => m.Map(user)).Returns(userDto); + + // Act + var result = await _userService.GetUserByIdAsync(userId); + + // Assert + _mockUserRepository.Verify(repo => repo.GetByIdAsync(userId), Times.Once); + result.Should().NotBeNull(); + result.Id.Should().Be(userId); + } + + [Fact] + public async Task GetUserByIdAsync_UserNotFound() + { + // Arrange + var userId = Guid.NewGuid(); + _mockUserRepository.Setup(repo => repo.GetByIdAsync(userId)).ReturnsAsync((User)null); + + // Act + Func act = async () => await _userService.GetUserByIdAsync(userId); + + // Assert + await act.Should().ThrowAsync().WithMessage($"User with ID {userId} not found."); + _mockUserRepository.Verify(repo => repo.GetByIdAsync(userId), Times.Once); + } + + [Fact] + public async Task GetAllUsersAsync_ReturnsAllUsers() + { + // Arrange + var users = new List + { + new User { Id = Guid.NewGuid(), Name = "User 1", Email = "user1@example.com" }, + new User { Id = Guid.NewGuid(), Name = "User 2", Email = "user2@example.com" } + }; + var userDtos = users.Select(u => new UserDto { Id = u.Id, Name = u.Name, Email = u.Email }).ToList(); + + _mockUserRepository.Setup(repo => repo.GetAllAsync()).ReturnsAsync(users); + _mockMapper.Setup(m => m.Map>(users)).Returns(userDtos); + + // Act + var result = await _userService.GetAllUsersAsync(); + + // Assert + _mockUserRepository.Verify(repo => repo.GetAllAsync(), Times.Once); + result.Should().NotBeNull(); + result.Should().HaveCount(users.Count); + result.Should().BeEquivalentTo(userDtos); + } + + [Fact] + public async Task LoginWithGoogleAsync_NewUser() + { + // Arrange + var googleLoginDto = new GoogleLoginDto { GoogleId = "google123", Email = "newgoogleuser@example.com", Name = "Google User" }; + var newUser = new User { Id = Guid.NewGuid(), GoogleId = googleLoginDto.GoogleId, Email = googleLoginDto.Email, Name = googleLoginDto.Name }; + var userDto = new UserDto { Id = newUser.Id, GoogleId = newUser.GoogleId, Email = newUser.Email, Name = newUser.Name }; + + _mockUserRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())) + .ReturnsAsync((User)null); // Simulate no existing user + _mockUserRepository.Setup(repo => repo.AddAsync(It.IsAny())).Returns(Task.CompletedTask); + _mockMapper.Setup(m => m.Map(googleLoginDto)).Returns(newUser); // Map DTO to User for saving + _mockMapper.Setup(m => m.Map(It.IsAny())).Returns(userDto); + + + // Act + var result = await _userService.LoginWithGoogleAsync(googleLoginDto); + + // Assert + _mockUserRepository.Verify(repo => repo.FirstOrDefaultAsync(It.IsAny>>()), Times.Once); + _mockUserRepository.Verify(repo => repo.AddAsync(It.Is(u => u.GoogleId == googleLoginDto.GoogleId)), Times.Once); + result.Should().NotBeNull(); + result.Token.Should().NotBeEmpty(); + result.User.Should().NotBeNull(); + result.User.Email.Should().Be(googleLoginDto.Email); + result.User.Name.Should().Be(googleLoginDto.Name); + } + + [Fact] + public async Task LoginWithGoogleAsync_ExistingUser() + { + // Arrange + var googleLoginDto = new GoogleLoginDto { GoogleId = "google123", Email = "existinggoogleuser@example.com", Name = "Google User" }; + var existingUser = new User { Id = Guid.NewGuid(), GoogleId = googleLoginDto.GoogleId, Email = googleLoginDto.Email, Name = "Old Name" }; // Name might be different + var userDto = new UserDto { Id = existingUser.Id, GoogleId = existingUser.GoogleId, Email = existingUser.Email, Name = existingUser.Name }; + + + _mockUserRepository.Setup(repo => repo.FirstOrDefaultAsync(It.IsAny>>())) + .ReturnsAsync(existingUser); + _mockMapper.Setup(m => m.Map(existingUser)).Returns(userDto); + + + // Act + var result = await _userService.LoginWithGoogleAsync(googleLoginDto); + + // Assert + _mockUserRepository.Verify(repo => repo.FirstOrDefaultAsync(It.IsAny>>()), Times.Once); + _mockUserRepository.Verify(repo => repo.AddAsync(It.IsAny()), Times.Never); + result.Should().NotBeNull(); + result.Token.Should().NotBeEmpty(); + result.User.Should().NotBeNull(); + result.User.Email.Should().Be(existingUser.Email); // Email from existing user + result.User.Name.Should().Be(existingUser.Name); // Name from existing user + } + } +} diff --git a/MinimalChat.Tests/UsersControllerTests.cs b/MinimalChat.Tests/UsersControllerTests.cs new file mode 100644 index 0000000..097983d --- /dev/null +++ b/MinimalChat.Tests/UsersControllerTests.cs @@ -0,0 +1,254 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using MinimalChat.API.Controllers; +using MinimalChat.Domain.DTOs; +using MinimalChat.Domain.Interfaces; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace MinimalChat.Tests +{ + public class UsersControllerTests + { + private readonly Mock _mockUserService; + private readonly Mock> _mockLogger; + private readonly UsersController _controller; + + public UsersControllerTests() + { + _mockUserService = new Mock(); + _mockLogger = new Mock>(); // Or NullLogger.Instance + _controller = new UsersController(_mockUserService.Object, _mockLogger.Object); + } + + [Fact] + public async Task Register_ValidModel_ReturnsOk() + { + // Arrange + var registrationDto = new RegistrationDto { Name = "Test User", Email = "test@example.com", Password = "Password123!" }; + var userDto = new UserDto { Id = Guid.NewGuid(), Name = registrationDto.Name, Email = registrationDto.Email }; + _mockUserService.Setup(s => s.RegisterUserAsync(registrationDto)).ReturnsAsync(userDto); + + // Act + var result = await _controller.Register(registrationDto); + + // Assert + _mockUserService.Verify(s => s.RegisterUserAsync(registrationDto), Times.Once); + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(userDto); + } + + [Fact] + public async Task Register_InvalidModel_ReturnsBadRequest() + { + // Arrange + _controller.ModelState.AddModelError("Email", "Email is required"); + var registrationDto = new RegistrationDto { Name = "Test User", Password = "Password123!" }; // Invalid DTO + + // Act + var result = await _controller.Register(registrationDto); + + // Assert + _mockUserService.Verify(s => s.RegisterUserAsync(It.IsAny()), Times.Never); + var badRequestResult = result.Should().BeOfType().Subject; + badRequestResult.StatusCode.Should().Be(400); + } + + [Fact] + public async Task Register_UserServiceThrowsArgumentException_ReturnsBadRequest() + { + // Arrange + var registrationDto = new RegistrationDto { Name = "Test User", Email = "test@example.com", Password = "Password123!" }; + var exceptionMessage = "Email already exists."; + _mockUserService.Setup(s => s.RegisterUserAsync(registrationDto)).ThrowsAsync(new ArgumentException(exceptionMessage)); + + // Act + var result = await _controller.Register(registrationDto); + + // Assert + var badRequestResult = result.Should().BeOfType().Subject; + badRequestResult.StatusCode.Should().Be(400); + badRequestResult.Value.Should().Be(exceptionMessage); + } + + [Fact] + public async Task Login_ValidCredentials_ReturnsOk() + { + // Arrange + var loginDto = new LoginDto { Email = "test@example.com", Password = "Password123!" }; + var loginResponseDto = new LoginResponseDto { Token = "test_token", User = new UserDto { Email = loginDto.Email } }; + _mockUserService.Setup(s => s.LoginAsync(loginDto)).ReturnsAsync(loginResponseDto); + + // Act + var result = await _controller.Login(loginDto); + + // Assert + _mockUserService.Verify(s => s.LoginAsync(loginDto), Times.Once); + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(loginResponseDto); + } + + [Fact] + public async Task Login_InvalidModel_ReturnsBadRequest() + { + // Arrange + _controller.ModelState.AddModelError("Email", "Email is required"); + var loginDto = new LoginDto { Password = "Password123!" }; + + // Act + var result = await _controller.Login(loginDto); + + // Assert + _mockUserService.Verify(s => s.LoginAsync(It.IsAny()), Times.Never); + var badRequestResult = result.Should().BeOfType().Subject; + badRequestResult.StatusCode.Should().Be(400); + } + + [Fact] + public async Task Login_UserServiceThrowsKeyNotFoundException_ReturnsNotFound() + { + // Arrange + var loginDto = new LoginDto { Email = "nonexistent@example.com", Password = "Password123!" }; + var exceptionMessage = "User not found."; + _mockUserService.Setup(s => s.LoginAsync(loginDto)).ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.Login(loginDto); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.StatusCode.Should().Be(404); + notFoundResult.Value.Should().Be(exceptionMessage); + } + + [Fact] + public async Task Login_UserServiceThrowsArgumentException_ReturnsBadRequest() + { + // Arrange + var loginDto = new LoginDto { Email = "test@example.com", Password = "WrongPassword" }; + var exceptionMessage = "Invalid credentials."; + _mockUserService.Setup(s => s.LoginAsync(loginDto)).ThrowsAsync(new ArgumentException(exceptionMessage)); + + // Act + var result = await _controller.Login(loginDto); + + // Assert + var badRequestResult = result.Should().BeOfType().Subject; + badRequestResult.StatusCode.Should().Be(400); + badRequestResult.Value.Should().Be(exceptionMessage); + } + + [Fact] + public async Task GetUser_UserExists_ReturnsOk() + { + // Arrange + var userId = Guid.NewGuid(); + var userDto = new UserDto { Id = userId, Name = "Test User", Email = "test@example.com" }; + _mockUserService.Setup(s => s.GetUserByIdAsync(userId)).ReturnsAsync(userDto); + + // Act + var result = await _controller.GetUser(userId); + + // Assert + _mockUserService.Verify(s => s.GetUserByIdAsync(userId), Times.Once); + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(userDto); + } + + [Fact] + public async Task GetUser_UserDoesNotExist_ReturnsNotFound() + { + // Arrange + var userId = Guid.NewGuid(); + var exceptionMessage = $"User with ID {userId} not found."; + _mockUserService.Setup(s => s.GetUserByIdAsync(userId)).ThrowsAsync(new KeyNotFoundException(exceptionMessage)); + + // Act + var result = await _controller.GetUser(userId); + + // Assert + var notFoundResult = result.Should().BeOfType().Subject; + notFoundResult.StatusCode.Should().Be(404); + notFoundResult.Value.Should().Be(exceptionMessage); + } + + [Fact] + public async Task GetAllUsers_ReturnsOkWithUserList() + { + // Arrange + var users = new List + { + new UserDto { Id = Guid.NewGuid(), Name = "User 1", Email = "user1@example.com" }, + new UserDto { Id = Guid.NewGuid(), Name = "User 2", Email = "user2@example.com" } + }; + _mockUserService.Setup(s => s.GetAllUsersAsync()).ReturnsAsync(users); + + // Act + var result = await _controller.GetAllUsers(); + + // Assert + _mockUserService.Verify(s => s.GetAllUsersAsync(), Times.Once); + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(users); + } + + [Fact] + public async Task LoginWithGoogle_ValidToken_ReturnsOk() + { + // Arrange + var googleLoginDto = new GoogleLoginDto { GoogleId = "google123", Email = "googleuser@example.com", Name = "Google User" }; + var loginResponseDto = new LoginResponseDto { Token = "google_token", User = new UserDto { Email = googleLoginDto.Email, Name = googleLoginDto.Name } }; + _mockUserService.Setup(s => s.LoginWithGoogleAsync(googleLoginDto)).ReturnsAsync(loginResponseDto); + + // Act + var result = await _controller.LoginWithGoogle(googleLoginDto); + + // Assert + _mockUserService.Verify(s => s.LoginWithGoogleAsync(googleLoginDto), Times.Once); + var okResult = result.Should().BeOfType().Subject; + okResult.StatusCode.Should().Be(200); + okResult.Value.Should().BeEquivalentTo(loginResponseDto); + } + + [Fact] + public async Task LoginWithGoogle_InvalidModel_ReturnsBadRequest() + { + // Arrange + _controller.ModelState.AddModelError("GoogleId", "GoogleId is required"); + var googleLoginDto = new GoogleLoginDto { Email = "googleuser@example.com", Name = "Google User" }; // Invalid + + // Act + var result = await _controller.LoginWithGoogle(googleLoginDto); + + // Assert + _mockUserService.Verify(s => s.LoginWithGoogleAsync(It.IsAny()), Times.Never); + var badRequestResult = result.Should().BeOfType().Subject; + badRequestResult.StatusCode.Should().Be(400); + } + + [Fact] + public async Task LoginWithGoogle_UserServiceThrowsException_ReturnsBadRequest() + { + // Arrange + var googleLoginDto = new GoogleLoginDto { GoogleId = "google123", Email = "googleuser@example.com", Name = "Google User" }; + var exceptionMessage = "An error occurred during Google login."; + _mockUserService.Setup(s => s.LoginWithGoogleAsync(googleLoginDto)).ThrowsAsync(new Exception(exceptionMessage)); + + // Act + var result = await _controller.LoginWithGoogle(googleLoginDto); + + // Assert + var badRequestResult = result.Should().BeOfType().Subject; + badRequestResult.StatusCode.Should().Be(400); + badRequestResult.Value.Should().Be(exceptionMessage); + } + } +}