1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 13:59:36 +01:00

refactor(usermanagement): implement code review findings for User Management BC

Address all 18 findings from security code review (5 critical, 7 medium, 6 low):

Domain: make User and Role immutable with wither-pattern, add status transition
guards (ACTIVE->LOCKED, LOCKED->ACTIVE, ACTIVE|LOCKED->INACTIVE, INACTIVE->ACTIVE)

Application: enforce authorization via AuthorizationPort in all use cases, add
input validation, introduce LockUserCommand/UnlockUserCommand/RemoveRoleCommand,
fix audit event on password change failure (K5), use flatMap/mapError chains

Infrastructure: JWT blacklist with TTL and scheduled cleanup, login rate limiting
(5 attempts/15min), configurable CORS, generic error messages, conditional Swagger,
seed data context restriction

Tests: unit tests for all 10 use cases, adapted domain and integration tests
This commit is contained in:
Sebastian Frick 2026-02-19 10:11:20 +01:00
parent a1161cfbad
commit 05878b1ce9
45 changed files with 1989 additions and 2207 deletions

View file

@ -0,0 +1,131 @@
package de.effigenix.application.usermanagement;
import de.effigenix.application.usermanagement.command.AssignRoleCommand;
import de.effigenix.application.usermanagement.dto.UserDTO;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("AssignRole Use Case")
class AssignRoleTest {
@Mock private UserRepository userRepository;
@Mock private RoleRepository roleRepository;
@Mock private AuditLogger auditLogger;
@Mock private AuthorizationPort authPort;
private AssignRole assignRole;
private ActorId performedBy;
private User testUser;
private Role workerRole;
@BeforeEach
void setUp() {
assignRole = new AssignRole(userRepository, roleRepository, auditLogger, authPort);
performedBy = ActorId.of("admin-user");
testUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null
);
workerRole = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, new HashSet<>(), "Production Worker");
}
@Test
@DisplayName("should_AssignRole_When_ValidCommandProvided")
void should_AssignRole_When_ValidCommandProvided() {
when(authPort.can(performedBy, UserManagementAction.ROLE_ASSIGN)).thenReturn(true);
when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.of(workerRole)));
when(userRepository.save(any())).thenReturn(Result.success(null));
Result<UserError, UserDTO> result = assignRole.execute(
new AssignRoleCommand("user-1", RoleName.PRODUCTION_WORKER), performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().roles()).hasSize(1);
verify(userRepository).save(argThat(user -> user.roles().contains(workerRole)));
verify(auditLogger).log(eq(AuditEvent.ROLE_ASSIGNED), eq("user-1"), anyString(), eq(performedBy));
}
@Test
@DisplayName("should_FailWithUnauthorized_When_ActorLacksPermission")
void should_FailWithUnauthorized_When_ActorLacksPermission() {
when(authPort.can(performedBy, UserManagementAction.ROLE_ASSIGN)).thenReturn(false);
Result<UserError, UserDTO> result = assignRole.execute(
new AssignRoleCommand("user-1", RoleName.PRODUCTION_WORKER), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_FailWithInvalidInput_When_UserIdIsBlank")
void should_FailWithInvalidInput_When_UserIdIsBlank() {
when(authPort.can(performedBy, UserManagementAction.ROLE_ASSIGN)).thenReturn(true);
Result<UserError, UserDTO> result = assignRole.execute(
new AssignRoleCommand("", RoleName.PRODUCTION_WORKER), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidInput.class);
}
@Test
@DisplayName("should_FailWithInvalidInput_When_RoleNameIsNull")
void should_FailWithInvalidInput_When_RoleNameIsNull() {
when(authPort.can(performedBy, UserManagementAction.ROLE_ASSIGN)).thenReturn(true);
Result<UserError, UserDTO> result = assignRole.execute(
new AssignRoleCommand("user-1", null), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidInput.class);
}
@Test
@DisplayName("should_FailWithUserNotFound_When_UserDoesNotExist")
void should_FailWithUserNotFound_When_UserDoesNotExist() {
when(authPort.can(performedBy, UserManagementAction.ROLE_ASSIGN)).thenReturn(true);
when(userRepository.findById(UserId.of("nonexistent"))).thenReturn(Result.success(Optional.empty()));
Result<UserError, UserDTO> result = assignRole.execute(
new AssignRoleCommand("nonexistent", RoleName.PRODUCTION_WORKER), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.UserNotFound.class);
}
@Test
@DisplayName("should_FailWithRoleNotFound_When_RoleDoesNotExist")
void should_FailWithRoleNotFound_When_RoleDoesNotExist() {
when(authPort.can(performedBy, UserManagementAction.ROLE_ASSIGN)).thenReturn(true);
when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
when(roleRepository.findByName(RoleName.ADMIN)).thenReturn(Result.success(Optional.empty()));
Result<UserError, UserDTO> result = assignRole.execute(
new AssignRoleCommand("user-1", RoleName.ADMIN), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.RoleNotFound.class);
verify(userRepository, never()).save(any());
}
}

View file

@ -21,25 +21,14 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for AuthenticateUser Use Case.
* Tests authentication flow, credential validation, status checks, and audit logging.
*/
@ExtendWith(MockitoExtension.class)
@DisplayName("AuthenticateUser Use Case")
class AuthenticateUserTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordHasher passwordHasher;
@Mock
private SessionManager sessionManager;
@Mock
private AuditLogger auditLogger;
@Mock private UserRepository userRepository;
@Mock private PasswordHasher passwordHasher;
@Mock private SessionManager sessionManager;
@Mock private AuditLogger auditLogger;
@InjectMocks
private AuthenticateUser authenticateUser;
@ -55,15 +44,8 @@ class AuthenticateUserTest {
validPasswordHash = new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
testUser = User.reconstitute(
UserId.of("user-1"),
"john.doe",
"john@example.com",
validPasswordHash,
new HashSet<>(),
"branch-1",
UserStatus.ACTIVE,
LocalDateTime.now(),
null
UserId.of("user-1"), "john.doe", "john@example.com", validPasswordHash,
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null
);
sessionToken = new SessionToken("jwt-token", "Bearer", 3600L, LocalDateTime.now().plusSeconds(3600), "refresh-token");
@ -72,16 +54,13 @@ class AuthenticateUserTest {
@Test
@DisplayName("should_AuthenticateUser_When_ValidCredentialsProvided")
void should_AuthenticateUser_When_ValidCredentialsProvided() {
// Arrange
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("Password123!", validPasswordHash)).thenReturn(true);
when(sessionManager.createSession(testUser)).thenReturn(sessionToken);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);
// Assert
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEqualTo(sessionToken);
verify(userRepository).save(any());
@ -91,14 +70,10 @@ class AuthenticateUserTest {
@Test
@DisplayName("should_FailWithInvalidCredentials_When_UserNotFound")
void should_FailWithInvalidCredentials_When_UserNotFound() {
// Arrange
when(userRepository.findByUsername("nonexistent")).thenReturn(Result.success(Optional.empty()));
// Act
AuthenticateCommand command = new AuthenticateCommand("nonexistent", "Password123!");
Result<UserError, SessionToken> result = authenticateUser.execute(command);
Result<UserError, SessionToken> result = authenticateUser.execute(new AuthenticateCommand("nonexistent", "Password123!"));
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidCredentials.class);
verify(auditLogger).log(eq(AuditEvent.LOGIN_FAILED), anyString());
@ -107,25 +82,14 @@ class AuthenticateUserTest {
@Test
@DisplayName("should_FailWithLockedUser_When_UserStatusIsLocked")
void should_FailWithLockedUser_When_UserStatusIsLocked() {
// Arrange
User lockedUser = User.reconstitute(
UserId.of("user-2"),
"john.doe",
"john@example.com",
validPasswordHash,
new HashSet<>(),
"branch-1",
UserStatus.LOCKED,
LocalDateTime.now(),
null
UserId.of("user-2"), "john.doe", "john@example.com", validPasswordHash,
new HashSet<>(), "branch-1", UserStatus.LOCKED, LocalDateTime.now(), null
);
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(lockedUser)));
// Act
Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.UserLocked.class);
verify(auditLogger).log(eq(AuditEvent.LOGIN_BLOCKED), anyString(), any());
@ -134,209 +98,57 @@ class AuthenticateUserTest {
@Test
@DisplayName("should_FailWithInactiveUser_When_UserStatusIsInactive")
void should_FailWithInactiveUser_When_UserStatusIsInactive() {
// Arrange
User inactiveUser = User.reconstitute(
UserId.of("user-3"),
"john.doe",
"john@example.com",
validPasswordHash,
new HashSet<>(),
"branch-1",
UserStatus.INACTIVE,
LocalDateTime.now(),
null
UserId.of("user-3"), "john.doe", "john@example.com", validPasswordHash,
new HashSet<>(), "branch-1", UserStatus.INACTIVE, LocalDateTime.now(), null
);
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(inactiveUser)));
// Act
Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.UserInactive.class);
verify(auditLogger).log(eq(AuditEvent.LOGIN_FAILED), anyString());
}
@Test
@DisplayName("should_FailWithInvalidCredentials_When_PasswordDoesNotMatch")
void should_FailWithInvalidCredentials_When_PasswordDoesNotMatch() {
// Arrange
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("WrongPassword", validPasswordHash)).thenReturn(false);
// Act
AuthenticateCommand command = new AuthenticateCommand("john.doe", "WrongPassword");
Result<UserError, SessionToken> result = authenticateUser.execute(command);
Result<UserError, SessionToken> result = authenticateUser.execute(new AuthenticateCommand("john.doe", "WrongPassword"));
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidCredentials.class);
verify(auditLogger).log(eq(AuditEvent.LOGIN_FAILED), anyString(), any());
}
@Test
@DisplayName("should_CreateSessionToken_When_AuthenticationSucceeds")
void should_CreateSessionToken_When_AuthenticationSucceeds() {
// Arrange
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("Password123!", validPasswordHash)).thenReturn(true);
when(sessionManager.createSession(testUser)).thenReturn(sessionToken);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);
// Assert
assertThat(result.isSuccess()).isTrue();
verify(sessionManager).createSession(testUser);
}
@Test
@DisplayName("should_UpdateLastLoginTimestamp_When_AuthenticationSucceeds")
void should_UpdateLastLoginTimestamp_When_AuthenticationSucceeds() {
// Arrange
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("Password123!", validPasswordHash)).thenReturn(true);
when(sessionManager.createSession(testUser)).thenReturn(sessionToken);
when(userRepository.save(any())).thenReturn(Result.success(null));
LocalDateTime before = LocalDateTime.now();
// Act
Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);
LocalDateTime after = LocalDateTime.now();
// Assert
assertThat(result.isSuccess()).isTrue();
assertThat(testUser.lastLogin()).isBetween(before, after);
}
@Test
@DisplayName("should_SaveUserWithUpdatedLastLogin_When_AuthenticationSucceeds")
void should_SaveUserWithUpdatedLastLogin_When_AuthenticationSucceeds() {
// Arrange
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("Password123!", validPasswordHash)).thenReturn(true);
when(sessionManager.createSession(testUser)).thenReturn(sessionToken);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
authenticateUser.execute(validCommand);
// Assert
verify(userRepository).save(any(User.class));
}
@Test
@DisplayName("should_LogLoginSuccessAuditEvent_When_AuthenticationSucceeds")
void should_LogLoginSuccessAuditEvent_When_AuthenticationSucceeds() {
// Arrange
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("Password123!", validPasswordHash)).thenReturn(true);
when(sessionManager.createSession(testUser)).thenReturn(sessionToken);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);
// Assert
assertThat(result.isSuccess()).isTrue();
verify(auditLogger).log(eq(AuditEvent.LOGIN_SUCCESS), eq("user-1"), any(ActorId.class));
}
@Test
@DisplayName("should_LogLoginFailureAuditEvent_When_PasswordIncorrect")
void should_LogLoginFailureAuditEvent_When_PasswordIncorrect() {
// Arrange
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("WrongPassword", validPasswordHash)).thenReturn(false);
// Act
AuthenticateCommand command = new AuthenticateCommand("john.doe", "WrongPassword");
Result<UserError, SessionToken> result = authenticateUser.execute(command);
// Assert
assertThat(result.isFailure()).isTrue();
verify(auditLogger).log(eq(AuditEvent.LOGIN_FAILED), anyString(), any());
}
@Test
@DisplayName("should_VerifyPasswordBeforeCheckingStatus_When_UserExists")
void should_VerifyPasswordBeforeCheckingStatus_When_UserExists() {
// Arrange
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("Password123!", validPasswordHash)).thenReturn(true);
when(sessionManager.createSession(testUser)).thenReturn(sessionToken);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
authenticateUser.execute(validCommand);
// Assert
verify(passwordHasher).verify("Password123!", validPasswordHash);
}
@Test
@DisplayName("should_CheckStatusBeforeCreatingSession_When_UserActive")
void should_CheckStatusBeforeCreatingSession_When_UserActive() {
// Arrange
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("Password123!", validPasswordHash)).thenReturn(true);
when(sessionManager.createSession(testUser)).thenReturn(sessionToken);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);
// Assert
assertThat(result.isSuccess()).isTrue();
// Session should be created only for active users
verify(sessionManager).createSession(testUser);
// Verify save was called with a user (immutable, so it's a new instance)
verify(userRepository).save(argThat(user -> user.lastLogin() != null));
}
@Test
@DisplayName("should_NotCreateSession_When_UserLocked")
void should_NotCreateSession_When_UserLocked() {
// Arrange
User lockedUser = User.reconstitute(
UserId.of("user-4"),
"john.doe",
"john@example.com",
validPasswordHash,
new HashSet<>(),
"branch-1",
UserStatus.LOCKED,
LocalDateTime.now(),
null
UserId.of("user-4"), "john.doe", "john@example.com", validPasswordHash,
new HashSet<>(), "branch-1", UserStatus.LOCKED, LocalDateTime.now(), null
);
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(lockedUser)));
// Act
Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);
authenticateUser.execute(validCommand);
// Assert
assertThat(result.isFailure()).isTrue();
verify(sessionManager, never()).createSession(any());
}
@Test
@DisplayName("should_ReturnSessionToken_When_AuthenticationSucceeds")
void should_ReturnSessionToken_When_AuthenticationSucceeds() {
// Arrange
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("Password123!", validPasswordHash)).thenReturn(true);
SessionToken expectedToken = new SessionToken("jwt-xyz", "Bearer", 3600L, LocalDateTime.now().plusSeconds(3600), "refresh-xyz");
when(sessionManager.createSession(testUser)).thenReturn(expectedToken);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, SessionToken> result = authenticateUser.execute(validCommand);
// Assert
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEqualTo(expectedToken);
}
}

View file

@ -4,11 +4,11 @@ import de.effigenix.application.usermanagement.command.ChangePasswordCommand;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ -20,26 +20,16 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for ChangePassword Use Case.
* Tests password verification, password validation, update logic, and audit logging.
*/
@ExtendWith(MockitoExtension.class)
@DisplayName("ChangePassword Use Case")
class ChangePasswordTest {
@Mock
private UserRepository userRepository;
@Mock private UserRepository userRepository;
@Mock private PasswordHasher passwordHasher;
@Mock private AuditLogger auditLogger;
@Mock private AuthorizationPort authPort;
@Mock
private PasswordHasher passwordHasher;
@Mock
private AuditLogger auditLogger;
@InjectMocks
private ChangePassword changePassword;
private User testUser;
private PasswordHash oldPasswordHash;
private PasswordHash newPasswordHash;
@ -48,59 +38,58 @@ class ChangePasswordTest {
@BeforeEach
void setUp() {
changePassword = new ChangePassword(userRepository, passwordHasher, auditLogger, authPort);
oldPasswordHash = new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
newPasswordHash = new PasswordHash("$2b$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
testUser = User.reconstitute(
UserId.of("user-123"),
"john.doe",
"john@example.com",
oldPasswordHash,
new HashSet<>(),
"branch-1",
UserStatus.ACTIVE,
LocalDateTime.now(),
null
UserId.of("user-123"), "john.doe", "john@example.com", oldPasswordHash,
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null
);
validCommand = new ChangePasswordCommand("user-123", "OldPassword123!", "NewPassword456!");
performedBy = ActorId.of("user-123");
performedBy = ActorId.of("user-123"); // self-service
}
@Test
@DisplayName("should_ChangePassword_When_ValidCurrentPasswordProvided")
void should_ChangePassword_When_ValidCurrentPasswordProvided() {
// Arrange
@DisplayName("should_ChangePassword_When_ValidCurrentPasswordProvided_SelfService")
void should_ChangePassword_When_ValidCurrentPasswordProvided_SelfService() {
// Self-service: no authPort check needed
when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("OldPassword123!", oldPasswordHash)).thenReturn(true);
when(passwordHasher.isValidPassword("NewPassword456!")).thenReturn(true);
when(passwordHasher.hash("NewPassword456!")).thenReturn(newPasswordHash);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, Void> result = changePassword.execute(validCommand, performedBy);
// Assert
assertThat(result.isSuccess()).isTrue();
verify(userRepository).save(any(User.class));
verify(auditLogger).log(eq(AuditEvent.PASSWORD_CHANGED), eq("user-123"), eq(performedBy));
}
@Test
@DisplayName("should_FailWithUnauthorized_When_DifferentActorWithoutPermission")
void should_FailWithUnauthorized_When_DifferentActorWithoutPermission() {
ActorId otherActor = ActorId.of("other-user");
when(authPort.can(otherActor, UserManagementAction.PASSWORD_CHANGE)).thenReturn(false);
Result<UserError, Void> result = changePassword.execute(validCommand, otherActor);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_FailWithUserNotFound_When_UserIdDoesNotExist")
void should_FailWithUserNotFound_When_UserIdDoesNotExist() {
// Arrange
when(userRepository.findById(UserId.of("nonexistent-id"))).thenReturn(Result.success(Optional.empty()));
ChangePasswordCommand command = new ChangePasswordCommand("nonexistent-id", "OldPassword123!", "NewPassword456!");
ActorId actor = ActorId.of("nonexistent-id");
ChangePasswordCommand command = new ChangePasswordCommand(
"nonexistent-id",
"OldPassword123!",
"NewPassword456!"
);
Result<UserError, Void> result = changePassword.execute(command, actor);
// Act
Result<UserError, Void> result = changePassword.execute(command, performedBy);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.UserNotFound.class);
verify(userRepository, never()).save(any());
@ -109,235 +98,41 @@ class ChangePasswordTest {
@Test
@DisplayName("should_FailWithInvalidCredentials_When_CurrentPasswordIncorrect")
void should_FailWithInvalidCredentials_When_CurrentPasswordIncorrect() {
// Arrange
when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("WrongPassword", oldPasswordHash)).thenReturn(false);
ChangePasswordCommand command = new ChangePasswordCommand("user-123", "WrongPassword", "NewPassword456!");
ChangePasswordCommand command = new ChangePasswordCommand(
"user-123",
"WrongPassword",
"NewPassword456!"
);
// Act
Result<UserError, Void> result = changePassword.execute(command, performedBy);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidCredentials.class);
verify(auditLogger).log(eq(AuditEvent.PASSWORD_CHANGE_FAILED), eq("user-123"), eq(performedBy));
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_FailWithInvalidPassword_When_NewPasswordTooWeak")
void should_FailWithInvalidPassword_When_NewPasswordTooWeak() {
// Arrange
when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("OldPassword123!", oldPasswordHash)).thenReturn(true);
when(passwordHasher.isValidPassword("weak")).thenReturn(false);
ChangePasswordCommand command = new ChangePasswordCommand("user-123", "OldPassword123!", "weak");
// Act
Result<UserError, Void> result = changePassword.execute(command, performedBy);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidPassword.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_VerifyCurrentPasswordBeforeValidatingNewPassword")
void should_VerifyCurrentPasswordBeforeValidatingNewPassword() {
// Arrange
when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("OldPassword123!", oldPasswordHash)).thenReturn(false);
@DisplayName("should_FailWithInvalidInput_When_BlankUserId")
void should_FailWithInvalidInput_When_BlankUserId() {
ChangePasswordCommand command = new ChangePasswordCommand("", "OldPassword123!", "NewPassword456!");
// Act
Result<UserError, Void> result = changePassword.execute(validCommand, performedBy);
// Assert
assertThat(result.isFailure()).isTrue();
// Password validation should not be called if current password is wrong
verify(passwordHasher, never()).isValidPassword(anyString());
}
@Test
@DisplayName("should_HashNewPassword_When_AllValidationsPass")
void should_HashNewPassword_When_AllValidationsPass() {
// Arrange
when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("OldPassword123!", oldPasswordHash)).thenReturn(true);
when(passwordHasher.isValidPassword("NewPassword456!")).thenReturn(true);
when(passwordHasher.hash("NewPassword456!")).thenReturn(newPasswordHash);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
changePassword.execute(validCommand, performedBy);
// Assert
verify(passwordHasher).hash("NewPassword456!");
}
@Test
@DisplayName("should_UpdateUserPassword_When_NewHashObtained")
void should_UpdateUserPassword_When_NewHashObtained() {
// Arrange
when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("OldPassword123!", oldPasswordHash)).thenReturn(true);
when(passwordHasher.isValidPassword("NewPassword456!")).thenReturn(true);
when(passwordHasher.hash("NewPassword456!")).thenReturn(newPasswordHash);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, Void> result = changePassword.execute(validCommand, performedBy);
// Assert
assertThat(result.isSuccess()).isTrue();
assertThat(testUser.passwordHash()).isEqualTo(newPasswordHash);
}
@Test
@DisplayName("should_SaveUpdatedUserToRepository_When_PasswordChanged")
void should_SaveUpdatedUserToRepository_When_PasswordChanged() {
// Arrange
when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("OldPassword123!", oldPasswordHash)).thenReturn(true);
when(passwordHasher.isValidPassword("NewPassword456!")).thenReturn(true);
when(passwordHasher.hash("NewPassword456!")).thenReturn(newPasswordHash);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
changePassword.execute(validCommand, performedBy);
// Assert
verify(userRepository).save(any(User.class));
}
@Test
@DisplayName("should_LogPasswordChangedAuditEvent_When_PasswordSuccessfullyChanged")
void should_LogPasswordChangedAuditEvent_When_PasswordSuccessfullyChanged() {
// Arrange
when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("OldPassword123!", oldPasswordHash)).thenReturn(true);
when(passwordHasher.isValidPassword("NewPassword456!")).thenReturn(true);
when(passwordHasher.hash("NewPassword456!")).thenReturn(newPasswordHash);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, Void> result = changePassword.execute(validCommand, performedBy);
// Assert
assertThat(result.isSuccess()).isTrue();
verify(auditLogger).log(eq(AuditEvent.PASSWORD_CHANGED), eq("user-123"), eq(performedBy));
}
@Test
@DisplayName("should_LogFailureAuditEvent_When_CurrentPasswordIncorrect")
void should_LogFailureAuditEvent_When_CurrentPasswordIncorrect() {
// Arrange
when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("WrongPassword", oldPasswordHash)).thenReturn(false);
ChangePasswordCommand command = new ChangePasswordCommand(
"user-123",
"WrongPassword",
"NewPassword456!"
);
// Act
Result<UserError, Void> result = changePassword.execute(command, performedBy);
// Assert
assertThat(result.isFailure()).isTrue();
verify(auditLogger).log(
eq(AuditEvent.PASSWORD_CHANGED),
eq("user-123"),
eq(performedBy)
);
}
@Test
@DisplayName("should_ReturnSuccess_When_PasswordChangedSuccessfully")
void should_ReturnSuccess_When_PasswordChangedSuccessfully() {
// Arrange
when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("OldPassword123!", oldPasswordHash)).thenReturn(true);
when(passwordHasher.isValidPassword("NewPassword456!")).thenReturn(true);
when(passwordHasher.hash("NewPassword456!")).thenReturn(newPasswordHash);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, Void> result = changePassword.execute(validCommand, performedBy);
// Assert
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isNull();
}
@Test
@DisplayName("should_ReturnFailure_When_UserNotFound")
void should_ReturnFailure_When_UserNotFound() {
// Arrange
when(userRepository.findById(UserId.of("invalid-id"))).thenReturn(Result.success(Optional.empty()));
ChangePasswordCommand command = new ChangePasswordCommand(
"invalid-id",
"OldPassword123!",
"NewPassword456!"
);
// Act
Result<UserError, Void> result = changePassword.execute(command, performedBy);
// Assert
assertThat(result.isFailure()).isTrue();
}
@Test
@DisplayName("should_AllowPasswordChangeForActiveUser")
void should_AllowPasswordChangeForActiveUser() {
// Arrange
testUser = User.reconstitute(
UserId.of("user-123"),
"john.doe",
"john@example.com",
oldPasswordHash,
new HashSet<>(),
"branch-1",
UserStatus.ACTIVE,
LocalDateTime.now(),
null
);
when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("OldPassword123!", oldPasswordHash)).thenReturn(true);
when(passwordHasher.isValidPassword("NewPassword456!")).thenReturn(true);
when(passwordHasher.hash("NewPassword456!")).thenReturn(newPasswordHash);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, Void> result = changePassword.execute(validCommand, performedBy);
// Assert
assertThat(result.isSuccess()).isTrue();
}
@Test
@DisplayName("should_UsePasswordHasherToVerifyCurrentPassword")
void should_UsePasswordHasherToVerifyCurrentPassword() {
// Arrange
when(userRepository.findById(UserId.of("user-123"))).thenReturn(Result.success(Optional.of(testUser)));
when(passwordHasher.verify("OldPassword123!", oldPasswordHash)).thenReturn(true);
when(passwordHasher.isValidPassword("NewPassword456!")).thenReturn(true);
when(passwordHasher.hash("NewPassword456!")).thenReturn(newPasswordHash);
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
changePassword.execute(validCommand, performedBy);
// Assert
verify(passwordHasher).verify("OldPassword123!", oldPasswordHash);
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidInput.class);
}
}

View file

@ -4,11 +4,11 @@ import de.effigenix.application.usermanagement.command.CreateUserCommand;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ -21,58 +21,35 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
/**
* Unit tests for CreateUser Use Case.
* Tests validation, uniqueness checks, role loading, user creation, and audit logging.
*/
@ExtendWith(MockitoExtension.class)
@DisplayName("CreateUser Use Case")
class CreateUserTest {
@Mock
private UserRepository userRepository;
@Mock private UserRepository userRepository;
@Mock private RoleRepository roleRepository;
@Mock private PasswordHasher passwordHasher;
@Mock private AuditLogger auditLogger;
@Mock private AuthorizationPort authPort;
@Mock
private RoleRepository roleRepository;
@Mock
private PasswordHasher passwordHasher;
@Mock
private AuditLogger auditLogger;
@InjectMocks
private CreateUser createUser;
private CreateUserCommand validCommand;
private ActorId performedBy;
private PasswordHash validPasswordHash;
@BeforeEach
void setUp() {
createUser = new CreateUser(userRepository, roleRepository, passwordHasher, auditLogger, authPort);
performedBy = ActorId.of("admin-user");
validPasswordHash = new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
validCommand = new CreateUserCommand(
"john.doe",
"john@example.com",
"Password123!",
Set.of(RoleName.PRODUCTION_WORKER),
"branch-1"
);
validCommand = new CreateUserCommand("john.doe", "john@example.com", "Password123!", Set.of(RoleName.PRODUCTION_WORKER), "branch-1");
}
@Test
@DisplayName("should_CreateUser_When_ValidCommandAndUniqueDetailsProvided")
void should_CreateUser_When_ValidCommandAndUniqueDetailsProvided() {
// Arrange
Role role = Role.reconstitute(
RoleId.generate(),
RoleName.PRODUCTION_WORKER,
new HashSet<>(),
"Production Worker"
);
Role role = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, new HashSet<>(), "Production Worker");
when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(false));
@ -80,157 +57,103 @@ class CreateUserTest {
when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.of(role)));
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, de.effigenix.application.usermanagement.dto.UserDTO> result =
createUser.execute(validCommand, performedBy);
var result = createUser.execute(validCommand, performedBy);
// Assert
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().username()).isEqualTo("john.doe");
assertThat(result.unsafeGetValue().email()).isEqualTo("john@example.com");
verify(userRepository).save(any());
verify(auditLogger).log(AuditEvent.USER_CREATED, result.unsafeGetValue().id(), performedBy);
verify(auditLogger).log(eq(AuditEvent.USER_CREATED), anyString(), eq(performedBy));
}
@Test
@DisplayName("should_FailWithUnauthorized_When_ActorLacksPermission")
void should_FailWithUnauthorized_When_ActorLacksPermission() {
when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(false);
var result = createUser.execute(validCommand, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_FailWithInvalidInput_When_EmptyRoleNamesProvided")
void should_FailWithInvalidInput_When_EmptyRoleNamesProvided() {
when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
var cmd = new CreateUserCommand("john.doe", "john@example.com", "Password123!", Set.of(), "branch-1");
var result = createUser.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidInput.class);
}
@Test
@DisplayName("should_FailWithInvalidPassword_When_WeakPasswordProvided")
void should_FailWithInvalidPassword_When_WeakPasswordProvided() {
// Arrange
when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
when(passwordHasher.isValidPassword("Password123!")).thenReturn(false);
// Act
Result<UserError, de.effigenix.application.usermanagement.dto.UserDTO> result =
createUser.execute(validCommand, performedBy);
var result = createUser.execute(validCommand, performedBy);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidPassword.class);
assertThat(result.unsafeGetError().message()).contains("at least 8 characters");
verify(userRepository, never()).save(any());
verify(auditLogger, never()).log(any(AuditEvent.class), anyString(), any());
}
@Test
@DisplayName("should_FailWithUsernameExists_When_DuplicateUsernameProvided")
void should_FailWithUsernameExists_When_DuplicateUsernameProvided() {
// Arrange
when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(true));
// Act
Result<UserError, de.effigenix.application.usermanagement.dto.UserDTO> result =
createUser.execute(validCommand, performedBy);
var result = createUser.execute(validCommand, performedBy);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.UsernameAlreadyExists.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_FailWithEmailExists_When_DuplicateEmailProvided")
void should_FailWithEmailExists_When_DuplicateEmailProvided() {
// Arrange
when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(true));
// Act
Result<UserError, de.effigenix.application.usermanagement.dto.UserDTO> result =
createUser.execute(validCommand, performedBy);
var result = createUser.execute(validCommand, performedBy);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.EmailAlreadyExists.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_UsePasswordHasherToHashPassword_When_PasswordValid")
void should_UsePasswordHasherToHashPassword_When_PasswordValid() {
// Arrange
Role role = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, new HashSet<>(), "Worker");
when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(false));
when(passwordHasher.hash("Password123!")).thenReturn(validPasswordHash);
when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.of(role)));
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, de.effigenix.application.usermanagement.dto.UserDTO> result =
createUser.execute(validCommand, performedBy);
// Assert
assertThat(result.isSuccess()).isTrue();
verify(passwordHasher).hash("Password123!");
}
@Test
@DisplayName("should_LoadRolesByName_When_RoleNamesProvided")
void should_LoadRolesByName_When_RoleNamesProvided() {
// Arrange
Role role1 = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, new HashSet<>(), "Worker");
Role role2 = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_MANAGER, new HashSet<>(), "Manager");
CreateUserCommand commandWithMultipleRoles = new CreateUserCommand(
"john.doe",
"john@example.com",
"Password123!",
Set.of(RoleName.PRODUCTION_WORKER, RoleName.PRODUCTION_MANAGER),
"branch-1"
);
when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(false));
when(passwordHasher.hash("Password123!")).thenReturn(validPasswordHash);
when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.of(role1)));
when(roleRepository.findByName(RoleName.PRODUCTION_MANAGER)).thenReturn(Result.success(Optional.of(role2)));
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, de.effigenix.application.usermanagement.dto.UserDTO> result =
createUser.execute(commandWithMultipleRoles, performedBy);
// Assert
assertThat(result.isSuccess()).isTrue();
verify(roleRepository).findByName(RoleName.PRODUCTION_WORKER);
verify(roleRepository).findByName(RoleName.PRODUCTION_MANAGER);
}
@Test
@DisplayName("should_FailWithRoleNotFound_When_RoleNotFound")
void should_FailWithRoleNotFound_When_RoleNotFound() {
// Arrange
when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(false));
when(passwordHasher.hash("Password123!")).thenReturn(validPasswordHash);
when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.empty()));
// Act
Result<UserError, de.effigenix.application.usermanagement.dto.UserDTO> result =
createUser.execute(validCommand, performedBy);
var result = createUser.execute(validCommand, performedBy);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.RoleNotFound.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_CreateActiveUser_When_UserCreatedWithFactoryMethod")
void should_CreateActiveUser_When_UserCreatedWithFactoryMethod() {
// Arrange
@DisplayName("should_CreateActiveUser_When_UserCreatedSuccessfully")
void should_CreateActiveUser_When_UserCreatedSuccessfully() {
Role role = Role.reconstitute(RoleId.generate(), RoleName.ADMIN, new HashSet<>(), "Admin");
when(authPort.can(performedBy, UserManagementAction.USER_CREATE)).thenReturn(true);
when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(false));
@ -238,79 +161,9 @@ class CreateUserTest {
when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.of(role)));
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, de.effigenix.application.usermanagement.dto.UserDTO> result =
createUser.execute(validCommand, performedBy);
var result = createUser.execute(validCommand, performedBy);
// Assert
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().status()).isEqualTo(UserStatus.ACTIVE);
}
@Test
@DisplayName("should_LogUserCreatedAuditEvent_When_UserSuccessfullyCreated")
void should_LogUserCreatedAuditEvent_When_UserSuccessfullyCreated() {
// Arrange
Role role = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, new HashSet<>(), "Worker");
when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(false));
when(passwordHasher.hash("Password123!")).thenReturn(validPasswordHash);
when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.of(role)));
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, de.effigenix.application.usermanagement.dto.UserDTO> result =
createUser.execute(validCommand, performedBy);
// Assert
assertThat(result.isSuccess()).isTrue();
verify(auditLogger).log(eq(AuditEvent.USER_CREATED), anyString(), eq(performedBy));
}
@Test
@DisplayName("should_SaveUserToRepository_When_AllValidationsPass")
void should_SaveUserToRepository_When_AllValidationsPass() {
// Arrange
Role role = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, new HashSet<>(), "Worker");
when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(false));
when(passwordHasher.hash("Password123!")).thenReturn(validPasswordHash);
when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.of(role)));
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
createUser.execute(validCommand, performedBy);
// Assert
verify(userRepository).save(any(User.class));
}
@Test
@DisplayName("should_ReturnUserDTO_When_UserCreatedSuccessfully")
void should_ReturnUserDTO_When_UserCreatedSuccessfully() {
// Arrange
Role role = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, new HashSet<>(), "Worker");
when(passwordHasher.isValidPassword("Password123!")).thenReturn(true);
when(userRepository.existsByUsername("john.doe")).thenReturn(Result.success(false));
when(userRepository.existsByEmail("john@example.com")).thenReturn(Result.success(false));
when(passwordHasher.hash("Password123!")).thenReturn(validPasswordHash);
when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.of(role)));
when(userRepository.save(any())).thenReturn(Result.success(null));
// Act
Result<UserError, de.effigenix.application.usermanagement.dto.UserDTO> result =
createUser.execute(validCommand, performedBy);
// Assert
assertThat(result.isSuccess()).isTrue();
var userDTO = result.unsafeGetValue();
assertThat(userDTO.username()).isEqualTo("john.doe");
assertThat(userDTO.email()).isEqualTo("john@example.com");
assertThat(userDTO.status()).isEqualTo(UserStatus.ACTIVE);
}
}

View file

@ -0,0 +1,81 @@
package de.effigenix.application.usermanagement;
import de.effigenix.application.usermanagement.dto.UserDTO;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("GetUser Use Case")
class GetUserTest {
@Mock private UserRepository userRepository;
@Mock private AuthorizationPort authPort;
private GetUser getUser;
private ActorId performedBy;
private User testUser;
@BeforeEach
void setUp() {
getUser = new GetUser(userRepository, authPort);
performedBy = ActorId.of("admin-user");
testUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null
);
}
@Test
@DisplayName("should_ReturnUser_When_UserExistsAndAuthorized")
void should_ReturnUser_When_UserExistsAndAuthorized() {
when(authPort.can(performedBy, UserManagementAction.USER_VIEW)).thenReturn(true);
when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
Result<UserError, UserDTO> result = getUser.execute("user-1", performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().username()).isEqualTo("john.doe");
assertThat(result.unsafeGetValue().email()).isEqualTo("john@example.com");
assertThat(result.unsafeGetValue().id()).isEqualTo("user-1");
}
@Test
@DisplayName("should_FailWithUnauthorized_When_ActorLacksPermission")
void should_FailWithUnauthorized_When_ActorLacksPermission() {
when(authPort.can(performedBy, UserManagementAction.USER_VIEW)).thenReturn(false);
Result<UserError, UserDTO> result = getUser.execute("user-1", performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
verify(userRepository, never()).findById(any());
}
@Test
@DisplayName("should_FailWithUserNotFound_When_UserDoesNotExist")
void should_FailWithUserNotFound_When_UserDoesNotExist() {
when(authPort.can(performedBy, UserManagementAction.USER_VIEW)).thenReturn(true);
when(userRepository.findById(UserId.of("nonexistent"))).thenReturn(Result.success(Optional.empty()));
Result<UserError, UserDTO> result = getUser.execute("nonexistent", performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.UserNotFound.class);
}
}

View file

@ -0,0 +1,112 @@
package de.effigenix.application.usermanagement;
import de.effigenix.application.usermanagement.dto.UserDTO;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort;
import de.effigenix.shared.security.BranchId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("ListUsers Use Case")
class ListUsersTest {
@Mock private UserRepository userRepository;
@Mock private AuthorizationPort authPort;
private ListUsers listUsers;
private ActorId performedBy;
private User user1;
private User user2;
@BeforeEach
void setUp() {
listUsers = new ListUsers(userRepository, authPort);
performedBy = ActorId.of("admin-user");
user1 = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null
);
user2 = User.reconstitute(
UserId.of("user-2"), "jane.doe", "jane@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null
);
}
@Test
@DisplayName("should_ReturnAllUsers_When_Authorized")
void should_ReturnAllUsers_When_Authorized() {
when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(true);
when(userRepository.findAll()).thenReturn(Result.success(List.of(user1, user2)));
Result<UserError, List<UserDTO>> result = listUsers.execute(performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2);
assertThat(result.unsafeGetValue()).extracting(UserDTO::username)
.containsExactlyInAnyOrder("john.doe", "jane.doe");
}
@Test
@DisplayName("should_ReturnEmptyList_When_NoUsersExist")
void should_ReturnEmptyList_When_NoUsersExist() {
when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(true);
when(userRepository.findAll()).thenReturn(Result.success(List.of()));
Result<UserError, List<UserDTO>> result = listUsers.execute(performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty();
}
@Test
@DisplayName("should_FailWithUnauthorized_When_ActorLacksPermission")
void should_FailWithUnauthorized_When_ActorLacksPermission() {
when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(false);
Result<UserError, List<UserDTO>> result = listUsers.execute(performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
verify(userRepository, never()).findAll();
}
@Test
@DisplayName("should_ReturnBranchUsers_When_FilteredByBranch")
void should_ReturnBranchUsers_When_FilteredByBranch() {
when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(true);
when(userRepository.findByBranchId("branch-1")).thenReturn(Result.success(List.of(user1, user2)));
Result<UserError, List<UserDTO>> result = listUsers.executeForBranch(BranchId.of("branch-1"), performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2);
}
@Test
@DisplayName("should_FailWithUnauthorized_When_ActorLacksPermissionForBranchList")
void should_FailWithUnauthorized_When_ActorLacksPermissionForBranchList() {
when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(false);
Result<UserError, List<UserDTO>> result = listUsers.executeForBranch(BranchId.of("branch-1"), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
verify(userRepository, never()).findByBranchId(anyString());
}
}

View file

@ -0,0 +1,131 @@
package de.effigenix.application.usermanagement;
import de.effigenix.application.usermanagement.command.LockUserCommand;
import de.effigenix.application.usermanagement.dto.UserDTO;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("LockUser Use Case")
class LockUserTest {
@Mock private UserRepository userRepository;
@Mock private AuditLogger auditLogger;
@Mock private AuthorizationPort authPort;
private LockUser lockUser;
private ActorId performedBy;
private User activeUser;
@BeforeEach
void setUp() {
lockUser = new LockUser(userRepository, auditLogger, authPort);
performedBy = ActorId.of("admin-user");
activeUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null
);
}
@Test
@DisplayName("should_LockUser_When_UserIsActive")
void should_LockUser_When_UserIsActive() {
when(authPort.can(performedBy, UserManagementAction.USER_LOCK)).thenReturn(true);
when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(activeUser)));
when(userRepository.save(any())).thenReturn(Result.success(null));
Result<UserError, UserDTO> result = lockUser.execute(new LockUserCommand("user-1"), performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().status()).isEqualTo(UserStatus.LOCKED);
verify(userRepository).save(argThat(user -> user.status() == UserStatus.LOCKED));
verify(auditLogger).log(eq(AuditEvent.USER_LOCKED), eq("user-1"), eq(performedBy));
}
@Test
@DisplayName("should_FailWithUnauthorized_When_ActorLacksPermission")
void should_FailWithUnauthorized_When_ActorLacksPermission() {
when(authPort.can(performedBy, UserManagementAction.USER_LOCK)).thenReturn(false);
Result<UserError, UserDTO> result = lockUser.execute(new LockUserCommand("user-1"), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_FailWithInvalidInput_When_UserIdIsBlank")
void should_FailWithInvalidInput_When_UserIdIsBlank() {
when(authPort.can(performedBy, UserManagementAction.USER_LOCK)).thenReturn(true);
Result<UserError, UserDTO> result = lockUser.execute(new LockUserCommand(""), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidInput.class);
}
@Test
@DisplayName("should_FailWithUserNotFound_When_UserDoesNotExist")
void should_FailWithUserNotFound_When_UserDoesNotExist() {
when(authPort.can(performedBy, UserManagementAction.USER_LOCK)).thenReturn(true);
when(userRepository.findById(UserId.of("nonexistent"))).thenReturn(Result.success(Optional.empty()));
Result<UserError, UserDTO> result = lockUser.execute(new LockUserCommand("nonexistent"), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.UserNotFound.class);
}
@Test
@DisplayName("should_FailWithInvalidStatusTransition_When_UserAlreadyLocked")
void should_FailWithInvalidStatusTransition_When_UserAlreadyLocked() {
User lockedUser = User.reconstitute(
UserId.of("user-2"), "jane.doe", "jane@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.LOCKED, LocalDateTime.now(), null
);
when(authPort.can(performedBy, UserManagementAction.USER_LOCK)).thenReturn(true);
when(userRepository.findById(UserId.of("user-2"))).thenReturn(Result.success(Optional.of(lockedUser)));
Result<UserError, UserDTO> result = lockUser.execute(new LockUserCommand("user-2"), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidStatusTransition.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_FailWithInvalidStatusTransition_When_UserIsInactive")
void should_FailWithInvalidStatusTransition_When_UserIsInactive() {
User inactiveUser = User.reconstitute(
UserId.of("user-3"), "bob", "bob@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.INACTIVE, LocalDateTime.now(), null
);
when(authPort.can(performedBy, UserManagementAction.USER_LOCK)).thenReturn(true);
when(userRepository.findById(UserId.of("user-3"))).thenReturn(Result.success(Optional.of(inactiveUser)));
Result<UserError, UserDTO> result = lockUser.execute(new LockUserCommand("user-3"), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidStatusTransition.class);
}
}

View file

@ -0,0 +1,132 @@
package de.effigenix.application.usermanagement;
import de.effigenix.application.usermanagement.command.RemoveRoleCommand;
import de.effigenix.application.usermanagement.dto.UserDTO;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("RemoveRole Use Case")
class RemoveRoleTest {
@Mock private UserRepository userRepository;
@Mock private RoleRepository roleRepository;
@Mock private AuditLogger auditLogger;
@Mock private AuthorizationPort authPort;
private RemoveRole removeRole;
private ActorId performedBy;
private Role workerRole;
private User userWithRole;
@BeforeEach
void setUp() {
removeRole = new RemoveRole(userRepository, roleRepository, auditLogger, authPort);
performedBy = ActorId.of("admin-user");
workerRole = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, new HashSet<>(), "Production Worker");
userWithRole = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(Set.of(workerRole)), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null
);
}
@Test
@DisplayName("should_RemoveRole_When_ValidCommandProvided")
void should_RemoveRole_When_ValidCommandProvided() {
when(authPort.can(performedBy, UserManagementAction.ROLE_REMOVE)).thenReturn(true);
when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(userWithRole)));
when(roleRepository.findByName(RoleName.PRODUCTION_WORKER)).thenReturn(Result.success(Optional.of(workerRole)));
when(userRepository.save(any())).thenReturn(Result.success(null));
Result<UserError, UserDTO> result = removeRole.execute(
new RemoveRoleCommand("user-1", RoleName.PRODUCTION_WORKER), performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().roles()).isEmpty();
verify(userRepository).save(argThat(user -> user.roles().isEmpty()));
verify(auditLogger).log(eq(AuditEvent.ROLE_REMOVED), eq("user-1"), anyString(), eq(performedBy));
}
@Test
@DisplayName("should_FailWithUnauthorized_When_ActorLacksPermission")
void should_FailWithUnauthorized_When_ActorLacksPermission() {
when(authPort.can(performedBy, UserManagementAction.ROLE_REMOVE)).thenReturn(false);
Result<UserError, UserDTO> result = removeRole.execute(
new RemoveRoleCommand("user-1", RoleName.PRODUCTION_WORKER), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_FailWithInvalidInput_When_UserIdIsBlank")
void should_FailWithInvalidInput_When_UserIdIsBlank() {
when(authPort.can(performedBy, UserManagementAction.ROLE_REMOVE)).thenReturn(true);
Result<UserError, UserDTO> result = removeRole.execute(
new RemoveRoleCommand("", RoleName.PRODUCTION_WORKER), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidInput.class);
}
@Test
@DisplayName("should_FailWithInvalidInput_When_RoleNameIsNull")
void should_FailWithInvalidInput_When_RoleNameIsNull() {
when(authPort.can(performedBy, UserManagementAction.ROLE_REMOVE)).thenReturn(true);
Result<UserError, UserDTO> result = removeRole.execute(
new RemoveRoleCommand("user-1", null), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidInput.class);
}
@Test
@DisplayName("should_FailWithUserNotFound_When_UserDoesNotExist")
void should_FailWithUserNotFound_When_UserDoesNotExist() {
when(authPort.can(performedBy, UserManagementAction.ROLE_REMOVE)).thenReturn(true);
when(userRepository.findById(UserId.of("nonexistent"))).thenReturn(Result.success(Optional.empty()));
Result<UserError, UserDTO> result = removeRole.execute(
new RemoveRoleCommand("nonexistent", RoleName.PRODUCTION_WORKER), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.UserNotFound.class);
}
@Test
@DisplayName("should_FailWithRoleNotFound_When_RoleDoesNotExist")
void should_FailWithRoleNotFound_When_RoleDoesNotExist() {
when(authPort.can(performedBy, UserManagementAction.ROLE_REMOVE)).thenReturn(true);
when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(userWithRole)));
when(roleRepository.findByName(RoleName.ADMIN)).thenReturn(Result.success(Optional.empty()));
Result<UserError, UserDTO> result = removeRole.execute(
new RemoveRoleCommand("user-1", RoleName.ADMIN), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.RoleNotFound.class);
verify(userRepository, never()).save(any());
}
}

View file

@ -0,0 +1,131 @@
package de.effigenix.application.usermanagement;
import de.effigenix.application.usermanagement.command.UnlockUserCommand;
import de.effigenix.application.usermanagement.dto.UserDTO;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("UnlockUser Use Case")
class UnlockUserTest {
@Mock private UserRepository userRepository;
@Mock private AuditLogger auditLogger;
@Mock private AuthorizationPort authPort;
private UnlockUser unlockUser;
private ActorId performedBy;
private User lockedUser;
@BeforeEach
void setUp() {
unlockUser = new UnlockUser(userRepository, auditLogger, authPort);
performedBy = ActorId.of("admin-user");
lockedUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.LOCKED, LocalDateTime.now(), null
);
}
@Test
@DisplayName("should_UnlockUser_When_UserIsLocked")
void should_UnlockUser_When_UserIsLocked() {
when(authPort.can(performedBy, UserManagementAction.USER_UNLOCK)).thenReturn(true);
when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(lockedUser)));
when(userRepository.save(any())).thenReturn(Result.success(null));
Result<UserError, UserDTO> result = unlockUser.execute(new UnlockUserCommand("user-1"), performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().status()).isEqualTo(UserStatus.ACTIVE);
verify(userRepository).save(argThat(user -> user.status() == UserStatus.ACTIVE));
verify(auditLogger).log(eq(AuditEvent.USER_UNLOCKED), eq("user-1"), eq(performedBy));
}
@Test
@DisplayName("should_FailWithUnauthorized_When_ActorLacksPermission")
void should_FailWithUnauthorized_When_ActorLacksPermission() {
when(authPort.can(performedBy, UserManagementAction.USER_UNLOCK)).thenReturn(false);
Result<UserError, UserDTO> result = unlockUser.execute(new UnlockUserCommand("user-1"), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_FailWithInvalidInput_When_UserIdIsBlank")
void should_FailWithInvalidInput_When_UserIdIsBlank() {
when(authPort.can(performedBy, UserManagementAction.USER_UNLOCK)).thenReturn(true);
Result<UserError, UserDTO> result = unlockUser.execute(new UnlockUserCommand(""), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidInput.class);
}
@Test
@DisplayName("should_FailWithUserNotFound_When_UserDoesNotExist")
void should_FailWithUserNotFound_When_UserDoesNotExist() {
when(authPort.can(performedBy, UserManagementAction.USER_UNLOCK)).thenReturn(true);
when(userRepository.findById(UserId.of("nonexistent"))).thenReturn(Result.success(Optional.empty()));
Result<UserError, UserDTO> result = unlockUser.execute(new UnlockUserCommand("nonexistent"), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.UserNotFound.class);
}
@Test
@DisplayName("should_FailWithInvalidStatusTransition_When_UserIsActive")
void should_FailWithInvalidStatusTransition_When_UserIsActive() {
User activeUser = User.reconstitute(
UserId.of("user-2"), "jane.doe", "jane@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null
);
when(authPort.can(performedBy, UserManagementAction.USER_UNLOCK)).thenReturn(true);
when(userRepository.findById(UserId.of("user-2"))).thenReturn(Result.success(Optional.of(activeUser)));
Result<UserError, UserDTO> result = unlockUser.execute(new UnlockUserCommand("user-2"), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidStatusTransition.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_FailWithInvalidStatusTransition_When_UserIsInactive")
void should_FailWithInvalidStatusTransition_When_UserIsInactive() {
User inactiveUser = User.reconstitute(
UserId.of("user-3"), "bob", "bob@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.INACTIVE, LocalDateTime.now(), null
);
when(authPort.can(performedBy, UserManagementAction.USER_UNLOCK)).thenReturn(true);
when(userRepository.findById(UserId.of("user-3"))).thenReturn(Result.success(Optional.of(inactiveUser)));
Result<UserError, UserDTO> result = unlockUser.execute(new UnlockUserCommand("user-3"), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidStatusTransition.class);
}
}

View file

@ -0,0 +1,149 @@
package de.effigenix.application.usermanagement;
import de.effigenix.application.usermanagement.command.UpdateUserCommand;
import de.effigenix.application.usermanagement.dto.UserDTO;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("UpdateUser Use Case")
class UpdateUserTest {
@Mock private UserRepository userRepository;
@Mock private AuditLogger auditLogger;
@Mock private AuthorizationPort authPort;
private UpdateUser updateUser;
private ActorId performedBy;
private User testUser;
@BeforeEach
void setUp() {
updateUser = new UpdateUser(userRepository, auditLogger, authPort);
performedBy = ActorId.of("admin-user");
testUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null
);
}
@Test
@DisplayName("should_UpdateEmail_When_NewEmailProvided")
void should_UpdateEmail_When_NewEmailProvided() {
when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(true);
when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
when(userRepository.existsByEmail("newemail@example.com")).thenReturn(Result.success(false));
when(userRepository.save(any())).thenReturn(Result.success(null));
Result<UserError, UserDTO> result = updateUser.execute(
new UpdateUserCommand("user-1", "newemail@example.com", null), performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().email()).isEqualTo("newemail@example.com");
verify(userRepository).save(argThat(user -> "newemail@example.com".equals(user.email())));
verify(auditLogger).log(eq(AuditEvent.USER_UPDATED), eq("user-1"), eq(performedBy));
}
@Test
@DisplayName("should_UpdateBranch_When_NewBranchProvided")
void should_UpdateBranch_When_NewBranchProvided() {
when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(true);
when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
when(userRepository.save(any())).thenReturn(Result.success(null));
Result<UserError, UserDTO> result = updateUser.execute(
new UpdateUserCommand("user-1", null, "branch-2"), performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().branchId()).isEqualTo("branch-2");
verify(userRepository).save(argThat(user -> "branch-2".equals(user.branchId())));
}
@Test
@DisplayName("should_UpdateEmailAndBranch_When_BothProvided")
void should_UpdateEmailAndBranch_When_BothProvided() {
when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(true);
when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
when(userRepository.existsByEmail("new@example.com")).thenReturn(Result.success(false));
when(userRepository.save(any())).thenReturn(Result.success(null));
Result<UserError, UserDTO> result = updateUser.execute(
new UpdateUserCommand("user-1", "new@example.com", "branch-2"), performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().email()).isEqualTo("new@example.com");
assertThat(result.unsafeGetValue().branchId()).isEqualTo("branch-2");
}
@Test
@DisplayName("should_FailWithUnauthorized_When_ActorLacksPermission")
void should_FailWithUnauthorized_When_ActorLacksPermission() {
when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(false);
Result<UserError, UserDTO> result = updateUser.execute(
new UpdateUserCommand("user-1", "new@example.com", null), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_FailWithUserNotFound_When_UserDoesNotExist")
void should_FailWithUserNotFound_When_UserDoesNotExist() {
when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(true);
when(userRepository.findById(UserId.of("nonexistent"))).thenReturn(Result.success(Optional.empty()));
Result<UserError, UserDTO> result = updateUser.execute(
new UpdateUserCommand("nonexistent", "new@example.com", null), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.UserNotFound.class);
}
@Test
@DisplayName("should_FailWithEmailAlreadyExists_When_DuplicateEmail")
void should_FailWithEmailAlreadyExists_When_DuplicateEmail() {
when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(true);
when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
when(userRepository.existsByEmail("taken@example.com")).thenReturn(Result.success(true));
Result<UserError, UserDTO> result = updateUser.execute(
new UpdateUserCommand("user-1", "taken@example.com", null), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.EmailAlreadyExists.class);
verify(userRepository, never()).save(any());
}
@Test
@DisplayName("should_NotCheckEmailUniqueness_When_EmailUnchanged")
void should_NotCheckEmailUniqueness_When_EmailUnchanged() {
when(authPort.can(performedBy, UserManagementAction.USER_UPDATE)).thenReturn(true);
when(userRepository.findById(UserId.of("user-1"))).thenReturn(Result.success(Optional.of(testUser)));
when(userRepository.save(any())).thenReturn(Result.success(null));
Result<UserError, UserDTO> result = updateUser.execute(
new UpdateUserCommand("user-1", "john@example.com", "branch-2"), performedBy);
assertThat(result.isSuccess()).isTrue();
verify(userRepository, never()).existsByEmail(anyString());
}
}

View file

@ -11,7 +11,7 @@ import static org.assertj.core.api.Assertions.*;
/**
* Unit tests for Role Entity.
* Tests validation, permission management, factory methods, and equality.
* Tests validation, immutable permission management, factory methods, and equality.
*/
@DisplayName("Role Entity")
class RoleTest {
@ -25,21 +25,15 @@ class RoleTest {
void setUp() {
roleId = RoleId.generate();
roleName = RoleName.ADMIN;
permissions = new HashSet<>(Set.of(
Permission.USER_READ,
Permission.USER_WRITE,
Permission.ROLE_READ
));
permissions = new HashSet<>(Set.of(Permission.USER_READ, Permission.USER_WRITE, Permission.ROLE_READ));
description = "Administrator role with full access";
}
@Test
@DisplayName("should_CreateRole_When_ValidDataProvided")
void should_CreateRole_When_ValidDataProvided() {
// Act
Role role = Role.reconstitute(roleId, roleName, permissions, description);
// Assert
assertThat(role.id()).isEqualTo(roleId);
assertThat(role.name()).isEqualTo(roleName);
assertThat(role.permissions()).contains(Permission.USER_READ, Permission.USER_WRITE);
@ -49,10 +43,8 @@ class RoleTest {
@Test
@DisplayName("should_ReturnFailure_When_NullRoleNameProvided")
void should_ReturnFailure_When_NullRoleNameProvided() {
// Act
Result<UserError, Role> result = Role.create(null, permissions, description);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.NullRole.class);
}
@ -60,30 +52,24 @@ class RoleTest {
@Test
@DisplayName("should_CreateRoleWithEmptyPermissions_When_NullPermissionsProvided")
void should_CreateRoleWithEmptyPermissions_When_NullPermissionsProvided() {
// Act
Role role = Role.reconstitute(roleId, roleName, null, description);
// Assert
assertThat(role.permissions()).isEmpty();
}
@Test
@DisplayName("should_CreateRoleWithNullDescription_When_DescriptionNotProvided")
void should_CreateRoleWithNullDescription_When_DescriptionNotProvided() {
// Act
Role role = Role.reconstitute(roleId, roleName, permissions, null);
// Assert
assertThat(role.description()).isNull();
}
@Test
@DisplayName("should_CreateRole_When_FactoryMethodCalled")
void should_CreateRole_When_FactoryMethodCalled() {
// Act
Role role = Role.create(roleName, permissions, description).unsafeGetValue();
// Assert
assertThat(role.id()).isNotNull();
assertThat(role.name()).isEqualTo(roleName);
assertThat(role.permissions()).isEqualTo(permissions);
@ -91,135 +77,106 @@ class RoleTest {
}
@Test
@DisplayName("should_AddPermission_When_ValidPermissionProvided")
void should_AddPermission_When_ValidPermissionProvided() {
// Arrange
@DisplayName("should_ReturnNewRoleWithPermission_When_AddPermissionCalled")
void should_ReturnNewRoleWithPermission_When_AddPermissionCalled() {
Role role = Role.reconstitute(roleId, roleName, new HashSet<>(), description);
// Act
Result<UserError, Void> result = role.addPermission(Permission.USER_DELETE);
Result<UserError, Role> result = role.addPermission(Permission.USER_DELETE);
// Assert
assertThat(result.isSuccess()).isTrue();
assertThat(role.permissions()).contains(Permission.USER_DELETE);
assertThat(result.unsafeGetValue().permissions()).contains(Permission.USER_DELETE);
assertThat(role.permissions()).doesNotContain(Permission.USER_DELETE); // original unchanged
}
@Test
@DisplayName("should_ReturnFailure_When_NullPermissionProvidedToAddPermission")
void should_ReturnFailure_When_NullPermissionProvidedToAddPermission() {
// Arrange
Role role = Role.reconstitute(roleId, roleName, new HashSet<>(), description);
// Act
Result<UserError, Void> result = role.addPermission(null);
Result<UserError, Role> result = role.addPermission(null);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.NullRole.class);
assertThat(result.unsafeGetError()).isInstanceOf(UserError.NullPermission.class);
}
@Test
@DisplayName("should_AddMultiplePermissions_When_MethodCalledRepeatedly")
void should_AddMultiplePermissions_When_MethodCalledRepeatedly() {
// Arrange
@DisplayName("should_ReturnNewRoleWithMultiplePermissions_When_AddedSequentially")
void should_ReturnNewRoleWithMultiplePermissions_When_AddedSequentially() {
Role role = Role.reconstitute(roleId, roleName, new HashSet<>(), description);
// Act
role.addPermission(Permission.USER_READ);
role.addPermission(Permission.USER_WRITE);
role.addPermission(Permission.USER_DELETE);
Role updated = role.addPermission(Permission.USER_READ).unsafeGetValue()
.addPermission(Permission.USER_WRITE).unsafeGetValue()
.addPermission(Permission.USER_DELETE).unsafeGetValue();
// Assert
assertThat(role.permissions()).hasSize(3);
assertThat(role.permissions()).contains(
Permission.USER_READ,
Permission.USER_WRITE,
Permission.USER_DELETE
);
assertThat(updated.permissions()).hasSize(3);
assertThat(updated.permissions()).contains(Permission.USER_READ, Permission.USER_WRITE, Permission.USER_DELETE);
assertThat(role.permissions()).isEmpty(); // original unchanged
}
@Test
@DisplayName("should_NotAddDuplicatePermission_When_SamePermissionAddedTwice")
void should_NotAddDuplicatePermission_When_SamePermissionAddedTwice() {
// Arrange
Role role = Role.reconstitute(roleId, roleName, new HashSet<>(), description);
// Act
role.addPermission(Permission.USER_READ);
role.addPermission(Permission.USER_READ);
Role updated = role.addPermission(Permission.USER_READ).unsafeGetValue()
.addPermission(Permission.USER_READ).unsafeGetValue();
// Assert
assertThat(role.permissions()).hasSize(1);
assertThat(updated.permissions()).hasSize(1);
}
@Test
@DisplayName("should_RemovePermission_When_PermissionProvided")
void should_RemovePermission_When_PermissionProvided() {
// Arrange
Set<Permission> initialPermissions = new HashSet<>(Set.of(
Permission.USER_READ,
Permission.USER_WRITE
));
@DisplayName("should_ReturnNewRoleWithoutPermission_When_RemovePermissionCalled")
void should_ReturnNewRoleWithoutPermission_When_RemovePermissionCalled() {
Set<Permission> initialPermissions = new HashSet<>(Set.of(Permission.USER_READ, Permission.USER_WRITE));
Role role = Role.reconstitute(roleId, roleName, initialPermissions, description);
// Act
role.removePermission(Permission.USER_READ);
Result<UserError, Role> result = role.removePermission(Permission.USER_READ);
// Assert
assertThat(role.permissions()).doesNotContain(Permission.USER_READ);
assertThat(role.permissions()).contains(Permission.USER_WRITE);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().permissions()).doesNotContain(Permission.USER_READ);
assertThat(result.unsafeGetValue().permissions()).contains(Permission.USER_WRITE);
assertThat(role.permissions()).contains(Permission.USER_READ); // original unchanged
}
@Test
@DisplayName("should_NotThrowException_When_RemovingNonExistentPermission")
void should_NotThrowException_When_RemovingNonExistentPermission() {
// Arrange
@DisplayName("should_ReturnFailure_When_NullPermissionProvidedToRemovePermission")
void should_ReturnFailure_When_NullPermissionProvidedToRemovePermission() {
Role role = Role.reconstitute(roleId, roleName, new HashSet<>(), description);
// Act & Assert
assertThatCode(() -> role.removePermission(Permission.USER_READ))
.doesNotThrowAnyException();
Result<UserError, Role> result = role.removePermission(null);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.NullPermission.class);
}
@Test
@DisplayName("should_UpdateDescription_When_NewDescriptionProvided")
void should_UpdateDescription_When_NewDescriptionProvided() {
// Arrange
@DisplayName("should_ReturnNewRoleWithDescription_When_UpdateDescriptionCalled")
void should_ReturnNewRoleWithDescription_When_UpdateDescriptionCalled() {
Role role = Role.reconstitute(roleId, roleName, permissions, description);
String newDescription = "Updated administrator role";
// Act
role.updateDescription(newDescription);
Result<UserError, Role> result = role.updateDescription(newDescription);
// Assert
assertThat(role.description()).isEqualTo(newDescription);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().description()).isEqualTo(newDescription);
assertThat(role.description()).isEqualTo(description); // original unchanged
}
@Test
@DisplayName("should_SetDescriptionToNull_When_NullDescriptionProvided")
void should_SetDescriptionToNull_When_NullDescriptionProvided() {
// Arrange
Role role = Role.reconstitute(roleId, roleName, permissions, description);
// Act
role.updateDescription(null);
Role updated = role.updateDescription(null).unsafeGetValue();
// Assert
assertThat(role.description()).isNull();
assertThat(updated.description()).isNull();
}
@Test
@DisplayName("should_CheckPermission_When_RoleHasPermission")
void should_CheckPermission_When_RoleHasPermission() {
// Arrange
Role role = Role.reconstitute(
roleId,
roleName,
new HashSet<>(Set.of(Permission.USER_READ, Permission.USER_WRITE)),
description
);
Role role = Role.reconstitute(roleId, roleName, new HashSet<>(Set.of(Permission.USER_READ, Permission.USER_WRITE)), description);
// Act & Assert
assertThat(role.hasPermission(Permission.USER_READ)).isTrue();
assertThat(role.hasPermission(Permission.USER_WRITE)).isTrue();
}
@ -227,26 +184,17 @@ class RoleTest {
@Test
@DisplayName("should_CheckPermission_When_RoleLacksPermission")
void should_CheckPermission_When_RoleLacksPermission() {
// Arrange
Role role = Role.reconstitute(
roleId,
roleName,
new HashSet<>(Set.of(Permission.USER_READ)),
description
);
Role role = Role.reconstitute(roleId, roleName, new HashSet<>(Set.of(Permission.USER_READ)), description);
// Act & Assert
assertThat(role.hasPermission(Permission.USER_DELETE)).isFalse();
}
@Test
@DisplayName("should_BeEqualToAnother_When_BothHaveSameId")
void should_BeEqualToAnother_When_BothHaveSameId() {
// Arrange
Role role1 = Role.reconstitute(roleId, RoleName.ADMIN, permissions, description);
Role role2 = Role.reconstitute(roleId, RoleName.PRODUCTION_MANAGER, new HashSet<>(), "Different role");
// Act & Assert
assertThat(role1).isEqualTo(role2);
assertThat(role1.hashCode()).isEqualTo(role2.hashCode());
}
@ -254,24 +202,19 @@ class RoleTest {
@Test
@DisplayName("should_NotBeEqual_When_DifferentIds")
void should_NotBeEqual_When_DifferentIds() {
// Arrange
Role role1 = Role.reconstitute(RoleId.generate(), roleName, permissions, description);
Role role2 = Role.reconstitute(RoleId.generate(), roleName, permissions, description);
// Act & Assert
assertThat(role1).isNotEqualTo(role2);
}
@Test
@DisplayName("should_ReturnUnmodifiablePermissionSet_When_PermissionsRetrieved")
void should_ReturnUnmodifiablePermissionSet_When_PermissionsRetrieved() {
// Arrange
Role role = Role.reconstitute(roleId, roleName, permissions, description);
// Act
Set<Permission> retrievedPermissions = role.permissions();
// Assert
assertThatThrownBy(() -> retrievedPermissions.add(Permission.USER_DELETE))
.isInstanceOf(UnsupportedOperationException.class);
}
@ -279,49 +222,23 @@ class RoleTest {
@Test
@DisplayName("should_PreserveImmutabilityOfPermissions_When_PermissionsModified")
void should_PreserveImmutabilityOfPermissions_When_PermissionsModified() {
// Arrange
Set<Permission> initialPermissions = new HashSet<>(Set.of(Permission.USER_READ));
Role role = Role.reconstitute(roleId, roleName, initialPermissions, description);
// Act - Modify the original set passed to constructor
initialPermissions.add(Permission.USER_WRITE);
// Assert - Role should not be affected
assertThat(role.permissions()).doesNotContain(Permission.USER_WRITE);
}
@Test
@DisplayName("should_SupportMultipleRoleNames_When_DifferentNamesUsed")
void should_SupportMultipleRoleNames_When_DifferentNamesUsed() {
// Arrange & Act
Role adminRole = Role.reconstitute(RoleId.generate(), RoleName.ADMIN, permissions, "Admin");
Role managerRole = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_MANAGER, permissions, "Manager");
Role workerRole = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, permissions, "Worker");
// Assert
assertThat(adminRole.name()).isEqualTo(RoleName.ADMIN);
assertThat(managerRole.name()).isEqualTo(RoleName.PRODUCTION_MANAGER);
assertThat(workerRole.name()).isEqualTo(RoleName.PRODUCTION_WORKER);
}
@Test
@DisplayName("should_AllowDifferentPermissionSets_When_MultipleRolesCreated")
void should_AllowDifferentPermissionSets_When_MultipleRolesCreated() {
// Arrange
Set<Permission> adminPerms = new HashSet<>(Set.of(
Permission.USER_READ, Permission.USER_WRITE, Permission.USER_DELETE
));
Set<Permission> readerPerms = new HashSet<>(Set.of(
Permission.USER_READ
));
// Act
Role adminRole = Role.reconstitute(RoleId.generate(), RoleName.ADMIN, adminPerms, "Admin");
Role readerRole = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, readerPerms, "Reader");
// Assert
assertThat(adminRole.permissions()).hasSize(3);
assertThat(readerRole.permissions()).hasSize(1);
assertThat(adminRole.permissions()).isNotEqualTo(readerRole.permissions());
}
}

View file

@ -14,7 +14,7 @@ import static org.assertj.core.api.Assertions.*;
/**
* Unit tests for User Entity.
* Tests validation, business methods, status management, role assignment, and permissions.
* Tests validation, immutable business methods, status transitions, role assignment, and permissions.
*/
@DisplayName("User Entity")
class UserTest {
@ -41,20 +41,8 @@ class UserTest {
@Test
@DisplayName("should_CreateUser_When_ValidDataProvided")
void should_CreateUser_When_ValidDataProvided() {
// Act
User user = User.reconstitute(
userId,
username,
email,
passwordHash,
roles,
branchId,
UserStatus.ACTIVE,
createdAt,
null
);
User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.ACTIVE, createdAt, null);
// Assert
assertThat(user.id()).isEqualTo(userId);
assertThat(user.username()).isEqualTo(username);
assertThat(user.email()).isEqualTo(email);
@ -68,22 +56,10 @@ class UserTest {
@Test
@DisplayName("should_SetDefaultCreatedAtToNow_When_NullProvidedForCreatedAt")
void should_SetDefaultCreatedAtToNow_When_NullProvidedForCreatedAt() {
// Act
LocalDateTime before = LocalDateTime.now();
User user = User.reconstitute(
userId,
username,
email,
passwordHash,
roles,
branchId,
UserStatus.ACTIVE,
null,
null
);
User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.ACTIVE, null, null);
LocalDateTime after = LocalDateTime.now();
// Assert
assertThat(user.createdAt()).isNotNull();
assertThat(user.createdAt()).isBetween(before, after);
}
@ -91,10 +67,8 @@ class UserTest {
@Test
@DisplayName("should_ReturnFailure_When_NullUsernameProvided")
void should_ReturnFailure_When_NullUsernameProvided() {
// Act
Result<UserError, User> result = User.create(null, email, passwordHash, roles, branchId);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidUsername.class);
}
@ -102,10 +76,8 @@ class UserTest {
@Test
@DisplayName("should_ReturnFailure_When_EmptyUsernameProvided")
void should_ReturnFailure_When_EmptyUsernameProvided() {
// Act
Result<UserError, User> result = User.create("", email, passwordHash, roles, branchId);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidUsername.class);
}
@ -113,10 +85,8 @@ class UserTest {
@Test
@DisplayName("should_ReturnFailure_When_InvalidEmailProvided")
void should_ReturnFailure_When_InvalidEmailProvided() {
// Act
Result<UserError, User> result = User.create(username, "invalid-email", passwordHash, roles, branchId);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidEmail.class);
}
@ -125,10 +95,8 @@ class UserTest {
@ValueSource(strings = {"", " ", "notanemail"})
@DisplayName("should_ReturnFailure_When_InvalidEmailFormatsProvided")
void should_ReturnFailure_When_InvalidEmailFormatsProvided(String invalidEmail) {
// Act
Result<UserError, User> result = User.create(username, invalidEmail, passwordHash, roles, branchId);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidEmail.class);
}
@ -136,10 +104,8 @@ class UserTest {
@Test
@DisplayName("should_ReturnFailure_When_NullPasswordHashProvided")
void should_ReturnFailure_When_NullPasswordHashProvided() {
// Act
Result<UserError, User> result = User.create(username, email, null, roles, branchId);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.NullPasswordHash.class);
}
@ -147,16 +113,8 @@ class UserTest {
@Test
@DisplayName("should_CreateUser_When_FactoryMethodCalled")
void should_CreateUser_When_FactoryMethodCalled() {
// Act
User user = User.create(
username,
email,
passwordHash,
roles,
branchId
).unsafeGetValue();
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
// Assert
assertThat(user.username()).isEqualTo(username);
assertThat(user.email()).isEqualTo(email);
assertThat(user.passwordHash()).isEqualTo(passwordHash);
@ -168,137 +126,181 @@ class UserTest {
}
@Test
@DisplayName("should_UpdateLastLogin_When_MethodCalled")
void should_UpdateLastLogin_When_MethodCalled() {
// Arrange
@DisplayName("should_ReturnNewUserWithLastLogin_When_WithLastLoginCalled")
void should_ReturnNewUserWithLastLogin_When_WithLastLoginCalled() {
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
LocalDateTime now = LocalDateTime.now();
// Act
user.updateLastLogin(now);
User updated = user.withLastLogin(now).unsafeGetValue();
// Assert
assertThat(user.lastLogin()).isEqualTo(now);
assertThat(updated.lastLogin()).isEqualTo(now);
assertThat(user.lastLogin()).isNull(); // original unchanged
assertThat(updated.id()).isEqualTo(user.id());
}
@Test
@DisplayName("should_ChangePassword_When_NewHashProvided")
void should_ChangePassword_When_NewHashProvided() {
// Arrange
@DisplayName("should_ReturnNewUserWithPassword_When_ChangePasswordCalled")
void should_ReturnNewUserWithPassword_When_ChangePasswordCalled() {
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
PasswordHash newHash = new PasswordHash("$2b$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
// Act
Result<UserError, Void> result = user.changePassword(newHash);
Result<UserError, User> result = user.changePassword(newHash);
// Assert
assertThat(result.isSuccess()).isTrue();
assertThat(user.passwordHash()).isEqualTo(newHash);
assertThat(user.passwordHash()).isNotEqualTo(passwordHash);
assertThat(result.unsafeGetValue().passwordHash()).isEqualTo(newHash);
assertThat(user.passwordHash()).isEqualTo(passwordHash); // original unchanged
}
@Test
@DisplayName("should_ReturnFailure_When_NullPasswordHashProvidedToChangePassword")
void should_ReturnFailure_When_NullPasswordHashProvidedToChangePassword() {
// Arrange
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
// Act
Result<UserError, Void> result = user.changePassword(null);
Result<UserError, User> result = user.changePassword(null);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.NullPasswordHash.class);
}
// ==================== Status Transition Tests ====================
@Test
@DisplayName("should_LockUser_When_LockMethodCalled")
void should_LockUser_When_LockMethodCalled() {
// Arrange
@DisplayName("should_LockUser_When_StatusIsActive")
void should_LockUser_When_StatusIsActive() {
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
assertThat(user.status()).isEqualTo(UserStatus.ACTIVE);
// Act
user.lock();
Result<UserError, User> result = user.lock();
// Assert
assertThat(user.status()).isEqualTo(UserStatus.LOCKED);
assertThat(user.isLocked()).isTrue();
assertThat(user.isActive()).isFalse();
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().status()).isEqualTo(UserStatus.LOCKED);
assertThat(result.unsafeGetValue().isLocked()).isTrue();
assertThat(user.status()).isEqualTo(UserStatus.ACTIVE); // original unchanged
}
@Test
@DisplayName("should_UnlockUser_When_UnlockMethodCalled")
void should_UnlockUser_When_UnlockMethodCalled() {
// Arrange
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
user.lock();
assertThat(user.status()).isEqualTo(UserStatus.LOCKED);
@DisplayName("should_FailLock_When_StatusIsLocked")
void should_FailLock_When_StatusIsLocked() {
User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.LOCKED, createdAt, null);
// Act
user.unlock();
Result<UserError, User> result = user.lock();
// Assert
assertThat(user.status()).isEqualTo(UserStatus.ACTIVE);
assertThat(user.isActive()).isTrue();
assertThat(user.isLocked()).isFalse();
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidStatusTransition.class);
}
@Test
@DisplayName("should_DeactivateUser_When_DeactivateMethodCalled")
void should_DeactivateUser_When_DeactivateMethodCalled() {
// Arrange
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
@DisplayName("should_FailLock_When_StatusIsInactive")
void should_FailLock_When_StatusIsInactive() {
User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.INACTIVE, createdAt, null);
// Act
user.deactivate();
Result<UserError, User> result = user.lock();
// Assert
assertThat(user.status()).isEqualTo(UserStatus.INACTIVE);
assertThat(user.isActive()).isFalse();
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidStatusTransition.class);
}
@Test
@DisplayName("should_ActivateUser_When_ActivateMethodCalled")
void should_ActivateUser_When_ActivateMethodCalled() {
// Arrange
@DisplayName("should_UnlockUser_When_StatusIsLocked")
void should_UnlockUser_When_StatusIsLocked() {
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
user.deactivate();
assertThat(user.status()).isEqualTo(UserStatus.INACTIVE);
User locked = user.lock().unsafeGetValue();
// Act
user.activate();
Result<UserError, User> result = locked.unlock();
// Assert
assertThat(user.status()).isEqualTo(UserStatus.ACTIVE);
assertThat(user.isActive()).isTrue();
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().status()).isEqualTo(UserStatus.ACTIVE);
assertThat(result.unsafeGetValue().isActive()).isTrue();
}
@Test
@DisplayName("should_FailUnlock_When_StatusIsActive")
void should_FailUnlock_When_StatusIsActive() {
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
Result<UserError, User> result = user.unlock();
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidStatusTransition.class);
}
@Test
@DisplayName("should_DeactivateUser_When_StatusIsActive")
void should_DeactivateUser_When_StatusIsActive() {
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
Result<UserError, User> result = user.deactivate();
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().status()).isEqualTo(UserStatus.INACTIVE);
}
@Test
@DisplayName("should_DeactivateUser_When_StatusIsLocked")
void should_DeactivateUser_When_StatusIsLocked() {
User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.LOCKED, createdAt, null);
Result<UserError, User> result = user.deactivate();
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().status()).isEqualTo(UserStatus.INACTIVE);
}
@Test
@DisplayName("should_FailDeactivate_When_StatusIsInactive")
void should_FailDeactivate_When_StatusIsInactive() {
User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.INACTIVE, createdAt, null);
Result<UserError, User> result = user.deactivate();
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidStatusTransition.class);
}
@Test
@DisplayName("should_ActivateUser_When_StatusIsInactive")
void should_ActivateUser_When_StatusIsInactive() {
User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.INACTIVE, createdAt, null);
Result<UserError, User> result = user.activate();
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().status()).isEqualTo(UserStatus.ACTIVE);
assertThat(result.unsafeGetValue().isActive()).isTrue();
}
@Test
@DisplayName("should_FailActivate_When_StatusIsActive")
void should_FailActivate_When_StatusIsActive() {
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
Result<UserError, User> result = user.activate();
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidStatusTransition.class);
}
// ==================== Role Tests ====================
@Test
@DisplayName("should_AssignRole_When_RoleProvided")
void should_AssignRole_When_RoleProvided() {
// Arrange
User user = User.create(username, email, passwordHash, new HashSet<>(), branchId).unsafeGetValue();
Role role = createTestRole("ADMIN");
// Act
Result<UserError, Void> result = user.assignRole(role);
Result<UserError, User> result = user.assignRole(role);
// Assert
assertThat(result.isSuccess()).isTrue();
assertThat(user.roles()).contains(role);
assertThat(result.unsafeGetValue().roles()).contains(role);
assertThat(user.roles()).doesNotContain(role); // original unchanged
}
@Test
@DisplayName("should_ReturnFailure_When_NullRoleProvidedToAssignRole")
void should_ReturnFailure_When_NullRoleProvidedToAssignRole() {
// Arrange
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
// Act
Result<UserError, Void> result = user.assignRole(null);
Result<UserError, User> result = user.assignRole(null);
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.NullRole.class);
}
@ -306,43 +308,49 @@ class UserTest {
@Test
@DisplayName("should_RemoveRole_When_RoleProvided")
void should_RemoveRole_When_RoleProvided() {
// Arrange
Role role = createTestRole("ADMIN");
User user = User.create(username, email, passwordHash, new HashSet<>(Set.of(role)), branchId).unsafeGetValue();
assertThat(user.roles()).contains(role);
// Act
user.removeRole(role);
Result<UserError, User> result = user.removeRole(role);
// Assert
assertThat(user.roles()).doesNotContain(role);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().roles()).doesNotContain(role);
assertThat(user.roles()).contains(role); // original unchanged
}
@Test
@DisplayName("should_ReturnFailure_When_NullRoleProvidedToRemoveRole")
void should_ReturnFailure_When_NullRoleProvidedToRemoveRole() {
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
Result<UserError, User> result = user.removeRole(null);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.NullRole.class);
}
// ==================== Email & Branch Tests ====================
@Test
@DisplayName("should_UpdateEmail_When_ValidEmailProvided")
void should_UpdateEmail_When_ValidEmailProvided() {
// Arrange
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
String newEmail = "newemail@example.com";
// Act
Result<UserError, Void> result = user.updateEmail(newEmail);
Result<UserError, User> result = user.updateEmail(newEmail);
// Assert
assertThat(result.isSuccess()).isTrue();
assertThat(user.email()).isEqualTo(newEmail);
assertThat(result.unsafeGetValue().email()).isEqualTo(newEmail);
assertThat(user.email()).isEqualTo(email); // original unchanged
}
@Test
@DisplayName("should_ReturnFailure_When_InvalidEmailProvidedToUpdateEmail")
void should_ReturnFailure_When_InvalidEmailProvidedToUpdateEmail() {
// Arrange
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
// Act
Result<UserError, Void> result = user.updateEmail("invalid-email");
Result<UserError, User> result = user.updateEmail("invalid-email");
// Assert
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidEmail.class);
}
@ -350,71 +358,46 @@ class UserTest {
@Test
@DisplayName("should_UpdateBranch_When_BranchIdProvided")
void should_UpdateBranch_When_BranchIdProvided() {
// Arrange
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
String newBranchId = "branch-2";
// Act
user.updateBranch(newBranchId);
Result<UserError, User> result = user.updateBranch(newBranchId);
// Assert
assertThat(user.branchId()).isEqualTo(newBranchId);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().branchId()).isEqualTo(newBranchId);
assertThat(user.branchId()).isEqualTo(branchId); // original unchanged
}
// ==================== Permission Tests ====================
@Test
@DisplayName("should_ReturnAllPermissions_When_GetAllPermissionsMethodCalled")
void should_ReturnAllPermissions_When_GetAllPermissionsMethodCalled() {
// Arrange
Set<Permission> role1Perms = Set.of(Permission.USER_READ, Permission.USER_WRITE);
Set<Permission> role2Perms = Set.of(Permission.ROLE_READ, Permission.ROLE_WRITE);
Role role1 = createRoleWithPermissions("ADMIN", role1Perms);
Role role2 = createRoleWithPermissions("PRODUCTION_MANAGER", role2Perms);
User user = User.create(
username,
email,
passwordHash,
new HashSet<>(Set.of(role1, role2)),
branchId
).unsafeGetValue();
User user = User.create(username, email, passwordHash, new HashSet<>(Set.of(role1, role2)), branchId).unsafeGetValue();
// Act
Set<Permission> allPermissions = user.getAllPermissions();
// Assert
assertThat(allPermissions).contains(Permission.USER_READ, Permission.USER_WRITE,
Permission.ROLE_READ, Permission.ROLE_WRITE);
assertThat(allPermissions).contains(Permission.USER_READ, Permission.USER_WRITE, Permission.ROLE_READ, Permission.ROLE_WRITE);
}
@Test
@DisplayName("should_ReturnEmptyPermissions_When_UserHasNoRoles")
void should_ReturnEmptyPermissions_When_UserHasNoRoles() {
// Arrange
User user = User.create(username, email, passwordHash, new HashSet<>(), branchId).unsafeGetValue();
// Act
Set<Permission> permissions = user.getAllPermissions();
// Assert
assertThat(permissions).isEmpty();
assertThat(user.getAllPermissions()).isEmpty();
}
@Test
@DisplayName("should_CheckPermission_When_UserHasPermission")
void should_CheckPermission_When_UserHasPermission() {
// Arrange
Role role = createRoleWithPermissions(
"ADMIN",
Set.of(Permission.USER_READ, Permission.USER_WRITE)
);
User user = User.create(
username,
email,
passwordHash,
new HashSet<>(Set.of(role)),
branchId
).unsafeGetValue();
Role role = createRoleWithPermissions("ADMIN", Set.of(Permission.USER_READ, Permission.USER_WRITE));
User user = User.create(username, email, passwordHash, new HashSet<>(Set.of(role)), branchId).unsafeGetValue();
// Act & Assert
assertThat(user.hasPermission(Permission.USER_READ)).isTrue();
assertThat(user.hasPermission(Permission.USER_WRITE)).isTrue();
}
@ -422,51 +405,20 @@ class UserTest {
@Test
@DisplayName("should_CheckPermission_When_UserLacksPermission")
void should_CheckPermission_When_UserLacksPermission() {
// Arrange
Role role = createRoleWithPermissions(
"PRODUCTION_WORKER",
Set.of(Permission.USER_READ)
);
User user = User.create(
username,
email,
passwordHash,
new HashSet<>(Set.of(role)),
branchId
).unsafeGetValue();
Role role = createRoleWithPermissions("PRODUCTION_WORKER", Set.of(Permission.USER_READ));
User user = User.create(username, email, passwordHash, new HashSet<>(Set.of(role)), branchId).unsafeGetValue();
// Act & Assert
assertThat(user.hasPermission(Permission.USER_DELETE)).isFalse();
}
// ==================== Equality Tests ====================
@Test
@DisplayName("should_BeEqualToAnother_When_BothHaveSameId")
void should_BeEqualToAnother_When_BothHaveSameId() {
// Arrange
User user1 = User.reconstitute(
userId,
username,
email,
passwordHash,
roles,
branchId,
UserStatus.ACTIVE,
createdAt,
null
);
User user2 = User.reconstitute(
userId,
"different_username",
"different@example.com",
passwordHash,
roles,
"different-branch",
UserStatus.INACTIVE,
createdAt,
null
);
User user1 = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.ACTIVE, createdAt, null);
User user2 = User.reconstitute(userId, "different_username", "different@example.com", passwordHash, roles, "different-branch", UserStatus.INACTIVE, createdAt, null);
// Act & Assert
assertThat(user1).isEqualTo(user2);
assertThat(user1.hashCode()).isEqualTo(user2.hashCode());
}
@ -474,31 +426,20 @@ class UserTest {
@Test
@DisplayName("should_NotBeEqual_When_DifferentIds")
void should_NotBeEqual_When_DifferentIds() {
// Arrange
User user1 = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
User user2 = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
// Act & Assert
assertThat(user1).isNotEqualTo(user2);
}
@Test
@DisplayName("should_ReturnUnmodifiableRoleSet_When_RolesRetrieved")
void should_ReturnUnmodifiableRoleSet_When_RolesRetrieved() {
// Arrange
Role role = createTestRole("ADMIN");
User user = User.create(
username,
email,
passwordHash,
new HashSet<>(Set.of(role)),
branchId
).unsafeGetValue();
User user = User.create(username, email, passwordHash, new HashSet<>(Set.of(role)), branchId).unsafeGetValue();
// Act
Set<Role> retrievedRoles = user.roles();
// Assert
assertThatThrownBy(() -> retrievedRoles.add(createTestRole("PRODUCTION_MANAGER")))
.isInstanceOf(UnsupportedOperationException.class);
}
@ -506,20 +447,11 @@ class UserTest {
@Test
@DisplayName("should_ReturnUnmodifiablePermissionSet_When_PermissionsRetrieved")
void should_ReturnUnmodifiablePermissionSet_When_PermissionsRetrieved() {
// Arrange
Role role = createRoleWithPermissions("ADMIN", Set.of(Permission.USER_READ));
User user = User.create(
username,
email,
passwordHash,
new HashSet<>(Set.of(role)),
branchId
).unsafeGetValue();
User user = User.create(username, email, passwordHash, new HashSet<>(Set.of(role)), branchId).unsafeGetValue();
// Act
Set<Permission> permissions = user.getAllPermissions();
// Assert
assertThatThrownBy(() -> permissions.add(Permission.USER_WRITE))
.isInstanceOf(UnsupportedOperationException.class);
}
@ -527,40 +459,18 @@ class UserTest {
@Test
@DisplayName("should_PreserveNullRolesAsEmptySet_When_NullRolesProvided")
void should_PreserveNullRolesAsEmptySet_When_NullRolesProvided() {
// Act
User user = User.reconstitute(
userId,
username,
email,
passwordHash,
null,
branchId,
UserStatus.ACTIVE,
createdAt,
null
);
User user = User.reconstitute(userId, username, email, passwordHash, null, branchId, UserStatus.ACTIVE, createdAt, null);
// Assert
assertThat(user.roles()).isEmpty();
}
// ==================== Helper Methods ====================
private Role createTestRole(String roleName) {
return Role.reconstitute(
RoleId.generate(),
RoleName.valueOf(roleName),
new HashSet<>(),
"Test role: " + roleName
);
return Role.reconstitute(RoleId.generate(), RoleName.valueOf(roleName), new HashSet<>(), "Test role: " + roleName);
}
private Role createRoleWithPermissions(String roleName, Set<Permission> permissions) {
return Role.reconstitute(
RoleId.generate(),
RoleName.valueOf(roleName),
new HashSet<>(permissions),
"Test role: " + roleName
);
return Role.reconstitute(RoleId.generate(), RoleName.valueOf(roleName), new HashSet<>(permissions), "Test role: " + roleName);
}
}

View file

@ -564,7 +564,7 @@ class UserControllerIntegrationTest {
}
@Test
@DisplayName("Change password for non-existent user should return 404 Not Found")
@DisplayName("Change password for non-existent user should return 403 when not self-service")
void testChangePasswordForNonExistentUser() throws Exception {
String nonExistentId = UUID.randomUUID().toString();
ChangePasswordRequest request = new ChangePasswordRequest(
@ -572,11 +572,12 @@ class UserControllerIntegrationTest {
"NewSecurePass456!"
);
// Regular user has no PASSWORD_CHANGE permission for other users 403 before user lookup
mockMvc.perform(put("/api/users/{id}/password", nonExistentId)
.header("Authorization", "Bearer " + regularUserToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isNotFound())
.andExpect(status().isForbidden())
.andReturn();
}