using MikrocopApi.Dtos; using MikrocopApi.Exceptions; using MikrocopApi.Services; using MikrocopDb.Entities; using MikrocopDb.Repositories; using Moq; namespace MikrocopTests; [TestFixture] public sealed class UserServiceTests { [Test] public async Task CreateAsync_ReturnsCreatedUser_WhenRequestIsValid() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var request = CreateRequest(); repositoryMock.Setup(x => x.GetByUserNameAsync(request.UserName, It.IsAny())) .ReturnsAsync((UserEntity?)null); repositoryMock.Setup(x => x.GetByEmailAsync(request.Email, It.IsAny())) .ReturnsAsync((UserEntity?)null); hashingServiceMock.Setup(x => x.HashPassword(request.Password)) .Returns(("hash-value", "salt-value")); repositoryMock.Setup(x => x.AddAsync(It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); var result = await sut.CreateAsync(request); Assert.That(result.UserName, Is.EqualTo("new-user")); Assert.That(result.Email, Is.EqualTo("new.user@example.com")); repositoryMock.Verify(x => x.AddAsync(It.Is(u => u.UserName == "new-user" && u.FullName == "New User" && u.Email == "new.user@example.com" && u.PasswordHash == "hash-value" && u.PasswordSalt == "salt-value"), It.IsAny()), Times.Once); repositoryMock.VerifyAll(); hashingServiceMock.VerifyAll(); } [Test] public void CreateAsync_ThrowsConflict_WhenUserNameExists() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var request = CreateRequest(); repositoryMock.Setup(x => x.GetByUserNameAsync(request.UserName, It.IsAny())) .ReturnsAsync(CreateEntity()); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); Assert.ThrowsAsync(() => sut.CreateAsync(request)); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } [Test] public void CreateAsync_ThrowsConflict_WhenEmailExists() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var request = CreateRequest(); repositoryMock.Setup(x => x.GetByUserNameAsync(request.UserName, It.IsAny())) .ReturnsAsync((UserEntity?)null); repositoryMock.Setup(x => x.GetByEmailAsync(request.Email, It.IsAny())) .ReturnsAsync(CreateEntity()); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); Assert.ThrowsAsync(() => sut.CreateAsync(request)); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } [Test] public void CreateAsync_ThrowsBadRequest_WhenPasswordIsNotComplexEnough() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var request = CreateRequest(); request.Password = "weak"; repositoryMock.Setup(x => x.GetByUserNameAsync(request.UserName, It.IsAny())) .ReturnsAsync((UserEntity?)null); repositoryMock.Setup(x => x.GetByEmailAsync(request.Email, It.IsAny())) .ReturnsAsync((UserEntity?)null); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); Assert.ThrowsAsync(() => sut.CreateAsync(request)); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } [Test] public async Task UpdateAsync_UpdatesUserWithoutPassword_WhenPasswordIsNotProvided() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var id = Guid.NewGuid(); var existing = CreateEntity(id); var request = CreateUpdateRequest(password: null); repositoryMock.Setup(x => x.GetByIdAsync(id, It.IsAny())) .ReturnsAsync(existing); repositoryMock.Setup(x => x.GetByUserNameAsync(request.UserName, It.IsAny())) .ReturnsAsync((UserEntity?)null); repositoryMock.Setup(x => x.GetByEmailAsync(request.Email, It.IsAny())) .ReturnsAsync((UserEntity?)null); repositoryMock.Setup(x => x.UpdateAsync(existing, It.IsAny())) .Returns(Task.CompletedTask); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); await sut.UpdateAsync(id, request); Assert.That(existing.UserName, Is.EqualTo(request.UserName)); Assert.That(existing.PasswordHash, Is.EqualTo("hash")); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } [Test] public async Task UpdateAsync_HashesPassword_WhenPasswordIsProvided() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var id = Guid.NewGuid(); var existing = CreateEntity(id); var request = CreateUpdateRequest(password: "UpdatedPassword!123"); repositoryMock.Setup(x => x.GetByIdAsync(id, It.IsAny())) .ReturnsAsync(existing); repositoryMock.Setup(x => x.GetByUserNameAsync(request.UserName, It.IsAny())) .ReturnsAsync((UserEntity?)null); repositoryMock.Setup(x => x.GetByEmailAsync(request.Email, It.IsAny())) .ReturnsAsync((UserEntity?)null); hashingServiceMock.Setup(x => x.HashPassword(request.Password!)) .Returns(("new-hash", "new-salt")); repositoryMock.Setup(x => x.UpdateAsync(existing, It.IsAny())) .Returns(Task.CompletedTask); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); await sut.UpdateAsync(id, request); Assert.That(existing.PasswordHash, Is.EqualTo("new-hash")); Assert.That(existing.PasswordSalt, Is.EqualTo("new-salt")); repositoryMock.VerifyAll(); hashingServiceMock.VerifyAll(); } [Test] public void UpdateAsync_ThrowsNotFound_WhenUserDoesNotExist() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var id = Guid.NewGuid(); var request = CreateUpdateRequest(password: null); repositoryMock.Setup(x => x.GetByIdAsync(id, It.IsAny())) .ReturnsAsync((UserEntity?)null); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); Assert.ThrowsAsync(() => sut.UpdateAsync(id, request)); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } [Test] public void UpdateAsync_ThrowsConflict_WhenUserNameBelongsToAnotherUser() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var id = Guid.NewGuid(); var request = CreateUpdateRequest(password: null); repositoryMock.Setup(x => x.GetByIdAsync(id, It.IsAny())) .ReturnsAsync(CreateEntity(id)); repositoryMock.Setup(x => x.GetByUserNameAsync(request.UserName, It.IsAny())) .ReturnsAsync(CreateEntity(Guid.NewGuid())); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); Assert.ThrowsAsync(() => sut.UpdateAsync(id, request)); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } [Test] public void UpdateAsync_ThrowsConflict_WhenEmailBelongsToAnotherUser() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var id = Guid.NewGuid(); var request = CreateUpdateRequest(password: null); repositoryMock.Setup(x => x.GetByIdAsync(id, It.IsAny())) .ReturnsAsync(CreateEntity(id)); repositoryMock.Setup(x => x.GetByUserNameAsync(request.UserName, It.IsAny())) .ReturnsAsync((UserEntity?)null); repositoryMock.Setup(x => x.GetByEmailAsync(request.Email, It.IsAny())) .ReturnsAsync(CreateEntity(Guid.NewGuid())); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); Assert.ThrowsAsync(() => sut.UpdateAsync(id, request)); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } [Test] public void UpdateAsync_ThrowsBadRequest_WhenProvidedPasswordIsWeak() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var id = Guid.NewGuid(); var request = CreateUpdateRequest(password: "weak"); repositoryMock.Setup(x => x.GetByIdAsync(id, It.IsAny())) .ReturnsAsync(CreateEntity(id)); repositoryMock.Setup(x => x.GetByUserNameAsync(request.UserName, It.IsAny())) .ReturnsAsync((UserEntity?)null); repositoryMock.Setup(x => x.GetByEmailAsync(request.Email, It.IsAny())) .ReturnsAsync((UserEntity?)null); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); Assert.ThrowsAsync(() => sut.UpdateAsync(id, request)); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } [Test] public async Task GetByIdAsync_ReturnsUser_WhenUserExists() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var id = Guid.NewGuid(); var user = CreateEntity(id); repositoryMock.Setup(x => x.GetByIdAsync(id, It.IsAny())) .ReturnsAsync(user); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); var result = await sut.GetByIdAsync(id); Assert.That(result.Id, Is.EqualTo(id)); Assert.That(result.UserName, Is.EqualTo(user.UserName)); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } [Test] public void GetByIdAsync_ThrowsNotFound_WhenUserDoesNotExist() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var id = Guid.NewGuid(); repositoryMock.Setup(x => x.GetByIdAsync(id, It.IsAny())) .ReturnsAsync((UserEntity?)null); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); Assert.ThrowsAsync(() => sut.GetByIdAsync(id)); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } [Test] public async Task DeleteAsync_DeletesUser_WhenUserExists() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var id = Guid.NewGuid(); var user = CreateEntity(id); repositoryMock.Setup(x => x.GetByIdAsync(id, It.IsAny())) .ReturnsAsync(user); repositoryMock.Setup(x => x.DeleteAsync(user, It.IsAny())) .Returns(Task.CompletedTask); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); await sut.DeleteAsync(id); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } [Test] public void DeleteAsync_ThrowsNotFound_WhenUserDoesNotExist() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var id = Guid.NewGuid(); repositoryMock.Setup(x => x.GetByIdAsync(id, It.IsAny())) .ReturnsAsync((UserEntity?)null); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); Assert.ThrowsAsync(() => sut.DeleteAsync(id)); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } [Test] public async Task ValidatePasswordAsync_ReturnsResult_WhenUserExists() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var id = Guid.NewGuid(); var user = CreateEntity(id); const string password = "AnyPassword!123"; repositoryMock.Setup(x => x.GetByIdAsync(id, It.IsAny())) .ReturnsAsync(user); hashingServiceMock.Setup(x => x.VerifyPassword(password, user.PasswordHash, user.PasswordSalt)) .Returns(true); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); var result = await sut.ValidatePasswordAsync(id, password); Assert.That(result, Is.True); repositoryMock.VerifyAll(); hashingServiceMock.VerifyAll(); } [Test] public void ValidatePasswordAsync_ThrowsNotFound_WhenUserDoesNotExist() { var repositoryMock = new Mock(MockBehavior.Strict); var hashingServiceMock = new Mock(MockBehavior.Strict); var id = Guid.NewGuid(); repositoryMock.Setup(x => x.GetByIdAsync(id, It.IsAny())) .ReturnsAsync((UserEntity?)null); var sut = new UserService(repositoryMock.Object, hashingServiceMock.Object); Assert.ThrowsAsync(() => sut.ValidatePasswordAsync(id, "AnyPassword!123")); repositoryMock.VerifyAll(); hashingServiceMock.VerifyNoOtherCalls(); } private static CreateUserDto CreateRequest() { return new CreateUserDto { UserName = " new-user ", FullName = " New User ", Email = " new.user@example.com ", MobileNumber = " +38640123456 ", Language = " en ", Culture = " en-US ", Password = "ValidPassword!123" }; } private static UpdateUserDto CreateUpdateRequest(string? password) { return new UpdateUserDto { UserName = "updated-user", FullName = "Updated User", Email = "updated.user@example.com", MobileNumber = "+38640999888", Language = "en", Culture = "en-US", Password = password }; } private static UserEntity CreateEntity(Guid? id = null) { return new UserEntity { Id = id ?? Guid.NewGuid(), UserName = "existing-user", FullName = "Existing User", Email = "existing@example.com", MobileNumber = "+38640111222", Language = "en", Culture = "en-US", PasswordHash = "hash", PasswordSalt = "salt" }; } }