Refactor: Revert to hard deletes for messages. Remove IsDeleted flag and update repository and configurations accordingly.

This commit is contained in:
google-labs-jules[bot]
2025-07-04 06:03:14 +00:00
parent fab3d6b67b
commit 204e8dba9c
21 changed files with 1299 additions and 185 deletions
@@ -0,0 +1,248 @@
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using System;
using System.Threading.Tasks;
using Xunit;
using Govor.API.Hubs;
using Govor.Application.Interfaces.Messages;
using Govor.Application.Interfaces.Messages.Parameters;
using Govor.Core.Models;
using Microsoft.AspNetCore.Hosting;
using Govor.API; // Assuming Startup or Program is here for WebApplicationFactory
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using Govor.Contracts.Requests.SignalR; // For MessageRequest etc.
using Govor.Contracts.Responses.SignalR; // For UserMessageResponse etc.
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
namespace Govor.API.Tests.IntegrationTests.Hubs;
// Base class for Hub tests to handle TestServer and client creation
public abstract class HubTestBase : IAsyncLifetime
{
protected TestServer TestServer;
protected HubConnection ClientConnection;
protected Mock<IMessageService> MockMessageService;
protected Guid TestUserId = Guid.NewGuid();
protected string TestUserToken;
public virtual async Task InitializeAsync()
{
MockMessageService = new Mock<IMessageService>();
TestUserToken = GenerateTestToken(TestUserId.ToString(), "testuser");
var webHostBuilder = new WebHostBuilder()
.UseEnvironment("Testing") // Use a specific environment for tests
.ConfigureServices(services =>
{
// Replace the real IMessageService with our mock for testing Hub logic
services.AddSingleton(MockMessageService.Object);
// Configure Authentication
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = false,
SignatureValidator = (token, parameters) => new JwtSecurityToken(token), // Bypass signature validation for test tokens
NameClaimType = ClaimTypes.NameIdentifier
};
// For SignalR, need to allow token in query string for WebSocket
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken) &&
(context.HttpContext.Request.Path.StartsWithSegments("/hubs/chats"))) // Adjust path if needed
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization();
services.AddSignalR();
// Add other necessary services that ChatsHub might depend on, possibly mocked
// services.AddSingleton(Mock.Of<IUserService>()); // If ChatsHub uses IUserService for groups
})
.ConfigureLogging(logging =>
{
logging.AddDebug(); // Or any other logger
})
.UseStartup<Startup>(); // Assuming you have a Startup.cs, or use Program.cs for .NET 6+
TestServer = new TestServer(webHostBuilder);
ClientConnection = new HubConnectionBuilder()
.WithUrl($"ws://localhost/hubs/chats", options => // Adjust URL as per your hub route
{
options.HttpMessageHandlerFactory = _ => TestServer.CreateHandler();
options.AccessTokenProvider = () => Task.FromResult<string?>(TestUserToken);
})
.Build();
await ClientConnection.StartAsync();
}
public virtual async Task DisposeAsync()
{
if (ClientConnection != null)
{
await ClientConnection.DisposeAsync();
}
TestServer?.Dispose();
}
private string GenerateTestToken(string userId, string userName)
{
var claims = new[]
{
new Claim("userID", userId), // Ensure this matches the claim name used in ChatsHub.GetUserId()
new Claim(ClaimTypes.Name, userName)
};
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-super-secret-key-that-is-long-enough")); // Use a key from config or a test key
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken("TestIssuer", "TestAudience", claims, expires: DateTime.Now.AddHours(1), signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
public class ChatsHubTests : HubTestBase
{
[Fact]
public async Task Send_ValidMessageToUser_CallsServiceAndNotifiesRecipientAndSender()
{
// Arrange
var recipientId = Guid.NewGuid();
var messageContent = "Hello from integration test";
var messageId = Guid.NewGuid();
MockMessageService.Setup(s => s.SendMessageAsync(It.Is<SendMessage>(p =>
p.FromUserId == TestUserId &&
p.RecipientId == recipientId &&
p.RecipientType == RecipientType.User)))
.ReturnsAsync(new SendMessageResult(true, null, messageId));
var messageReceivedTcs = new TaskCompletionSource<UserMessageResponse>();
var messageSentTcs = new TaskCompletionSource<UserMessageResponse>();
// Simulate recipient client being connected and in their own group
// This is tricky without a second client connection. For now, we assume SignalR handles group delivery.
// A more thorough test would involve a second client.
ClientConnection.On<UserMessageResponse>("ReceiveMessage", (response) =>
{
// This would ideally be for the recipient. For a single client test, this might be tricky.
// If sender also receives their own message via "ReceiveMessage" to their own user group:
if(response.SenderId == TestUserId && response.RecipientId == recipientId)
{
//This is if sender also gets ReceiveMessage, for simplicity in single client test
messageReceivedTcs.TrySetResult(response);
}
});
ClientConnection.On<UserMessageResponse>("MessageSent", (response) => // Confirmation to sender
{
if(response.MessageId == messageId) messageSentTcs.TrySetResult(response);
});
var request = new MessageRequest
{
RecipientId = recipientId,
RecipientType = RecipientType.User,
EncryptedContent = messageContent,
MediaAttachments = new List<MediaReference>()
};
// Act
await ClientConnection.InvokeAsync("Send", request);
// Assert
var sentConfirmation = await Task.WhenAny(messageSentTcs.Task, Task.Delay(TimeSpan.FromSeconds(5))) == messageSentTcs.Task
? messageSentTcs.Task.Result : null;
// var receivedMessage = await Task.WhenAny(messageReceivedTcs.Task, Task.Delay(TimeSpan.FromSeconds(5))) == messageReceivedTcs.Task
// ? messageReceivedTcs.Task.Result : null;
Assert.NotNull(sentConfirmation);
Assert.Equal(messageId, sentConfirmation.MessageId);
Assert.Equal(messageContent, sentConfirmation.EncryptedContent);
// Assert.NotNull(receivedMessage); // This part is harder to assert reliably with one client for recipient
// Assert.Equal(messageId, receivedMessage.MessageId);
MockMessageService.Verify(s => s.SendMessageAsync(It.Is<SendMessage>(p =>
p.FromUserId == TestUserId &&
p.RecipientId == recipientId &&
p.EncryptContent == messageContent)), Times.Once);
}
[Fact]
public async Task Edit_ValidEditRequest_CallsServiceAndNotifies()
{
// Arrange
var originalSenderId = TestUserId; // Editor must be the sender
var messageToEditId = Guid.NewGuid();
var newContent = "Updated content";
var recipientId = Guid.NewGuid(); // Could be user or group
var originalMessage = new Message {
Id = messageToEditId,
SenderId = originalSenderId,
RecipientId = recipientId,
RecipientType = RecipientType.User
};
MockMessageService.Setup(s => s.EditMessageAsync(It.Is<EditMessage>(p =>
p.EditorId == TestUserId &&
p.MessageId == messageToEditId &&
p.NewContent == newContent)))
.ReturnsAsync(new EditMessageResult(true, null, originalMessage));
var messageEditedTcs = new TaskCompletionSource<MessageEditedResponse>();
ClientConnection.On<MessageEditedResponse>("MessageEdited", (response) =>
{
if(response.MessageId == messageToEditId) messageEditedTcs.TrySetResult(response);
});
var request = new EditMessageRequest
{
MessageId = messageToEditId,
NewEncryptedContent = newContent
};
// Act
await ClientConnection.InvokeAsync("Edit", request);
// Assert
var editedNotification = await Task.WhenAny(messageEditedTcs.Task, Task.Delay(TimeSpan.FromSeconds(5))) == messageEditedTcs.Task
? messageEditedTcs.Task.Result : null;
Assert.NotNull(editedNotification);
Assert.Equal(messageToEditId, editedNotification.MessageId);
Assert.Equal(newContent, editedNotification.NewEncryptedContent);
Assert.Equal(originalSenderId, editedNotification.SenderId); // Verify original sender is preserved
MockMessageService.Verify(s => s.EditMessageAsync(It.Is<EditMessage>(p =>
p.EditorId == TestUserId &&
p.MessageId == messageToEditId &&
p.NewContent == newContent)), Times.Once);
}
// TODO: Add tests for Remove method
// TODO: Add tests for OnConnectedAsync (e.g., joining groups - might require IUserService mock)
// TODO: Add tests for unauthorized scenarios (e.g., editing/deleting someone else's message)
}
@@ -0,0 +1,276 @@
using Moq;
using Microsoft.Extensions.Logging;
using Govor.Application.Services;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Messages;
using Govor.Application.Interfaces.Messages.Parameters;
using Govor.Core.Models;
using Govor.Core.Repositories.Messages;
using Govor.Core.Repositories.Users;
using Govor.Core.Repositories.Groups;
using Xunit;
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Govor.Application.Exceptions.VerifyFriendship; // Assuming this exception type
namespace Govor.API.Tests.UnitTests.Services;
public class MessageServiceTests
{
private readonly Mock<IMessagesRepository> _mockMessagesRepo;
private readonly Mock<IUsersRepository> _mockUsersRepo;
private readonly Mock<IGroupsRepository> _mockGroupsRepo;
private readonly Mock<IVerifyFriendship> _mockVerifyFriendship;
private readonly Mock<ILogger<MessageService>> _mockLogger;
private readonly MessageService _messageService;
public MessageServiceTests()
{
_mockMessagesRepo = new Mock<IMessagesRepository>();
_mockUsersRepo = new Mock<IUsersRepository>();
_mockGroupsRepo = new Mock<IGroupsRepository>();
_mockVerifyFriendship = new Mock<IVerifyFriendship>();
_mockLogger = new Mock<ILogger<MessageService>>();
_messageService = new MessageService(
_mockMessagesRepo.Object,
_mockUsersRepo.Object,
_mockGroupsRepo.Object,
_mockVerifyFriendship.Object,
_mockLogger.Object);
}
// --- SendMessageAsync Tests ---
[Fact]
public async Task SendMessageAsync_ToUser_Success()
{
// Arrange
var senderId = Guid.NewGuid();
var recipientId = Guid.NewGuid();
var sendMessageParams = new SendMessage("Hello", null, recipientId, RecipientType.User, senderId, DateTime.UtcNow, new List<SendMedia>());
_mockUsersRepo.Setup(r => r.ExistsByIdAsync(recipientId)).ReturnsAsync(true);
_mockVerifyFriendship.Setup(v => v.VerifyAsync(senderId, recipientId)).Returns(Task.CompletedTask);
_mockMessagesRepo.Setup(r => r.AddAsync(It.IsAny<Message>())).Returns(Task.CompletedTask);
// Act
var result = await _messageService.SendMessageAsync(sendMessageParams);
// Assert
Assert.True(result.IsSuccess);
Assert.Null(result.Exception);
Assert.NotEqual(Guid.Empty, result.MessageId);
_mockMessagesRepo.Verify(r => r.AddAsync(It.Is<Message>(m =>
m.SenderId == senderId &&
m.RecipientId == recipientId &&
m.RecipientType == RecipientType.User &&
m.EncryptedContent == "Hello")), Times.Once);
}
[Fact]
public async Task SendMessageAsync_ToGroup_Success()
{
// Arrange
var senderId = Guid.NewGuid();
var groupId = Guid.NewGuid();
var sendMessageParams = new SendMessage("Hello Group", null, groupId, RecipientType.Group, senderId, DateTime.UtcNow, new List<SendMedia>());
_mockGroupsRepo.Setup(r => r.ExistsAsync(groupId)).ReturnsAsync(true);
// _mockGroupsRepo.Setup(r => r.IsUserMemberOfGroupAsync(senderId, groupId)).ReturnsAsync(true); // Assuming membership check
_mockMessagesRepo.Setup(r => r.AddAsync(It.IsAny<Message>())).Returns(Task.CompletedTask);
// Act
var result = await _messageService.SendMessageAsync(sendMessageParams);
// Assert
Assert.True(result.IsSuccess);
Assert.NotEqual(Guid.Empty, result.MessageId);
_mockMessagesRepo.Verify(r => r.AddAsync(It.Is<Message>(m => m.RecipientId == groupId && m.RecipientType == RecipientType.Group)), Times.Once);
}
[Fact]
public async Task SendMessageAsync_ToUser_RecipientNotFound_ReturnsFailure()
{
// Arrange
var senderId = Guid.NewGuid();
var recipientId = Guid.NewGuid();
var sendMessageParams = new SendMessage("Hello", null, recipientId, RecipientType.User, senderId, DateTime.UtcNow, new List<SendMedia>());
_mockUsersRepo.Setup(r => r.ExistsByIdAsync(recipientId)).ReturnsAsync(false);
// Act
var result = await _messageService.SendMessageAsync(sendMessageParams);
// Assert
Assert.False(result.IsSuccess);
Assert.NotNull(result.Exception);
Assert.IsType<KeyNotFoundException>(result.Exception);
Assert.Equal(Guid.Empty, result.MessageId);
}
[Fact]
public async Task SendMessageAsync_ToUser_FriendshipVerificationFails_ReturnsFailure()
{
// Arrange
var senderId = Guid.NewGuid();
var recipientId = Guid.NewGuid();
var sendMessageParams = new SendMessage("Hello", null, recipientId, RecipientType.User, senderId, DateTime.UtcNow, new List<SendMedia>());
_mockUsersRepo.Setup(r => r.ExistsByIdAsync(recipientId)).ReturnsAsync(true);
_mockVerifyFriendship.Setup(v => v.VerifyAsync(senderId, recipientId)).ThrowsAsync(new FriendshipException("Not friends"));
// Act
var result = await _messageService.SendMessageAsync(sendMessageParams);
// Assert
Assert.False(result.IsSuccess);
Assert.NotNull(result.Exception);
Assert.IsType<FriendshipException>(result.Exception);
Assert.Equal(Guid.Empty, result.MessageId);
}
// --- EditMessageAsync Tests ---
[Fact]
public async Task EditMessageAsync_Success()
{
// Arrange
var editorId = Guid.NewGuid();
var messageId = Guid.NewGuid();
var originalMessage = new Message { Id = messageId, SenderId = editorId, EncryptedContent = "Old", RecipientId = Guid.NewGuid(), RecipientType = RecipientType.User };
var editParams = new EditMessage(editorId, messageId, "New Content", DateTime.UtcNow);
_mockMessagesRepo.Setup(r => r.GetByIdAsync(messageId)).ReturnsAsync(originalMessage);
_mockMessagesRepo.Setup(r => r.UpdateAsync(It.IsAny<Message>())).Returns(Task.CompletedTask);
// Act
var result = await _messageService.EditMessageAsync(editParams);
// Assert
Assert.True(result.IsSuccess);
Assert.NotNull(result.OriginalMessage);
Assert.Equal(messageId, result.OriginalMessage!.Id);
_mockMessagesRepo.Verify(r => r.UpdateAsync(It.Is<Message>(m =>
m.Id == messageId &&
m.EncryptedContent == "New Content" &&
m.IsEdited == true &&
m.EditedAt == editParams.EditedAt)), Times.Once);
}
[Fact]
public async Task EditMessageAsync_MessageNotFound_ReturnsFailure()
{
// Arrange
var editorId = Guid.NewGuid();
var messageId = Guid.NewGuid();
var editParams = new EditMessage(editorId, messageId, "New Content", DateTime.UtcNow);
_mockMessagesRepo.Setup(r => r.GetByIdAsync(messageId)).ReturnsAsync((Message?)null);
// Act
var result = await _messageService.EditMessageAsync(editParams);
// Assert
Assert.False(result.IsSuccess);
Assert.IsType<KeyNotFoundException>(result.Exception);
Assert.Null(result.OriginalMessage);
}
[Fact]
public async Task EditMessageAsync_NotSender_ReturnsFailure()
{
// Arrange
var editorId = Guid.NewGuid();
var senderId = Guid.NewGuid(); // Different from editorId
var messageId = Guid.NewGuid();
var originalMessage = new Message { Id = messageId, SenderId = senderId, EncryptedContent = "Old" };
var editParams = new EditMessage(editorId, messageId, "New Content", DateTime.UtcNow);
_mockMessagesRepo.Setup(r => r.GetByIdAsync(messageId)).ReturnsAsync(originalMessage);
// Act
var result = await _messageService.EditMessageAsync(editParams);
// Assert
Assert.False(result.IsSuccess);
Assert.IsType<UnauthorizedAccessException>(result.Exception);
Assert.Null(result.OriginalMessage);
}
// --- DeleteMessageAsync Tests ---
[Fact]
public async Task DeleteMessageAsync_Success()
{
// Arrange
var deleterId = Guid.NewGuid();
var messageId = Guid.NewGuid();
var originalMessage = new Message { Id = messageId, SenderId = deleterId, RecipientId = Guid.NewGuid(), RecipientType = RecipientType.User };
var deleteParams = new DeleteMessage(deleterId, messageId);
_mockMessagesRepo.Setup(r => r.GetByIdAsync(messageId)).ReturnsAsync(originalMessage);
// For soft delete, the repo's DeleteAsync would internally mark IsDeleted=true and save.
// If it were hard delete, it would be _mockMessagesRepo.Setup(r => r.DeleteAsync(messageId)).Returns(Task.CompletedTask);
// Assuming DeleteAsync in repo handles the soft delete logic (sets IsDeleted, DeletedAt, then calls UpdateAsync or similar)
// For this test, we verify that the service calls the repo's DeleteAsync method.
// The actual soft delete implementation is tested at the repository level.
// However, MessageService is responsible for *initiating* the delete.
// If MessageService directly manipulated IsDeleted, we'd mock repo.UpdateAsync.
// Since it calls repo.DeleteAsync, we mock that.
_mockMessagesRepo.Setup(r => r.DeleteAsync(messageId)).Returns(Task.CompletedTask);
// Act
var result = await _messageService.DeleteMessageAsync(deleteParams);
// Assert
Assert.True(result.IsSuccess);
Assert.NotNull(result.OriginalMessage);
Assert.Equal(messageId, result.OriginalMessage!.Id);
_mockMessagesRepo.Verify(r => r.DeleteAsync(messageId), Times.Once);
}
[Fact]
public async Task DeleteMessageAsync_MessageNotFound_ReturnsFailure()
{
// Arrange
var deleterId = Guid.NewGuid();
var messageId = Guid.NewGuid();
var deleteParams = new DeleteMessage(deleterId, messageId);
_mockMessagesRepo.Setup(r => r.GetByIdAsync(messageId)).ReturnsAsync((Message?)null);
// Act
var result = await _messageService.DeleteMessageAsync(deleteParams);
// Assert
Assert.False(result.IsSuccess);
Assert.IsType<KeyNotFoundException>(result.Exception);
Assert.Null(result.OriginalMessage);
}
[Fact]
public async Task DeleteMessageAsync_NotSender_ReturnsFailure()
{
// Arrange
var deleterId = Guid.NewGuid();
var senderId = Guid.NewGuid(); // Different
var messageId = Guid.NewGuid();
var originalMessage = new Message { Id = messageId, SenderId = senderId };
var deleteParams = new DeleteMessage(deleterId, messageId);
_mockMessagesRepo.Setup(r => r.GetByIdAsync(messageId)).ReturnsAsync(originalMessage);
// Act
var result = await _messageService.DeleteMessageAsync(deleteParams);
// Assert
Assert.False(result.IsSuccess);
Assert.IsType<UnauthorizedAccessException>(result.Exception);
Assert.Null(result.OriginalMessage);
}
}