diff --git a/apps/server-net/src/Modules/Chats/Knot.Modules.Chats.csproj b/apps/server-net/src/Modules/Chats/Knot.Modules.Chats.csproj index 66029ca..a16e4d5 100644 --- a/apps/server-net/src/Modules/Chats/Knot.Modules.Chats.csproj +++ b/apps/server-net/src/Modules/Chats/Knot.Modules.Chats.csproj @@ -20,6 +20,11 @@ + + + + + net10.0 enable diff --git a/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Chats/CreateChatCommandHandlerTests.cs b/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Chats/CreateChatCommandHandlerTests.cs new file mode 100644 index 0000000..0c56093 --- /dev/null +++ b/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Chats/CreateChatCommandHandlerTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using NSubstitute; +using Xunit; +using Knot.Shared.Kernel; +using Knot.Modules.Chats.Domain; +using Knot.Modules.Chats.Application.Abstractions; +using Knot.Modules.Chats.Application.Chats.Create; + +namespace Knot.Modules.Chats.UnitTests.Chats; + +public class CreateChatCommandHandlerTests +{ + private readonly IChatRepository _chatRepository; + private readonly IChatsUnitOfWork _unitOfWork; + private readonly CreateChatCommandHandler _handler; + + public CreateChatCommandHandlerTests() + { + _chatRepository = Substitute.For(); + _unitOfWork = Substitute.For(); + _handler = new CreateChatCommandHandler(_chatRepository, _unitOfWork); + } + + [Fact] + public async Task Handle_ShouldCreateChatAndAddMembers() + { + // Arrange + var request = new CreateChatCommand("Test Group", ChatType.Group, new List { Guid.NewGuid(), Guid.NewGuid() }); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeEmpty(); + + _chatRepository.Received(1).Add(Arg.Is(c => + c.Name == "Test Group" && + c.Type == ChatType.Group && + c.Members.Count == 2 && + c.Members.First().Role == ChatRole.Owner && + c.Members.Last().Role == ChatRole.Member)); + + await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); + } +} diff --git a/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Chats/GetChatsQueryHandlerTests.cs b/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Chats/GetChatsQueryHandlerTests.cs new file mode 100644 index 0000000..458c684 --- /dev/null +++ b/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Chats/GetChatsQueryHandlerTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using NSubstitute; +using Xunit; +using Knot.Shared.Kernel; +using Knot.Modules.Chats.Domain; +using Knot.Modules.Chats.Application.Abstractions; +using Knot.Modules.Chats.Application.Chats.GetChats; +using Knot.Modules.Chats.Application.DTOs; + +namespace Knot.Modules.Chats.UnitTests.Chats; + +public class GetChatsQueryHandlerTests +{ + private readonly IChatRepository _chatRepository; + private readonly IUserDisplayNameProvider _userProvider; + private readonly IMessageRepository _messageRepository; + private readonly GetChatsQueryHandler _handler; + + public GetChatsQueryHandlerTests() + { + _chatRepository = Substitute.For(); + _userProvider = Substitute.For(); + _messageRepository = Substitute.For(); + + _handler = new GetChatsQueryHandler(_chatRepository, _userProvider, _messageRepository); + } + + [Fact] + public async Task Handle_ShouldReturnUserChats_WhenTheyExist() + { + // Arrange + var userId = Guid.NewGuid(); + var request = new GetChatsQuery(userId); + + var chat1 = Chat.Create("Test Chat", ChatType.Group); + chat1.GetType().GetProperty("Id")?.SetValue(chat1, Guid.NewGuid()); + chat1.AddMember(userId, ChatRole.Owner); + + var chat2 = Chat.Create("Personal", ChatType.Personal); + chat2.GetType().GetProperty("Id")?.SetValue(chat2, Guid.NewGuid()); + chat2.AddMember(userId, ChatRole.Member); + + _chatRepository.GetUserChatsAsync(userId, Arg.Any()) + .Returns(new List { chat1, chat2 }); + + _messageRepository.GetChatMessagesAsync(Arg.Any(), 1, 0, Arg.Any()) + .Returns(new List()); + + _userProvider.GetUsersInfoAsync(Arg.Any>(), Arg.Any()) + .Returns(new Dictionary()); + + _messageRepository.GetUnreadCountAsync(Arg.Any(), userId, Arg.Any()) + .Returns(0); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + + // It always appends synthetic "favorites" chat at the end if not found + result.Value.Count.Should().Be(3); + result.Value.Any(c => c.Name == "Test Chat").Should().BeTrue(); + result.Value.Any(c => c.Type == "favorites").Should().BeTrue(); + } +} diff --git a/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Messages/SendMessageCommandHandlerTests.cs b/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Messages/SendMessageCommandHandlerTests.cs new file mode 100644 index 0000000..26650dc --- /dev/null +++ b/apps/server-net/tests/Knot.Modules.Chats.UnitTests/Messages/SendMessageCommandHandlerTests.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using NSubstitute; +using Xunit; +using Knot.Shared.Kernel; +using Knot.Modules.Chats.Domain; +using Knot.Modules.Chats.Application.Abstractions; +using Knot.Modules.Chats.Application.Messages.Send; + +namespace Knot.Modules.Chats.UnitTests.Messages; + +public class SendMessageCommandHandlerTests +{ + private readonly IChatRepository _chatRepository; + private readonly IMessageRepository _messageRepository; + private readonly IChatsUnitOfWork _unitOfWork; + private readonly SendMessageCommandHandler _handler; + + public SendMessageCommandHandlerTests() + { + _chatRepository = Substitute.For(); + _messageRepository = Substitute.For(); + _unitOfWork = Substitute.For(); + + _handler = new SendMessageCommandHandler(_chatRepository, _messageRepository, _unitOfWork); + } + + [Fact] + public async Task Handle_ShouldReturnFailure_WhenChatNotFound() + { + // Arrange + var command = new SendMessageCommand(Guid.NewGuid(), Guid.NewGuid(), "Hello", "text"); + _chatRepository.GetByIdAsync(command.ChatId, Arg.Any()) + .Returns((Chat)null!); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Code.Should().Be(ChatErrors.ChatsNotFound.Code); + } + + [Fact] + public async Task Handle_ShouldReturnFailure_WhenSenderIsNotMember() + { + // Arrange + var chat = Chat.Create("Test", ChatType.Group); + var command = new SendMessageCommand(chat.Id, Guid.NewGuid(), "Hello", "text"); + + _chatRepository.GetByIdAsync(command.ChatId, Arg.Any()) + .Returns(chat); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Code.Should().Be(ChatErrors.ChatsForbidden.Code); + } + + [Fact] + public async Task Handle_ShouldCreateMessage_WhenAuthorized() + { + // Arrange + var senderId = Guid.NewGuid(); + var chat = Chat.Create("Test", ChatType.Group); + chat.AddMember(senderId, ChatRole.Member); + + var command = new SendMessageCommand(chat.Id, senderId, "Hello", "text"); + + _chatRepository.GetByIdAsync(command.ChatId, Arg.Any()) + .Returns(chat); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeEmpty(); + + _messageRepository.Received(1).Add(Arg.Is(m => + m.ChatId == chat.Id && + m.SenderId == senderId && + m.Content == "Hello" && + m.Type == "text")); + + await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); + } +} diff --git a/apps/server-net/tests/Knot.Modules.Identity.UnitTests/LoginUserCommandHandlerTests.cs b/apps/server-net/tests/Knot.Modules.Identity.UnitTests/LoginUserCommandHandlerTests.cs index ee1d486..e634e8c 100644 --- a/apps/server-net/tests/Knot.Modules.Identity.UnitTests/LoginUserCommandHandlerTests.cs +++ b/apps/server-net/tests/Knot.Modules.Identity.UnitTests/LoginUserCommandHandlerTests.cs @@ -4,8 +4,10 @@ using Knot.Modules.Identity.Application.Abstractions; using Knot.Modules.Identity.Application.Users.Login; using Knot.Modules.Identity.Domain; using Knot.Shared.Kernel; +using Knot.Modules.Identity.Application.Users.Auth; using Xunit; -using BCrypt.Net; +using System.Threading; +using System.Threading.Tasks; namespace Knot.Modules.Identity.UnitTests; @@ -28,7 +30,7 @@ public class LoginUserCommandHandlerTests // Arrange var password = "password123"; var passwordHash = BCrypt.Net.BCrypt.HashPassword(password); - var user = User.Create("testuser", passwordHash, "Test User"); + var user = User.Create("testuser", passwordHash, "Test User", null, null); var command = new LoginUserCommand("testuser", password); @@ -42,7 +44,8 @@ public class LoginUserCommandHandlerTests // Assert result.IsSuccess.Should().BeTrue(); - result.Value.Should().Be("valid-jwt-token"); + result.Value.Token.Should().Be("valid-jwt-token"); + result.Value.User.Username.Should().Be("testuser"); } [Fact] @@ -58,7 +61,7 @@ public class LoginUserCommandHandlerTests // Assert result.IsFailure.Should().BeTrue(); - result.Error.Code.Should().Be("Identity.InvalidCredentials"); + result.Error.Code.Should().Be(IdentityErrors.IdentityInvalidCredentials.Code); } [Fact] @@ -67,7 +70,7 @@ public class LoginUserCommandHandlerTests // Arrange var correctPassword = "correctPassword"; var passwordHash = BCrypt.Net.BCrypt.HashPassword(correctPassword); - var user = User.Create("testuser", passwordHash, "Test User"); + var user = User.Create("testuser", passwordHash, "Test User", null, null); var command = new LoginUserCommand("testuser", "wrongPassword"); @@ -79,6 +82,6 @@ public class LoginUserCommandHandlerTests // Assert result.IsFailure.Should().BeTrue(); - result.Error.Code.Should().Be("Identity.InvalidCredentials"); + result.Error.Code.Should().Be(IdentityErrors.IdentityInvalidCredentials.Code); } } diff --git a/apps/server-net/tests/Knot.Modules.Identity.UnitTests/RegisterUserCommandHandlerTests.cs b/apps/server-net/tests/Knot.Modules.Identity.UnitTests/RegisterUserCommandHandlerTests.cs index ef6447b..efdb4d0 100644 --- a/apps/server-net/tests/Knot.Modules.Identity.UnitTests/RegisterUserCommandHandlerTests.cs +++ b/apps/server-net/tests/Knot.Modules.Identity.UnitTests/RegisterUserCommandHandlerTests.cs @@ -4,7 +4,10 @@ using Knot.Modules.Identity.Application.Abstractions; using Knot.Modules.Identity.Application.Users.Register; using Knot.Modules.Identity.Domain; using Knot.Shared.Kernel; +using Knot.Shared.Kernel.Configuration; using Xunit; +using System.Threading; +using System.Threading.Tasks; namespace Knot.Modules.Identity.UnitTests; @@ -12,13 +15,21 @@ public class RegisterUserCommandHandlerTests { private readonly IUserRepository _userRepository; private readonly IIdentityUnitOfWork _unitOfWork; + private readonly ISettingsService _settings; + private readonly IJwtTokenProvider _tokenProvider; private readonly RegisterUserCommandHandler _handler; public RegisterUserCommandHandlerTests() { _userRepository = Substitute.For(); _unitOfWork = Substitute.For(); - _handler = new RegisterUserCommandHandler(_userRepository, _unitOfWork); + _settings = Substitute.For(); + _tokenProvider = Substitute.For(); + + var options = new SystemSettingsDto { EnableRegistration = true }; + _settings.Current.Returns(options); + + _handler = new RegisterUserCommandHandler(_userRepository, _unitOfWork, _settings, _tokenProvider); } [Fact] @@ -28,13 +39,14 @@ public class RegisterUserCommandHandlerTests var command = new RegisterUserCommand("testuser", "password123", "Test User", "test@example.com", "My bio"); _userRepository.IsUsernameUniqueAsync(command.Username, Arg.Any()) .Returns(true); + _tokenProvider.Generate(Arg.Any()).Returns("my-token"); // Act var result = await _handler.Handle(command, CancellationToken.None); // Assert result.IsSuccess.Should().BeTrue(); - result.Value.Should().NotBeEmpty(); + result.Value.Token.Should().Be("my-token"); _userRepository.Received(1).Add(Arg.Is(u => u.Username == command.Username && @@ -57,7 +69,25 @@ public class RegisterUserCommandHandlerTests // Assert result.IsFailure.Should().BeTrue(); - result.Error.Code.Should().Be("Identity.UsernameNotUnique"); + result.Error.Code.Should().Be(IdentityErrors.IdentityUsernameNotUnique.Code); + + _userRepository.DidNotReceive().Add(Arg.Any()); + await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any()); + } + + [Fact] + public async Task Handle_ShouldReturnFailure_WhenRegistrationDisabled() + { + // Arrange + _settings.Current.Returns(new SystemSettingsDto { EnableRegistration = false }); + var command = new RegisterUserCommand("testuser", "password123", "Test User", null, null); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Code.Should().Be(IdentityErrors.IdentityRegistrationDisabled.Code); _userRepository.DidNotReceive().Add(Arg.Any()); await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any());