mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 17:19:56 +01:00
refactor: restructure repository with separate backend and frontend directories
- Move Java backend to backend/ directory - Create frontend/ directory for TypeScript TUI and future WebUI - Update .gitignore for Node.js and worktrees - Update README.md with new repository structure - Copy documentation to backend/
This commit is contained in:
parent
ec9114aa0a
commit
c2c48a03e8
141 changed files with 734 additions and 9 deletions
|
|
@ -0,0 +1,342 @@
|
|||
package de.effigenix.application.usermanagement;
|
||||
|
||||
import de.effigenix.application.usermanagement.command.AuthenticateCommand;
|
||||
import de.effigenix.application.usermanagement.dto.SessionToken;
|
||||
import de.effigenix.domain.usermanagement.*;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
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;
|
||||
|
||||
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.*;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@InjectMocks
|
||||
private AuthenticateUser authenticateUser;
|
||||
|
||||
private AuthenticateCommand validCommand;
|
||||
private User testUser;
|
||||
private PasswordHash validPasswordHash;
|
||||
private SessionToken sessionToken;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
validCommand = new AuthenticateCommand("john.doe", "Password123!");
|
||||
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
|
||||
);
|
||||
|
||||
sessionToken = new SessionToken("jwt-token", "Bearer", 3600L, LocalDateTime.now().plusSeconds(3600), "refresh-token");
|
||||
}
|
||||
|
||||
@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());
|
||||
verify(auditLogger).log(eq(AuditEvent.LOGIN_SUCCESS), anyString(), any());
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// Assert
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidCredentials.class);
|
||||
verify(auditLogger).log(eq(AuditEvent.LOGIN_FAILED), anyString());
|
||||
}
|
||||
|
||||
@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
|
||||
);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@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
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@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
|
||||
);
|
||||
|
||||
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(lockedUser)));
|
||||
|
||||
// Act
|
||||
Result<UserError, SessionToken> result = 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
package de.effigenix.application.usermanagement;
|
||||
|
||||
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 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;
|
||||
|
||||
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.*;
|
||||
|
||||
/**
|
||||
* 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 PasswordHasher passwordHasher;
|
||||
|
||||
@Mock
|
||||
private AuditLogger auditLogger;
|
||||
|
||||
@InjectMocks
|
||||
private ChangePassword changePassword;
|
||||
|
||||
private User testUser;
|
||||
private PasswordHash oldPasswordHash;
|
||||
private PasswordHash newPasswordHash;
|
||||
private ChangePasswordCommand validCommand;
|
||||
private ActorId performedBy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
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
|
||||
);
|
||||
|
||||
validCommand = new ChangePasswordCommand("user-123", "OldPassword123!", "NewPassword456!");
|
||||
performedBy = ActorId.of("user-123");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ChangePassword_When_ValidCurrentPasswordProvided")
|
||||
void should_ChangePassword_When_ValidCurrentPasswordProvided() {
|
||||
// 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(userRepository).save(any(User.class));
|
||||
}
|
||||
|
||||
@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!"
|
||||
);
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
@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!"
|
||||
);
|
||||
|
||||
// Act
|
||||
Result<UserError, Void> result = changePassword.execute(command, performedBy);
|
||||
|
||||
// Assert
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidCredentials.class);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
package de.effigenix.application.usermanagement;
|
||||
|
||||
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 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;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
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 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() {
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
@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"
|
||||
);
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_FailWithInvalidPassword_When_WeakPasswordProvided")
|
||||
void should_FailWithInvalidPassword_When_WeakPasswordProvided() {
|
||||
// Arrange
|
||||
when(passwordHasher.isValidPassword("Password123!")).thenReturn(false);
|
||||
|
||||
// Act
|
||||
Result<UserError, de.effigenix.application.usermanagement.dto.UserDTO> 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(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);
|
||||
|
||||
// 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(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);
|
||||
|
||||
// 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(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);
|
||||
|
||||
// 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
|
||||
Role role = Role.reconstitute(RoleId.generate(), RoleName.ADMIN, new HashSet<>(), "Admin");
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package de.effigenix.domain.usermanagement;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for PasswordHash Value Object.
|
||||
* Tests BCrypt validation, immutability, factory methods, and equality.
|
||||
*/
|
||||
@DisplayName("PasswordHash Value Object")
|
||||
class PasswordHashTest {
|
||||
|
||||
// Valid BCrypt hashes with different versions
|
||||
private static final String VALID_BCRYPT_2A = "$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW";
|
||||
private static final String VALID_BCRYPT_2B = "$2b$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW";
|
||||
private static final String VALID_BCRYPT_2Y = "$2y$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW";
|
||||
|
||||
@Test
|
||||
@DisplayName("should_CreatePasswordHash_When_ValidBcryptHashProvided")
|
||||
void should_CreatePasswordHash_When_ValidBcryptHashProvided() {
|
||||
// Act
|
||||
PasswordHash hash = new PasswordHash(VALID_BCRYPT_2A);
|
||||
|
||||
// Assert
|
||||
assertThat(hash.value()).isEqualTo(VALID_BCRYPT_2A);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_AcceptMultipleBcryptVersions_When_ValidHashesProvided")
|
||||
void should_AcceptMultipleBcryptVersions_When_ValidHashesProvided() {
|
||||
// Act & Assert
|
||||
assertThatCode(() -> {
|
||||
new PasswordHash(VALID_BCRYPT_2A);
|
||||
new PasswordHash(VALID_BCRYPT_2B);
|
||||
new PasswordHash(VALID_BCRYPT_2Y);
|
||||
}).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_NullHashProvided")
|
||||
void should_ThrowException_When_NullHashProvided() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new PasswordHash(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("PasswordHash cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_EmptyHashProvided")
|
||||
void should_ThrowException_When_EmptyHashProvided() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new PasswordHash(""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("PasswordHash cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_BlankHashProvided")
|
||||
void should_ThrowException_When_BlankHashProvided() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new PasswordHash(" "))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("PasswordHash cannot be null or empty");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"notabcrypthash", // Plain text
|
||||
"$1$salt$hash", // MD5 crypt format
|
||||
"$2c$12$invalid", // Invalid version ($2c$)
|
||||
"$2a$12$short", // Too short
|
||||
"$2a$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // Wrong length
|
||||
"password123", // Plain password
|
||||
"$2a$12$", // Incomplete hash
|
||||
})
|
||||
@DisplayName("should_ThrowException_When_InvalidBcryptFormatProvided")
|
||||
void should_ThrowException_When_InvalidBcryptFormatProvided(String invalidHash) {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new PasswordHash(invalidHash))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Invalid BCrypt hash format");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_CreateHashFromStaticFactory_When_OfMethodCalled")
|
||||
void should_CreateHashFromStaticFactory_When_OfMethodCalled() {
|
||||
// Act
|
||||
PasswordHash hash = PasswordHash.of(VALID_BCRYPT_2B);
|
||||
|
||||
// Assert
|
||||
assertThat(hash.value()).isEqualTo(VALID_BCRYPT_2B);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_BeImmutable_When_RecordCreated")
|
||||
void should_BeImmutable_When_RecordCreated() {
|
||||
// Arrange
|
||||
PasswordHash hash = new PasswordHash(VALID_BCRYPT_2A);
|
||||
|
||||
// Act & Assert - Records are immutable by design
|
||||
assertThat(hash).isNotNull();
|
||||
// Can't modify value as it's a record
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_BeEqual_When_SameHashProvided")
|
||||
void should_BeEqual_When_SameHashProvided() {
|
||||
// Arrange
|
||||
PasswordHash hash1 = new PasswordHash(VALID_BCRYPT_2A);
|
||||
PasswordHash hash2 = new PasswordHash(VALID_BCRYPT_2A);
|
||||
|
||||
// Act & Assert
|
||||
assertThat(hash1).isEqualTo(hash2);
|
||||
assertThat(hash1.hashCode()).isEqualTo(hash2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_NotBeEqual_When_DifferentHashesProvided")
|
||||
void should_NotBeEqual_When_DifferentHashesProvided() {
|
||||
// Arrange
|
||||
PasswordHash hash1 = new PasswordHash(VALID_BCRYPT_2A);
|
||||
PasswordHash hash2 = new PasswordHash(VALID_BCRYPT_2B);
|
||||
|
||||
// Act & Assert
|
||||
assertThat(hash1).isNotEqualTo(hash2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ValidateBcryptLength_When_TooShort")
|
||||
void should_ValidateBcryptLength_When_TooShort() {
|
||||
// Arrange
|
||||
String tooShort = "$2a$12$" + "x".repeat(40); // Less than 56 chars after prefix
|
||||
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new PasswordHash(tooShort))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Invalid BCrypt hash format");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ValidateBcryptLength_When_TooLong")
|
||||
void should_ValidateBcryptLength_When_TooLong() {
|
||||
// Arrange
|
||||
String tooLong = "$2a$12$" + "x".repeat(60); // More than 56 chars after prefix
|
||||
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new PasswordHash(tooLong))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Invalid BCrypt hash format");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package de.effigenix.domain.usermanagement;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for RoleId Value Object.
|
||||
* Tests validation, immutability, factory methods, and equality.
|
||||
*/
|
||||
@DisplayName("RoleId Value Object")
|
||||
class RoleIdTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("should_CreateRoleId_When_ValidValueProvided")
|
||||
void should_CreateRoleId_When_ValidValueProvided() {
|
||||
// Arrange
|
||||
String validId = "role-123";
|
||||
|
||||
// Act
|
||||
RoleId roleId = new RoleId(validId);
|
||||
|
||||
// Assert
|
||||
assertThat(roleId.value()).isEqualTo(validId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_NullValueProvided")
|
||||
void should_ThrowException_When_NullValueProvided() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new RoleId(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("RoleId cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_EmptyStringProvided")
|
||||
void should_ThrowException_When_EmptyStringProvided() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new RoleId(""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("RoleId cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_BlankStringProvided")
|
||||
void should_ThrowException_When_BlankStringProvided() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new RoleId(" "))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("RoleId cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_GenerateUniqueRoleId_When_GenerateMethodCalled")
|
||||
void should_GenerateUniqueRoleId_When_GenerateMethodCalled() {
|
||||
// Act
|
||||
RoleId roleId1 = RoleId.generate();
|
||||
RoleId roleId2 = RoleId.generate();
|
||||
|
||||
// Assert
|
||||
assertThat(roleId1.value()).isNotBlank();
|
||||
assertThat(roleId2.value()).isNotBlank();
|
||||
assertThat(roleId1.value()).isNotEqualTo(roleId2.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_CreateRoleIdFromString_When_OfMethodCalled")
|
||||
void should_CreateRoleIdFromString_When_OfMethodCalled() {
|
||||
// Arrange
|
||||
String validId = "test-role-id";
|
||||
|
||||
// Act
|
||||
RoleId roleId = RoleId.of(validId);
|
||||
|
||||
// Assert
|
||||
assertThat(roleId.value()).isEqualTo(validId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_BeImmutable_When_RecordCreated")
|
||||
void should_BeImmutable_When_RecordCreated() {
|
||||
// Arrange
|
||||
RoleId roleId = RoleId.generate();
|
||||
|
||||
// Act & Assert
|
||||
assertThat(roleId).isNotNull();
|
||||
// Can't modify value as it's a record
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_BeEqual_When_SameValueProvided")
|
||||
void should_BeEqual_When_SameValueProvided() {
|
||||
// Arrange
|
||||
String value = "same-role-id";
|
||||
RoleId roleId1 = new RoleId(value);
|
||||
RoleId roleId2 = new RoleId(value);
|
||||
|
||||
// Act & Assert
|
||||
assertThat(roleId1).isEqualTo(roleId2);
|
||||
assertThat(roleId1.hashCode()).isEqualTo(roleId2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_NotBeEqual_When_DifferentValuesProvided")
|
||||
void should_NotBeEqual_When_DifferentValuesProvided() {
|
||||
// Arrange
|
||||
RoleId roleId1 = new RoleId("role-1");
|
||||
RoleId roleId2 = new RoleId("role-2");
|
||||
|
||||
// Act & Assert
|
||||
assertThat(roleId1).isNotEqualTo(roleId2);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", " ", " \t "})
|
||||
@DisplayName("should_ThrowException_When_InvalidStringsProvided")
|
||||
void should_ThrowException_When_InvalidStringsProvided(String invalidValue) {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new RoleId(invalidValue))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
package de.effigenix.domain.usermanagement;
|
||||
|
||||
import de.effigenix.shared.common.Result;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Role Entity.
|
||||
* Tests validation, permission management, factory methods, and equality.
|
||||
*/
|
||||
@DisplayName("Role Entity")
|
||||
class RoleTest {
|
||||
|
||||
private RoleId roleId;
|
||||
private RoleName roleName;
|
||||
private Set<Permission> permissions;
|
||||
private String description;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
roleId = RoleId.generate();
|
||||
roleName = RoleName.ADMIN;
|
||||
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);
|
||||
assertThat(role.description()).isEqualTo(description);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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);
|
||||
assertThat(role.description()).isEqualTo(description);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_AddPermission_When_ValidPermissionProvided")
|
||||
void should_AddPermission_When_ValidPermissionProvided() {
|
||||
// Arrange
|
||||
Role role = Role.reconstitute(roleId, roleName, new HashSet<>(), description);
|
||||
|
||||
// Act
|
||||
Result<UserError, Void> result = role.addPermission(Permission.USER_DELETE);
|
||||
|
||||
// Assert
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(role.permissions()).contains(Permission.USER_DELETE);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// Assert
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(UserError.NullRole.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_AddMultiplePermissions_When_MethodCalledRepeatedly")
|
||||
void should_AddMultiplePermissions_When_MethodCalledRepeatedly() {
|
||||
// Arrange
|
||||
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);
|
||||
|
||||
// Assert
|
||||
assertThat(role.permissions()).hasSize(3);
|
||||
assertThat(role.permissions()).contains(
|
||||
Permission.USER_READ,
|
||||
Permission.USER_WRITE,
|
||||
Permission.USER_DELETE
|
||||
);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// Assert
|
||||
assertThat(role.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
|
||||
));
|
||||
Role role = Role.reconstitute(roleId, roleName, initialPermissions, description);
|
||||
|
||||
// Act
|
||||
role.removePermission(Permission.USER_READ);
|
||||
|
||||
// Assert
|
||||
assertThat(role.permissions()).doesNotContain(Permission.USER_READ);
|
||||
assertThat(role.permissions()).contains(Permission.USER_WRITE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_NotThrowException_When_RemovingNonExistentPermission")
|
||||
void should_NotThrowException_When_RemovingNonExistentPermission() {
|
||||
// Arrange
|
||||
Role role = Role.reconstitute(roleId, roleName, new HashSet<>(), description);
|
||||
|
||||
// Act & Assert
|
||||
assertThatCode(() -> role.removePermission(Permission.USER_READ))
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_UpdateDescription_When_NewDescriptionProvided")
|
||||
void should_UpdateDescription_When_NewDescriptionProvided() {
|
||||
// Arrange
|
||||
Role role = Role.reconstitute(roleId, roleName, permissions, description);
|
||||
String newDescription = "Updated administrator role";
|
||||
|
||||
// Act
|
||||
role.updateDescription(newDescription);
|
||||
|
||||
// Assert
|
||||
assertThat(role.description()).isEqualTo(newDescription);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_SetDescriptionToNull_When_NullDescriptionProvided")
|
||||
void should_SetDescriptionToNull_When_NullDescriptionProvided() {
|
||||
// Arrange
|
||||
Role role = Role.reconstitute(roleId, roleName, permissions, description);
|
||||
|
||||
// Act
|
||||
role.updateDescription(null);
|
||||
|
||||
// Assert
|
||||
assertThat(role.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
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
assertThat(role.hasPermission(Permission.USER_READ)).isTrue();
|
||||
assertThat(role.hasPermission(Permission.USER_WRITE)).isTrue();
|
||||
}
|
||||
|
||||
@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
|
||||
);
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package de.effigenix.domain.usermanagement;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for UserId Value Object.
|
||||
* Tests validation, immutability, factory methods, and equality.
|
||||
*/
|
||||
@DisplayName("UserId Value Object")
|
||||
class UserIdTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("should_CreateUserId_When_ValidValueProvided")
|
||||
void should_CreateUserId_When_ValidValueProvided() {
|
||||
// Arrange
|
||||
String validId = "user-123";
|
||||
|
||||
// Act
|
||||
UserId userId = new UserId(validId);
|
||||
|
||||
// Assert
|
||||
assertThat(userId.value()).isEqualTo(validId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_NullValueProvided")
|
||||
void should_ThrowException_When_NullValueProvided() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new UserId(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("UserId cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_EmptyStringProvided")
|
||||
void should_ThrowException_When_EmptyStringProvided() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new UserId(""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("UserId cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_BlankStringProvided")
|
||||
void should_ThrowException_When_BlankStringProvided() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new UserId(" "))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("UserId cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_GenerateUniqueUserId_When_GenerateMethodCalled")
|
||||
void should_GenerateUniqueUserId_When_GenerateMethodCalled() {
|
||||
// Act
|
||||
UserId userId1 = UserId.generate();
|
||||
UserId userId2 = UserId.generate();
|
||||
|
||||
// Assert
|
||||
assertThat(userId1.value()).isNotBlank();
|
||||
assertThat(userId2.value()).isNotBlank();
|
||||
assertThat(userId1.value()).isNotEqualTo(userId2.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_CreateUserIdFromString_When_OfMethodCalled")
|
||||
void should_CreateUserIdFromString_When_OfMethodCalled() {
|
||||
// Arrange
|
||||
String validId = "test-user-id";
|
||||
|
||||
// Act
|
||||
UserId userId = UserId.of(validId);
|
||||
|
||||
// Assert
|
||||
assertThat(userId.value()).isEqualTo(validId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_BeImmutable_When_RecordCreated")
|
||||
void should_BeImmutable_When_RecordCreated() {
|
||||
// Arrange
|
||||
UserId userId = UserId.generate();
|
||||
|
||||
// Act & Assert - Records are immutable by design
|
||||
assertThat(userId).isNotNull();
|
||||
// Can't modify value as it's a record
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_BeEqual_When_SameValueProvided")
|
||||
void should_BeEqual_When_SameValueProvided() {
|
||||
// Arrange
|
||||
String value = "same-user-id";
|
||||
UserId userId1 = new UserId(value);
|
||||
UserId userId2 = new UserId(value);
|
||||
|
||||
// Act & Assert
|
||||
assertThat(userId1).isEqualTo(userId2);
|
||||
assertThat(userId1.hashCode()).isEqualTo(userId2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_NotBeEqual_When_DifferentValuesProvided")
|
||||
void should_NotBeEqual_When_DifferentValuesProvided() {
|
||||
// Arrange
|
||||
UserId userId1 = new UserId("user-1");
|
||||
UserId userId2 = new UserId("user-2");
|
||||
|
||||
// Act & Assert
|
||||
assertThat(userId1).isNotEqualTo(userId2);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", " ", " \t "})
|
||||
@DisplayName("should_ThrowException_When_InvalidStringsProvided")
|
||||
void should_ThrowException_When_InvalidStringsProvided(String invalidValue) {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> new UserId(invalidValue))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,566 @@
|
|||
package de.effigenix.domain.usermanagement;
|
||||
|
||||
import de.effigenix.shared.common.Result;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for User Entity.
|
||||
* Tests validation, business methods, status management, role assignment, and permissions.
|
||||
*/
|
||||
@DisplayName("User Entity")
|
||||
class UserTest {
|
||||
|
||||
private UserId userId;
|
||||
private String username;
|
||||
private String email;
|
||||
private PasswordHash passwordHash;
|
||||
private Set<Role> roles;
|
||||
private String branchId;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
userId = UserId.generate();
|
||||
username = "john.doe";
|
||||
email = "john@example.com";
|
||||
passwordHash = new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
|
||||
roles = new HashSet<>();
|
||||
branchId = "branch-1";
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@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
|
||||
);
|
||||
|
||||
// Assert
|
||||
assertThat(user.id()).isEqualTo(userId);
|
||||
assertThat(user.username()).isEqualTo(username);
|
||||
assertThat(user.email()).isEqualTo(email);
|
||||
assertThat(user.passwordHash()).isEqualTo(passwordHash);
|
||||
assertThat(user.branchId()).isEqualTo(branchId);
|
||||
assertThat(user.status()).isEqualTo(UserStatus.ACTIVE);
|
||||
assertThat(user.createdAt()).isEqualTo(createdAt);
|
||||
assertThat(user.lastLogin()).isNull();
|
||||
}
|
||||
|
||||
@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
|
||||
);
|
||||
LocalDateTime after = LocalDateTime.now();
|
||||
|
||||
// Assert
|
||||
assertThat(user.createdAt()).isNotNull();
|
||||
assertThat(user.createdAt()).isBetween(before, after);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_CreateUser_When_FactoryMethodCalled")
|
||||
void should_CreateUser_When_FactoryMethodCalled() {
|
||||
// Act
|
||||
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);
|
||||
assertThat(user.branchId()).isEqualTo(branchId);
|
||||
assertThat(user.status()).isEqualTo(UserStatus.ACTIVE);
|
||||
assertThat(user.createdAt()).isNotNull();
|
||||
assertThat(user.lastLogin()).isNull();
|
||||
assertThat(user.id()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_UpdateLastLogin_When_MethodCalled")
|
||||
void should_UpdateLastLogin_When_MethodCalled() {
|
||||
// Arrange
|
||||
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// Act
|
||||
user.updateLastLogin(now);
|
||||
|
||||
// Assert
|
||||
assertThat(user.lastLogin()).isEqualTo(now);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ChangePassword_When_NewHashProvided")
|
||||
void should_ChangePassword_When_NewHashProvided() {
|
||||
// Arrange
|
||||
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);
|
||||
|
||||
// Assert
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(user.passwordHash()).isEqualTo(newHash);
|
||||
assertThat(user.passwordHash()).isNotEqualTo(passwordHash);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// Assert
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(UserError.NullPasswordHash.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_LockUser_When_LockMethodCalled")
|
||||
void should_LockUser_When_LockMethodCalled() {
|
||||
// Arrange
|
||||
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
|
||||
assertThat(user.status()).isEqualTo(UserStatus.ACTIVE);
|
||||
|
||||
// Act
|
||||
user.lock();
|
||||
|
||||
// Assert
|
||||
assertThat(user.status()).isEqualTo(UserStatus.LOCKED);
|
||||
assertThat(user.isLocked()).isTrue();
|
||||
assertThat(user.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// Act
|
||||
user.unlock();
|
||||
|
||||
// Assert
|
||||
assertThat(user.status()).isEqualTo(UserStatus.ACTIVE);
|
||||
assertThat(user.isActive()).isTrue();
|
||||
assertThat(user.isLocked()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_DeactivateUser_When_DeactivateMethodCalled")
|
||||
void should_DeactivateUser_When_DeactivateMethodCalled() {
|
||||
// Arrange
|
||||
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
|
||||
|
||||
// Act
|
||||
user.deactivate();
|
||||
|
||||
// Assert
|
||||
assertThat(user.status()).isEqualTo(UserStatus.INACTIVE);
|
||||
assertThat(user.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ActivateUser_When_ActivateMethodCalled")
|
||||
void should_ActivateUser_When_ActivateMethodCalled() {
|
||||
// Arrange
|
||||
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
|
||||
user.deactivate();
|
||||
assertThat(user.status()).isEqualTo(UserStatus.INACTIVE);
|
||||
|
||||
// Act
|
||||
user.activate();
|
||||
|
||||
// Assert
|
||||
assertThat(user.status()).isEqualTo(UserStatus.ACTIVE);
|
||||
assertThat(user.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// Assert
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(user.roles()).contains(role);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// Assert
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(UserError.NullRole.class);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// Assert
|
||||
assertThat(user.roles()).doesNotContain(role);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// Assert
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(user.email()).isEqualTo(newEmail);
|
||||
}
|
||||
|
||||
@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");
|
||||
|
||||
// Assert
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(UserError.InvalidEmail.class);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// Assert
|
||||
assertThat(user.branchId()).isEqualTo(newBranchId);
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
// Act
|
||||
Set<Permission> allPermissions = user.getAllPermissions();
|
||||
|
||||
// Assert
|
||||
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();
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
// Act & Assert
|
||||
assertThat(user.hasPermission(Permission.USER_READ)).isTrue();
|
||||
assertThat(user.hasPermission(Permission.USER_WRITE)).isTrue();
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
// Act & Assert
|
||||
assertThat(user.hasPermission(Permission.USER_DELETE)).isFalse();
|
||||
}
|
||||
|
||||
@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
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
assertThat(user1).isEqualTo(user2);
|
||||
assertThat(user1.hashCode()).isEqualTo(user2.hashCode());
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
// Act
|
||||
Set<Role> retrievedRoles = user.roles();
|
||||
|
||||
// Assert
|
||||
assertThatThrownBy(() -> retrievedRoles.add(createTestRole("PRODUCTION_MANAGER")))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
// Act
|
||||
Set<Permission> permissions = user.getAllPermissions();
|
||||
|
||||
// Assert
|
||||
assertThatThrownBy(() -> permissions.add(Permission.USER_WRITE))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
}
|
||||
|
||||
@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
|
||||
);
|
||||
|
||||
// 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
|
||||
);
|
||||
}
|
||||
|
||||
private Role createRoleWithPermissions(String roleName, Set<Permission> permissions) {
|
||||
return Role.reconstitute(
|
||||
RoleId.generate(),
|
||||
RoleName.valueOf(roleName),
|
||||
new HashSet<>(permissions),
|
||||
"Test role: " + roleName
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
package de.effigenix.infrastructure.security;
|
||||
|
||||
import de.effigenix.domain.usermanagement.PasswordHash;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for BCryptPasswordHasher Implementation.
|
||||
* Tests hashing, verification, and validation logic for password security.
|
||||
*/
|
||||
@DisplayName("BCryptPasswordHasher Implementation")
|
||||
class BCryptPasswordHasherTest {
|
||||
|
||||
private BCryptPasswordHasher hasher;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
hasher = new BCryptPasswordHasher();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_HashPassword_When_ValidPasswordProvided")
|
||||
void should_HashPassword_When_ValidPasswordProvided() {
|
||||
// Act
|
||||
PasswordHash hash = hasher.hash("ValidPassword123!");
|
||||
|
||||
// Assert
|
||||
assertThat(hash).isNotNull();
|
||||
assertThat(hash.value()).startsWith("$2a$12$")
|
||||
.hasSize(60);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_GenerateUniqueBcryptHash_When_SamePasswordHashedTwice")
|
||||
void should_GenerateUniqueBcryptHash_When_SamePasswordHashedTwice() {
|
||||
// Act
|
||||
PasswordHash hash1 = hasher.hash("SamePassword123!");
|
||||
PasswordHash hash2 = hasher.hash("SamePassword123!");
|
||||
|
||||
// Assert
|
||||
assertThat(hash1.value()).isNotEqualTo(hash2.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_NullPasswordProvided")
|
||||
void should_ThrowException_When_NullPasswordProvided() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> hasher.hash(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Password cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_EmptyPasswordProvided")
|
||||
void should_ThrowException_When_EmptyPasswordProvided() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> hasher.hash(""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Password cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_BlankPasswordProvided")
|
||||
void should_ThrowException_When_BlankPasswordProvided() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> hasher.hash(" "))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ThrowException_When_WeakPasswordProvidedToHash")
|
||||
void should_ThrowException_When_WeakPasswordProvidedToHash() {
|
||||
// Act & Assert
|
||||
assertThatThrownBy(() -> hasher.hash("weak"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Password does not meet minimum requirements");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_VerifyPassword_When_CorrectPasswordProvided")
|
||||
void should_VerifyPassword_When_CorrectPasswordProvided() {
|
||||
// Arrange
|
||||
String plainPassword = "MySecurePass123!";
|
||||
PasswordHash hash = hasher.hash(plainPassword);
|
||||
|
||||
// Act
|
||||
boolean isValid = hasher.verify(plainPassword, hash);
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_RejectPassword_When_IncorrectPasswordProvided")
|
||||
void should_RejectPassword_When_IncorrectPasswordProvided() {
|
||||
// Arrange
|
||||
PasswordHash hash = hasher.hash("CorrectPass123!");
|
||||
|
||||
// Act
|
||||
boolean isValid = hasher.verify("WrongPassword123!", hash);
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ReturnFalse_When_NullPasswordProvidedToVerify")
|
||||
void should_ReturnFalse_When_NullPasswordProvidedToVerify() {
|
||||
// Arrange
|
||||
PasswordHash hash = hasher.hash("ValidPassword123!");
|
||||
|
||||
// Act
|
||||
boolean isValid = hasher.verify(null, hash);
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ReturnFalse_When_NullHashProvidedToVerify")
|
||||
void should_ReturnFalse_When_NullHashProvidedToVerify() {
|
||||
// Act
|
||||
boolean isValid = hasher.verify("ValidPassword123!", null);
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ReturnFalse_When_BothNullProvidedToVerify")
|
||||
void should_ReturnFalse_When_BothNullProvidedToVerify() {
|
||||
// Act
|
||||
boolean isValid = hasher.verify(null, null);
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ValidatePasswordLength_When_LessThan8Characters")
|
||||
void should_ValidatePasswordLength_When_LessThan8Characters() {
|
||||
// Act
|
||||
boolean isValid = hasher.isValidPassword("Short1!");
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ValidatePasswordLength_When_Exactly8Characters")
|
||||
void should_ValidatePasswordLength_When_Exactly8Characters() {
|
||||
// Act
|
||||
boolean isValid = hasher.isValidPassword("Exactly1!");
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ValidateRequiresUpperCase_When_PasswordMissingUpperCase")
|
||||
void should_ValidateRequiresUpperCase_When_PasswordMissingUpperCase() {
|
||||
// Act
|
||||
boolean isValid = hasher.isValidPassword("lowercase123!");
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ValidateRequiresLowerCase_When_PasswordMissingLowerCase")
|
||||
void should_ValidateRequiresLowerCase_When_PasswordMissingLowerCase() {
|
||||
// Act
|
||||
boolean isValid = hasher.isValidPassword("UPPERCASE123!");
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ValidateRequiresDigit_When_PasswordMissingDigit")
|
||||
void should_ValidateRequiresDigit_When_PasswordMissingDigit() {
|
||||
// Act
|
||||
boolean isValid = hasher.isValidPassword("NoDigitPass!");
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ValidateRequiresSpecialChar_When_PasswordMissingSpecialChar")
|
||||
void should_ValidateRequiresSpecialChar_When_PasswordMissingSpecialChar() {
|
||||
// Act
|
||||
boolean isValid = hasher.isValidPassword("NoSpecial123");
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_AcceptValidPassword_When_AllRequirementsMet")
|
||||
void should_AcceptValidPassword_When_AllRequirementsMet() {
|
||||
// Act
|
||||
boolean isValid = hasher.isValidPassword("ValidPass123!");
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ReturnFalse_When_NullPasswordProvidedToValidate")
|
||||
void should_ReturnFalse_When_NullPasswordProvidedToValidate() {
|
||||
// Act
|
||||
boolean isValid = hasher.isValidPassword(null);
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"ValidPass123!",
|
||||
"Secure#Pass456",
|
||||
"Complex@Pass789",
|
||||
"MyP@ssw0rd",
|
||||
"Str0ng!Pass"
|
||||
})
|
||||
@DisplayName("should_AcceptAllValidPasswords_When_RequirementsMetWithDifferentFormats")
|
||||
void should_AcceptAllValidPasswords_When_RequirementsMetWithDifferentFormats(String password) {
|
||||
// Act
|
||||
boolean isValid = hasher.isValidPassword(password);
|
||||
|
||||
// Assert
|
||||
assertThat(isValid).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ProduceCorrectBcryptFormat_When_PasswordHashed")
|
||||
void should_ProduceCorrectBcryptFormat_When_PasswordHashed() {
|
||||
// Act
|
||||
PasswordHash hash = hasher.hash("ValidPassword123!");
|
||||
|
||||
// Assert
|
||||
assertThat(hash.value())
|
||||
.matches("\\$2[aby]\\$\\d{2}\\$.*") // Matches BCrypt format
|
||||
.hasSize(60); // BCrypt hash is 60 chars
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_UseStrength12_When_CreatingHasher")
|
||||
void should_UseStrength12_When_CreatingHasher() {
|
||||
// Arrange
|
||||
String password = "TestPassword123!";
|
||||
|
||||
// Act
|
||||
PasswordHash hash = hasher.hash(password);
|
||||
|
||||
// Assert
|
||||
// BCrypt with strength 12 produces format $2a$12$...
|
||||
assertThat(hash.value()).matches("\\$2[aby]\\$12\\$.*");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ResistTimingAttacks_When_VerifyingPassword")
|
||||
void should_ResistTimingAttacks_When_VerifyingPassword() {
|
||||
// Arrange
|
||||
PasswordHash hash = hasher.hash("ValidPassword123!");
|
||||
|
||||
// Act - Verify with correct and incorrect passwords
|
||||
long startCorrect = System.nanoTime();
|
||||
boolean resultCorrect = hasher.verify("ValidPassword123!", hash);
|
||||
long timeCorrect = System.nanoTime() - startCorrect;
|
||||
|
||||
long startIncorrect = System.nanoTime();
|
||||
boolean resultIncorrect = hasher.verify("WrongPassword123!", hash);
|
||||
long timeIncorrect = System.nanoTime() - startIncorrect;
|
||||
|
||||
// Assert
|
||||
assertThat(resultCorrect).isTrue();
|
||||
assertThat(resultIncorrect).isFalse();
|
||||
// BCrypt uses constant-time comparison, so times should be similar
|
||||
// (Allow for some variance due to system load)
|
||||
long timeDifference = Math.abs(timeCorrect - timeIncorrect);
|
||||
long maxAllowedDifference = Math.max(timeCorrect, timeIncorrect) / 2;
|
||||
assertThat(timeDifference).isLessThan(maxAllowedDifference);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_HandleInvalidHashGracefully_When_VerifyingWithMalformedHash")
|
||||
void should_HandleInvalidHashGracefully_When_VerifyingWithMalformedHash() {
|
||||
// Arrange
|
||||
PasswordHash malformedHash = new PasswordHash("$2a$" + "x".repeat(56));
|
||||
|
||||
// Act
|
||||
boolean result = hasher.verify("SomePassword123!", malformedHash);
|
||||
|
||||
// Assert
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_AcceptMultipleValidFormats_When_VerifyingDifferentBcryptVersions")
|
||||
void should_AcceptMultipleValidFormats_When_VerifyingDifferentBcryptVersions() {
|
||||
// Arrange
|
||||
String password = "ValidPassword123!";
|
||||
PasswordHash hash1 = hasher.hash(password);
|
||||
PasswordHash hash2 = hasher.hash(password);
|
||||
|
||||
// Act & Assert
|
||||
assertThat(hasher.verify(password, hash1)).isTrue();
|
||||
assertThat(hasher.verify(password, hash2)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_HashAndVerifyWithLongPassword_When_PasswordExceedsMinimum")
|
||||
void should_HashAndVerifyWithLongPassword_When_PasswordExceedsMinimum() {
|
||||
// Arrange
|
||||
String longPassword = "VeryLongPasswordWith123!ExtraCharactersForSecurity";
|
||||
|
||||
// Act
|
||||
PasswordHash hash = hasher.hash(longPassword);
|
||||
boolean verified = hasher.verify(longPassword, hash);
|
||||
|
||||
// Assert
|
||||
assertThat(verified).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_DifferentiateBetweenSimilarPasswords_When_VerifyingTypos")
|
||||
void should_DifferentiateBetweenSimilarPasswords_When_VerifyingTypos() {
|
||||
// Arrange
|
||||
PasswordHash hash = hasher.hash("CorrectPass123!");
|
||||
|
||||
// Act
|
||||
boolean result1 = hasher.verify("CorrectPass123!", hash); // Correct
|
||||
boolean result2 = hasher.verify("CorrectPass124!", hash); // One character different
|
||||
|
||||
// Assert
|
||||
assertThat(result1).isTrue();
|
||||
assertThat(result2).isFalse();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,350 @@
|
|||
package de.effigenix.infrastructure.usermanagement.persistence.mapper;
|
||||
|
||||
import de.effigenix.domain.usermanagement.*;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for RoleMapper.
|
||||
* Tests bidirectional mapping between Role domain entity and RoleEntity JPA entity.
|
||||
*/
|
||||
@DisplayName("RoleMapper")
|
||||
class RoleMapperTest {
|
||||
|
||||
private final RoleMapper roleMapper = new RoleMapper();
|
||||
|
||||
private Role domainRole;
|
||||
private RoleEntity jpaEntity;
|
||||
private Set<Permission> permissions;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
permissions = new HashSet<>(Set.of(
|
||||
Permission.USER_READ,
|
||||
Permission.USER_WRITE,
|
||||
Permission.ROLE_READ
|
||||
));
|
||||
|
||||
// Create JPA entity first
|
||||
jpaEntity = new RoleEntity(
|
||||
"role-123",
|
||||
RoleName.ADMIN,
|
||||
new HashSet<>(permissions),
|
||||
"Administrator role with full access"
|
||||
);
|
||||
|
||||
// Create domain role via mapper (which internally uses reconstitute)
|
||||
domainRole = roleMapper.toDomain(jpaEntity);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_MapRoleToEntity_When_DomainRoleProvided")
|
||||
void should_MapRoleToEntity_When_DomainRoleProvided() {
|
||||
// Act
|
||||
RoleEntity entity = roleMapper.toEntity(domainRole);
|
||||
|
||||
// Assert
|
||||
assertThat(entity).isNotNull();
|
||||
assertThat(entity.getId()).isEqualTo("role-123");
|
||||
assertThat(entity.getName()).isEqualTo(RoleName.ADMIN);
|
||||
assertThat(entity.getDescription()).isEqualTo("Administrator role with full access");
|
||||
assertThat(entity.getPermissions()).containsAll(permissions);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_MapRoleToDomain_When_JpaEntityProvided")
|
||||
void should_MapRoleToDomain_When_JpaEntityProvided() {
|
||||
// Act
|
||||
Role role = roleMapper.toDomain(jpaEntity);
|
||||
|
||||
// Assert
|
||||
assertThat(role).isNotNull();
|
||||
assertThat(role.id().value()).isEqualTo("role-123");
|
||||
assertThat(role.name()).isEqualTo(RoleName.ADMIN);
|
||||
assertThat(role.description()).isEqualTo("Administrator role with full access");
|
||||
assertThat(role.permissions()).containsAll(permissions);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ReturnNull_When_NullRoleProvidedToToEntity")
|
||||
void should_ReturnNull_When_NullRoleProvidedToToEntity() {
|
||||
// Act
|
||||
RoleEntity entity = roleMapper.toEntity(null);
|
||||
|
||||
// Assert
|
||||
assertThat(entity).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ReturnNull_When_NullEntityProvidedToToDomain")
|
||||
void should_ReturnNull_When_NullEntityProvidedToToDomain() {
|
||||
// Act
|
||||
Role role = roleMapper.toDomain(null);
|
||||
|
||||
// Assert
|
||||
assertThat(role).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_PreserveAllRoleFields_When_MappingToEntity")
|
||||
void should_PreserveAllRoleFields_When_MappingToEntity() {
|
||||
// Arrange
|
||||
Set<Permission> perms = new HashSet<>(Set.of(
|
||||
Permission.BATCH_READ,
|
||||
Permission.BATCH_WRITE,
|
||||
Permission.BATCH_COMPLETE
|
||||
));
|
||||
Role role = roleMapper.toDomain(new RoleEntity(
|
||||
"role-prod-manager",
|
||||
RoleName.PRODUCTION_MANAGER,
|
||||
new HashSet<>(perms),
|
||||
"Production Manager role"
|
||||
));
|
||||
|
||||
// Act
|
||||
RoleEntity entity = roleMapper.toEntity(role);
|
||||
|
||||
// Assert
|
||||
assertThat(entity.getId()).isEqualTo("role-prod-manager");
|
||||
assertThat(entity.getName()).isEqualTo(RoleName.PRODUCTION_MANAGER);
|
||||
assertThat(entity.getDescription()).isEqualTo("Production Manager role");
|
||||
assertThat(entity.getPermissions()).containsAll(perms);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_PreserveAllEntityFields_When_MappingToDomain")
|
||||
void should_PreserveAllEntityFields_When_MappingToDomain() {
|
||||
// Arrange
|
||||
Set<Permission> perms = new HashSet<>(Set.of(
|
||||
Permission.STOCK_READ,
|
||||
Permission.STOCK_WRITE
|
||||
));
|
||||
RoleEntity entity = new RoleEntity(
|
||||
"role-warehouse",
|
||||
RoleName.WAREHOUSE_WORKER,
|
||||
perms,
|
||||
"Warehouse Worker role"
|
||||
);
|
||||
|
||||
// Act
|
||||
Role role = roleMapper.toDomain(entity);
|
||||
|
||||
// Assert
|
||||
assertThat(role.id().value()).isEqualTo("role-warehouse");
|
||||
assertThat(role.name()).isEqualTo(RoleName.WAREHOUSE_WORKER);
|
||||
assertThat(role.description()).isEqualTo("Warehouse Worker role");
|
||||
assertThat(role.permissions()).containsAll(perms);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_HandleEmptyPermissions_When_RoleHasNoPermissions")
|
||||
void should_HandleEmptyPermissions_When_RoleHasNoPermissions() {
|
||||
// Arrange
|
||||
Role roleNoPerms = roleMapper.toDomain(new RoleEntity(
|
||||
"role-empty",
|
||||
RoleName.ADMIN,
|
||||
new HashSet<>(),
|
||||
"Empty role"
|
||||
));
|
||||
|
||||
// Act
|
||||
RoleEntity entity = roleMapper.toEntity(roleNoPerms);
|
||||
|
||||
// Assert
|
||||
assertThat(entity.getPermissions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_HandleNullPermissions_When_MappingToEntity")
|
||||
void should_HandleNullPermissions_When_MappingToEntity() {
|
||||
// Arrange
|
||||
Role roleNullPerms = roleMapper.toDomain(new RoleEntity(
|
||||
"role-null",
|
||||
RoleName.ADMIN,
|
||||
null,
|
||||
"Role with null permissions"
|
||||
));
|
||||
|
||||
// Act
|
||||
RoleEntity entity = roleMapper.toEntity(roleNullPerms);
|
||||
|
||||
// Assert
|
||||
assertThat(entity.getPermissions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_HandleNullPermissions_When_MappingToDomain")
|
||||
void should_HandleNullPermissions_When_MappingToDomain() {
|
||||
// Arrange
|
||||
RoleEntity entityNullPerms = new RoleEntity(
|
||||
"role-null",
|
||||
RoleName.ADMIN,
|
||||
null,
|
||||
"Entity with null permissions"
|
||||
);
|
||||
|
||||
// Act
|
||||
Role role = roleMapper.toDomain(entityNullPerms);
|
||||
|
||||
// Assert
|
||||
assertThat(role.permissions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_BidirectionalMapping_When_MappingRoleBackAndForth")
|
||||
void should_BidirectionalMapping_When_MappingRoleBackAndForth() {
|
||||
// Act
|
||||
RoleEntity entity = roleMapper.toEntity(domainRole);
|
||||
Role mappedBackRole = roleMapper.toDomain(entity);
|
||||
|
||||
// Assert
|
||||
assertThat(mappedBackRole.id().value()).isEqualTo(domainRole.id().value());
|
||||
assertThat(mappedBackRole.name()).isEqualTo(domainRole.name());
|
||||
assertThat(mappedBackRole.description()).isEqualTo(domainRole.description());
|
||||
assertThat(mappedBackRole.permissions()).containsAll(domainRole.permissions());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_CreateNewSetForPermissions_When_MappingToEntity")
|
||||
void should_CreateNewSetForPermissions_When_MappingToEntity() {
|
||||
// Act
|
||||
RoleEntity entity = roleMapper.toEntity(domainRole);
|
||||
|
||||
// Assert
|
||||
assertThat(entity.getPermissions()).isNotNull();
|
||||
assertThat(entity.getPermissions()).isInstanceOf(Set.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_CreateNewSetForPermissions_When_MappingToDomain")
|
||||
void should_CreateNewSetForPermissions_When_MappingToDomain() {
|
||||
// Act
|
||||
Role role = roleMapper.toDomain(jpaEntity);
|
||||
|
||||
// Assert
|
||||
assertThat(role.permissions()).isNotNull();
|
||||
assertThat(role.permissions()).isInstanceOf(Set.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_MapAllRoleNames_When_DifferentRoleNamesUsed")
|
||||
void should_MapAllRoleNames_When_DifferentRoleNamesUsed() {
|
||||
// Act
|
||||
Role adminRole = roleMapper.toDomain(
|
||||
new RoleEntity("id-1", RoleName.ADMIN, new HashSet<>(), "Admin")
|
||||
);
|
||||
Role prodWorkerRole = roleMapper.toDomain(
|
||||
new RoleEntity("id-2", RoleName.PRODUCTION_WORKER, new HashSet<>(), "Worker")
|
||||
);
|
||||
Role warehouseRole = roleMapper.toDomain(
|
||||
new RoleEntity("id-3", RoleName.WAREHOUSE_WORKER, new HashSet<>(), "Warehouse")
|
||||
);
|
||||
|
||||
// Assert
|
||||
assertThat(adminRole.name()).isEqualTo(RoleName.ADMIN);
|
||||
assertThat(prodWorkerRole.name()).isEqualTo(RoleName.PRODUCTION_WORKER);
|
||||
assertThat(warehouseRole.name()).isEqualTo(RoleName.WAREHOUSE_WORKER);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_MapAllPermissionTypes_When_DifferentPermissionsUsed")
|
||||
void should_MapAllPermissionTypes_When_DifferentPermissionsUsed() {
|
||||
// Arrange
|
||||
Set<Permission> allPermissions = new HashSet<>(Set.of(
|
||||
Permission.USER_READ,
|
||||
Permission.BATCH_WRITE,
|
||||
Permission.STOCK_READ,
|
||||
Permission.ORDER_DELETE,
|
||||
Permission.REPORT_GENERATE
|
||||
));
|
||||
Role roleWithVariousPerms = roleMapper.toDomain(new RoleEntity(
|
||||
"role-various",
|
||||
RoleName.ADMIN,
|
||||
new HashSet<>(allPermissions),
|
||||
"Role with various permissions"
|
||||
));
|
||||
|
||||
// Act
|
||||
RoleEntity entity = roleMapper.toEntity(roleWithVariousPerms);
|
||||
|
||||
// Assert
|
||||
assertThat(entity.getPermissions()).containsAll(allPermissions);
|
||||
assertThat(entity.getPermissions()).hasSize(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_HandleNullDescription_When_MappingToEntity")
|
||||
void should_HandleNullDescription_When_MappingToEntity() {
|
||||
// Arrange
|
||||
Role roleNullDesc = roleMapper.toDomain(new RoleEntity(
|
||||
"role-no-desc",
|
||||
RoleName.ADMIN,
|
||||
new HashSet<>(permissions),
|
||||
null
|
||||
));
|
||||
|
||||
// Act
|
||||
RoleEntity entity = roleMapper.toEntity(roleNullDesc);
|
||||
|
||||
// Assert
|
||||
assertThat(entity.getDescription()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_HandleNullDescription_When_MappingToDomain")
|
||||
void should_HandleNullDescription_When_MappingToDomain() {
|
||||
// Arrange
|
||||
RoleEntity entityNullDesc = new RoleEntity(
|
||||
"role-no-desc",
|
||||
RoleName.ADMIN,
|
||||
permissions,
|
||||
null
|
||||
);
|
||||
|
||||
// Act
|
||||
Role role = roleMapper.toDomain(entityNullDesc);
|
||||
|
||||
// Assert
|
||||
assertThat(role.description()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_MapLargePermissionSet_When_RoleHasManyPermissions")
|
||||
void should_MapLargePermissionSet_When_RoleHasManyPermissions() {
|
||||
// Arrange
|
||||
Set<Permission> largePermSet = new HashSet<>(Set.of(
|
||||
Permission.values() // All permissions
|
||||
));
|
||||
Role roleWithManyPerms = roleMapper.toDomain(new RoleEntity(
|
||||
"role-admin-full",
|
||||
RoleName.ADMIN,
|
||||
new HashSet<>(largePermSet),
|
||||
"Super admin with all permissions"
|
||||
));
|
||||
|
||||
// Act
|
||||
RoleEntity entity = roleMapper.toEntity(roleWithManyPerms);
|
||||
Role mappedBack = roleMapper.toDomain(entity);
|
||||
|
||||
// Assert
|
||||
assertThat(entity.getPermissions()).hasSize(largePermSet.size());
|
||||
assertThat(mappedBack.permissions()).hasSize(largePermSet.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_PreservePermissionImmutability_When_MappingToEntity")
|
||||
void should_PreservePermissionImmutability_When_MappingToEntity() {
|
||||
// Act
|
||||
RoleEntity entity = roleMapper.toEntity(domainRole);
|
||||
|
||||
// Assert - mapped entity should have same permissions
|
||||
assertThat(entity.getPermissions()).containsAll(domainRole.permissions());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
package de.effigenix.infrastructure.usermanagement.persistence.mapper;
|
||||
|
||||
import de.effigenix.domain.usermanagement.*;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for UserMapper.
|
||||
* Tests bidirectional mapping between User domain entity and UserEntity JPA entity.
|
||||
*
|
||||
* Uses real RoleMapper (no mocks) because both mappers are simple stateless components.
|
||||
* Domain User/Role objects are created via the mapper's toDomain() method which internally
|
||||
* calls the package-private reconstitute() factory method.
|
||||
*/
|
||||
@DisplayName("UserMapper")
|
||||
class UserMapperTest {
|
||||
|
||||
private final RoleMapper roleMapper = new RoleMapper();
|
||||
private final UserMapper userMapper = new UserMapper(roleMapper);
|
||||
|
||||
private User domainUser;
|
||||
private UserEntity jpaEntity;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
createdAt = LocalDateTime.now();
|
||||
|
||||
// Create JPA entity first
|
||||
jpaEntity = new UserEntity(
|
||||
"user-123",
|
||||
"john.doe",
|
||||
"john@example.com",
|
||||
"$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW",
|
||||
new HashSet<>(),
|
||||
"branch-1",
|
||||
UserStatus.ACTIVE,
|
||||
createdAt,
|
||||
null
|
||||
);
|
||||
|
||||
// Create domain user via mapper (which internally uses reconstitute)
|
||||
domainUser = userMapper.toDomain(jpaEntity);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_MapUserToEntity_When_DomainUserProvided")
|
||||
void should_MapUserToEntity_When_DomainUserProvided() {
|
||||
// Act
|
||||
UserEntity entity = userMapper.toEntity(domainUser);
|
||||
|
||||
// Assert
|
||||
assertThat(entity).isNotNull();
|
||||
assertThat(entity.getId()).isEqualTo("user-123");
|
||||
assertThat(entity.getUsername()).isEqualTo("john.doe");
|
||||
assertThat(entity.getEmail()).isEqualTo("john@example.com");
|
||||
assertThat(entity.getPasswordHash()).isEqualTo("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
|
||||
assertThat(entity.getBranchId()).isEqualTo("branch-1");
|
||||
assertThat(entity.getStatus()).isEqualTo(UserStatus.ACTIVE);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(createdAt);
|
||||
assertThat(entity.getLastLogin()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_MapUserToDomain_When_JpaEntityProvided")
|
||||
void should_MapUserToDomain_When_JpaEntityProvided() {
|
||||
// Act
|
||||
User user = userMapper.toDomain(jpaEntity);
|
||||
|
||||
// Assert
|
||||
assertThat(user).isNotNull();
|
||||
assertThat(user.id().value()).isEqualTo("user-123");
|
||||
assertThat(user.username()).isEqualTo("john.doe");
|
||||
assertThat(user.email()).isEqualTo("john@example.com");
|
||||
assertThat(user.passwordHash().value()).isEqualTo("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
|
||||
assertThat(user.branchId()).isEqualTo("branch-1");
|
||||
assertThat(user.status()).isEqualTo(UserStatus.ACTIVE);
|
||||
assertThat(user.createdAt()).isEqualTo(createdAt);
|
||||
assertThat(user.lastLogin()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ReturnNull_When_NullUserProvidedToToEntity")
|
||||
void should_ReturnNull_When_NullUserProvidedToToEntity() {
|
||||
// Act
|
||||
UserEntity entity = userMapper.toEntity(null);
|
||||
|
||||
// Assert
|
||||
assertThat(entity).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_ReturnNull_When_NullEntityProvidedToToDomain")
|
||||
void should_ReturnNull_When_NullEntityProvidedToToDomain() {
|
||||
// Act
|
||||
User user = userMapper.toDomain(null);
|
||||
|
||||
// Assert
|
||||
assertThat(user).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_PreserveAllUserFields_When_MappingToEntity")
|
||||
void should_PreserveAllUserFields_When_MappingToEntity() {
|
||||
// Arrange
|
||||
LocalDateTime lastLogin = LocalDateTime.now();
|
||||
UserEntity sourceEntity = new UserEntity(
|
||||
"user-456",
|
||||
"jane.smith",
|
||||
"jane@example.com",
|
||||
"$2b$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW",
|
||||
new HashSet<>(),
|
||||
"branch-2",
|
||||
UserStatus.LOCKED,
|
||||
createdAt,
|
||||
lastLogin
|
||||
);
|
||||
User userWithLastLogin = userMapper.toDomain(sourceEntity);
|
||||
|
||||
// Act
|
||||
UserEntity entity = userMapper.toEntity(userWithLastLogin);
|
||||
|
||||
// Assert
|
||||
assertThat(entity.getId()).isEqualTo("user-456");
|
||||
assertThat(entity.getUsername()).isEqualTo("jane.smith");
|
||||
assertThat(entity.getEmail()).isEqualTo("jane@example.com");
|
||||
assertThat(entity.getPasswordHash()).isEqualTo("$2b$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
|
||||
assertThat(entity.getBranchId()).isEqualTo("branch-2");
|
||||
assertThat(entity.getStatus()).isEqualTo(UserStatus.LOCKED);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(createdAt);
|
||||
assertThat(entity.getLastLogin()).isEqualTo(lastLogin);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_PreserveAllEntityFields_When_MappingToDomain")
|
||||
void should_PreserveAllEntityFields_When_MappingToDomain() {
|
||||
// Arrange
|
||||
LocalDateTime lastLogin = LocalDateTime.now();
|
||||
UserEntity entityWithLastLogin = new UserEntity(
|
||||
"user-789",
|
||||
"bob.jones",
|
||||
"bob@example.com",
|
||||
"$2y$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW",
|
||||
new HashSet<>(),
|
||||
"branch-3",
|
||||
UserStatus.INACTIVE,
|
||||
createdAt,
|
||||
lastLogin
|
||||
);
|
||||
|
||||
// Act
|
||||
User user = userMapper.toDomain(entityWithLastLogin);
|
||||
|
||||
// Assert
|
||||
assertThat(user.id().value()).isEqualTo("user-789");
|
||||
assertThat(user.username()).isEqualTo("bob.jones");
|
||||
assertThat(user.email()).isEqualTo("bob@example.com");
|
||||
assertThat(user.passwordHash().value()).isEqualTo("$2y$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
|
||||
assertThat(user.branchId()).isEqualTo("branch-3");
|
||||
assertThat(user.status()).isEqualTo(UserStatus.INACTIVE);
|
||||
assertThat(user.createdAt()).isEqualTo(createdAt);
|
||||
assertThat(user.lastLogin()).isEqualTo(lastLogin);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_MapRoleEntitiesCorrectly_When_UserHasRoles")
|
||||
void should_MapRoleEntitiesCorrectly_When_UserHasRoles() {
|
||||
// Arrange
|
||||
RoleEntity roleEntity1 = new RoleEntity("role-1", RoleName.ADMIN, new HashSet<>(), "Admin");
|
||||
RoleEntity roleEntity2 = new RoleEntity("role-2", RoleName.PRODUCTION_WORKER, new HashSet<>(), "Worker");
|
||||
|
||||
UserEntity userEntityWithRoles = new UserEntity(
|
||||
"user-999",
|
||||
"john.doe",
|
||||
"john@example.com",
|
||||
"$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW",
|
||||
new HashSet<>(Set.of(roleEntity1, roleEntity2)),
|
||||
"branch-1",
|
||||
UserStatus.ACTIVE,
|
||||
createdAt,
|
||||
null
|
||||
);
|
||||
|
||||
// Map entity to domain (toDomain maps roles via real roleMapper)
|
||||
User userWithRoles = userMapper.toDomain(userEntityWithRoles);
|
||||
|
||||
// Act
|
||||
UserEntity entity = userMapper.toEntity(userWithRoles);
|
||||
|
||||
// Assert
|
||||
assertThat(entity.getRoles()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_HandleEmptyRoleSet_When_UserHasNoRoles")
|
||||
void should_HandleEmptyRoleSet_When_UserHasNoRoles() {
|
||||
// Act
|
||||
UserEntity entity = userMapper.toEntity(domainUser);
|
||||
|
||||
// Assert
|
||||
assertThat(entity.getRoles()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_HandleNullRolesInEntity_When_MappingToDomain")
|
||||
void should_HandleNullRolesInEntity_When_MappingToDomain() {
|
||||
// Arrange
|
||||
jpaEntity.setRoles(null);
|
||||
|
||||
// Act
|
||||
User user = userMapper.toDomain(jpaEntity);
|
||||
|
||||
// Assert
|
||||
assertThat(user.roles()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_BidirectionalMapping_When_MappingUserBackAndForth")
|
||||
void should_BidirectionalMapping_When_MappingUserBackAndForth() {
|
||||
// Act
|
||||
UserEntity entity = userMapper.toEntity(domainUser);
|
||||
User mappedBackUser = userMapper.toDomain(entity);
|
||||
|
||||
// Assert
|
||||
assertThat(mappedBackUser.id().value()).isEqualTo(domainUser.id().value());
|
||||
assertThat(mappedBackUser.username()).isEqualTo(domainUser.username());
|
||||
assertThat(mappedBackUser.email()).isEqualTo(domainUser.email());
|
||||
assertThat(mappedBackUser.passwordHash().value()).isEqualTo(domainUser.passwordHash().value());
|
||||
assertThat(mappedBackUser.branchId()).isEqualTo(domainUser.branchId());
|
||||
assertThat(mappedBackUser.status()).isEqualTo(domainUser.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_CreateNewSetForRoles_When_MappingToEntity")
|
||||
void should_CreateNewSetForRoles_When_MappingToEntity() {
|
||||
// Act
|
||||
UserEntity entity = userMapper.toEntity(domainUser);
|
||||
|
||||
// Assert
|
||||
assertThat(entity.getRoles()).isNotNull();
|
||||
assertThat(entity.getRoles()).isInstanceOf(Set.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_CreateNewSetForRoles_When_MappingToDomain")
|
||||
void should_CreateNewSetForRoles_When_MappingToDomain() {
|
||||
// Act
|
||||
User user = userMapper.toDomain(jpaEntity);
|
||||
|
||||
// Assert
|
||||
assertThat(user.roles()).isNotNull();
|
||||
assertThat(user.roles()).isInstanceOf(Set.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should_MapAllUserStatuses_When_DifferentStatusesUsed")
|
||||
void should_MapAllUserStatuses_When_DifferentStatusesUsed() {
|
||||
// Arrange - create users with different statuses via entity -> domain mapping
|
||||
User activeUser = userMapper.toDomain(new UserEntity(
|
||||
"id-1", "user1", "u1@test.com",
|
||||
"$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW",
|
||||
new HashSet<>(), "b1", UserStatus.ACTIVE, createdAt, null));
|
||||
|
||||
User inactiveUser = userMapper.toDomain(new UserEntity(
|
||||
"id-2", "user2", "u2@test.com",
|
||||
"$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW",
|
||||
new HashSet<>(), "b1", UserStatus.INACTIVE, createdAt, null));
|
||||
|
||||
User lockedUser = userMapper.toDomain(new UserEntity(
|
||||
"id-3", "user3", "u3@test.com",
|
||||
"$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW",
|
||||
new HashSet<>(), "b1", UserStatus.LOCKED, createdAt, null));
|
||||
|
||||
// Act
|
||||
UserEntity activeEntity = userMapper.toEntity(activeUser);
|
||||
UserEntity inactiveEntity = userMapper.toEntity(inactiveUser);
|
||||
UserEntity lockedEntity = userMapper.toEntity(lockedUser);
|
||||
|
||||
// Assert
|
||||
assertThat(activeEntity.getStatus()).isEqualTo(UserStatus.ACTIVE);
|
||||
assertThat(inactiveEntity.getStatus()).isEqualTo(UserStatus.INACTIVE);
|
||||
assertThat(lockedEntity.getStatus()).isEqualTo(UserStatus.LOCKED);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,456 @@
|
|||
package de.effigenix.infrastructure.usermanagement.web;
|
||||
|
||||
import de.effigenix.application.usermanagement.dto.SessionToken;
|
||||
import de.effigenix.domain.usermanagement.RoleName;
|
||||
import de.effigenix.domain.usermanagement.UserStatus;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.RoleJpaRepository;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.UserJpaRepository;
|
||||
import de.effigenix.infrastructure.usermanagement.web.dto.LoginRequest;
|
||||
import de.effigenix.infrastructure.usermanagement.web.dto.LoginResponse;
|
||||
import de.effigenix.infrastructure.usermanagement.web.dto.RefreshTokenRequest;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* Integration tests for Authentication Controller.
|
||||
*
|
||||
* Tests authentication flows:
|
||||
* - Login with valid/invalid credentials
|
||||
* - JWT token validation
|
||||
* - Logout flow
|
||||
* - Refresh token flow
|
||||
*
|
||||
* Uses:
|
||||
* - @SpringBootTest for full application context
|
||||
* - @AutoConfigureMockMvc for MockMvc HTTP testing
|
||||
* - @Transactional for test isolation
|
||||
* - H2 in-memory database (configured in application-test.yml)
|
||||
*
|
||||
* Test Database:
|
||||
* - H2 in-memory database
|
||||
* - ddl-auto: create-drop (schema recreated for each test)
|
||||
* - Each test runs in a transaction and is rolled back after completion
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
@DisplayName("Authentication Controller Integration Tests")
|
||||
class AuthControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private UserJpaRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private RoleJpaRepository roleRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String jwtSecret;
|
||||
|
||||
@Value("${jwt.expiration}")
|
||||
private long jwtExpiration;
|
||||
|
||||
private String validUsername;
|
||||
private String validEmail;
|
||||
private String validPassword;
|
||||
private String validUserId;
|
||||
private String validRefreshToken;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
validUsername = "auth.test.user";
|
||||
validEmail = "auth.test@example.com";
|
||||
validPassword = "SecurePass123";
|
||||
validUserId = UUID.randomUUID().toString();
|
||||
|
||||
// Create test user with ADMIN role
|
||||
RoleEntity adminRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(),
|
||||
RoleName.ADMIN,
|
||||
Set.of(), // Empty permissions for testing
|
||||
"Admin role"
|
||||
);
|
||||
roleRepository.save(adminRole);
|
||||
|
||||
UserEntity testUser = new UserEntity(
|
||||
validUserId,
|
||||
validUsername,
|
||||
validEmail,
|
||||
passwordEncoder.encode(validPassword),
|
||||
Set.of(adminRole),
|
||||
"BRANCH-TEST-001",
|
||||
UserStatus.ACTIVE,
|
||||
LocalDateTime.now(),
|
||||
null
|
||||
);
|
||||
userRepository.save(testUser);
|
||||
|
||||
// Create a valid refresh token
|
||||
validRefreshToken = generateTestRefreshToken(validUserId, validUsername);
|
||||
}
|
||||
|
||||
// ==================== LOGIN TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Login with valid credentials should return 200 with JWT tokens")
|
||||
void testLoginWithValidCredentials() throws Exception {
|
||||
LoginRequest request = new LoginRequest(validUsername, validPassword);
|
||||
|
||||
MvcResult result = mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.accessToken").isNotEmpty())
|
||||
.andExpect(jsonPath("$.tokenType").value("Bearer"))
|
||||
.andExpect(jsonPath("$.expiresIn").isNumber())
|
||||
.andExpect(jsonPath("$.expiresAt").isNotEmpty())
|
||||
.andExpect(jsonPath("$.refreshToken").isNotEmpty())
|
||||
.andReturn();
|
||||
|
||||
String responseBody = result.getResponse().getContentAsString();
|
||||
LoginResponse response = objectMapper.readValue(responseBody, LoginResponse.class);
|
||||
|
||||
assertThat(response.accessToken()).isNotBlank();
|
||||
assertThat(response.refreshToken()).isNotBlank();
|
||||
assertThat(response.expiresIn()).isEqualTo(jwtExpiration / 1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Login with invalid username should return 401 Unauthorized")
|
||||
void testLoginWithInvalidUsername() throws Exception {
|
||||
LoginRequest request = new LoginRequest("non.existent.user", validPassword);
|
||||
|
||||
mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.code").exists())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Login with invalid password should return 401 Unauthorized")
|
||||
void testLoginWithInvalidPassword() throws Exception {
|
||||
LoginRequest request = new LoginRequest(validUsername, "WrongPassword123");
|
||||
|
||||
mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Login with locked user should return 401 Unauthorized")
|
||||
void testLoginWithLockedUser() throws Exception {
|
||||
// Lock the user
|
||||
UserEntity user = userRepository.findByUsername(validUsername).orElseThrow();
|
||||
user.setStatus(UserStatus.LOCKED);
|
||||
userRepository.save(user);
|
||||
|
||||
LoginRequest request = new LoginRequest(validUsername, validPassword);
|
||||
|
||||
mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Login with inactive user should return 401 Unauthorized")
|
||||
void testLoginWithInactiveUser() throws Exception {
|
||||
// Set user to inactive
|
||||
UserEntity user = userRepository.findByUsername(validUsername).orElseThrow();
|
||||
user.setStatus(UserStatus.INACTIVE);
|
||||
userRepository.save(user);
|
||||
|
||||
LoginRequest request = new LoginRequest(validUsername, validPassword);
|
||||
|
||||
mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Login with missing username should return 400 Bad Request")
|
||||
void testLoginWithMissingUsername() throws Exception {
|
||||
LoginRequest request = new LoginRequest("", validPassword);
|
||||
|
||||
mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Login with missing password should return 400 Bad Request")
|
||||
void testLoginWithMissingPassword() throws Exception {
|
||||
LoginRequest request = new LoginRequest(validUsername, "");
|
||||
|
||||
mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Login with missing both fields should return 400 Bad Request")
|
||||
void testLoginWithMissingBothFields() throws Exception {
|
||||
mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== LOGOUT TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Logout with valid JWT token should return 204 No Content")
|
||||
void testLogoutWithValidToken() throws Exception {
|
||||
// First login to get a valid token
|
||||
LoginRequest loginRequest = new LoginRequest(validUsername, validPassword);
|
||||
MvcResult loginResult = mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(loginRequest)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
String responseBody = loginResult.getResponse().getContentAsString();
|
||||
LoginResponse loginResponse = objectMapper.readValue(responseBody, LoginResponse.class);
|
||||
String accessToken = loginResponse.accessToken();
|
||||
|
||||
// Now logout with the token
|
||||
mockMvc.perform(post("/api/auth/logout")
|
||||
.header("Authorization", "Bearer " + accessToken))
|
||||
.andExpect(status().isNoContent())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Logout without token should return 401 Unauthorized")
|
||||
void testLogoutWithoutToken() throws Exception {
|
||||
mockMvc.perform(post("/api/auth/logout"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Logout with invalid token should return 401 Unauthorized")
|
||||
void testLogoutWithInvalidToken() throws Exception {
|
||||
String invalidToken = "invalid.token.here";
|
||||
|
||||
mockMvc.perform(post("/api/auth/logout")
|
||||
.header("Authorization", "Bearer " + invalidToken))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Logout with malformed Authorization header should return 401 Unauthorized")
|
||||
void testLogoutWithMalformedAuthHeader() throws Exception {
|
||||
mockMvc.perform(post("/api/auth/logout")
|
||||
.header("Authorization", "InvalidHeader"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== REFRESH TOKEN TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Refresh token with valid refresh token should return 401 (not yet implemented)")
|
||||
void testRefreshTokenWithValidToken() throws Exception {
|
||||
RefreshTokenRequest request = new RefreshTokenRequest(validRefreshToken);
|
||||
|
||||
// Note: refreshSession() is not yet implemented in JwtSessionManager,
|
||||
// so it throws UnsupportedOperationException which maps to 401
|
||||
mockMvc.perform(post("/api/auth/refresh")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Refresh token with invalid refresh token should return 401 Unauthorized")
|
||||
void testRefreshTokenWithInvalidToken() throws Exception {
|
||||
RefreshTokenRequest request = new RefreshTokenRequest("invalid.refresh.token");
|
||||
|
||||
mockMvc.perform(post("/api/auth/refresh")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Refresh token with missing token should return 400 Bad Request")
|
||||
void testRefreshTokenWithMissingToken() throws Exception {
|
||||
RefreshTokenRequest request = new RefreshTokenRequest("");
|
||||
|
||||
mockMvc.perform(post("/api/auth/refresh")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Refresh token with null token should return 400 Bad Request")
|
||||
void testRefreshTokenWithNullToken() throws Exception {
|
||||
mockMvc.perform(post("/api/auth/refresh")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Refresh token endpoint should not require authentication")
|
||||
void testRefreshTokenIsPublic() throws Exception {
|
||||
RefreshTokenRequest request = new RefreshTokenRequest(validRefreshToken);
|
||||
|
||||
// Endpoint is public (permitAll) - the 401 comes from the unimplemented
|
||||
// refreshSession(), not from missing JWT authentication
|
||||
mockMvc.perform(post("/api/auth/refresh")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== JWT TOKEN VALIDATION TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("JWT token should contain valid payload information")
|
||||
void testJWTTokenContainsValidPayload() throws Exception {
|
||||
LoginRequest request = new LoginRequest(validUsername, validPassword);
|
||||
|
||||
MvcResult result = mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
String responseBody = result.getResponse().getContentAsString();
|
||||
LoginResponse response = objectMapper.readValue(responseBody, LoginResponse.class);
|
||||
String accessToken = response.accessToken();
|
||||
|
||||
// Verify token structure (contains 3 parts separated by dots)
|
||||
String[] parts = accessToken.split("\\.");
|
||||
assertThat(parts).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Token expiration time should match configured expiration")
|
||||
void testTokenExpirationTime() throws Exception {
|
||||
LoginRequest request = new LoginRequest(validUsername, validPassword);
|
||||
|
||||
MvcResult result = mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
String responseBody = result.getResponse().getContentAsString();
|
||||
LoginResponse response = objectMapper.readValue(responseBody, LoginResponse.class);
|
||||
|
||||
// Verify expiration time is approximately correct (allowing 5 second margin)
|
||||
long expectedExpiration = jwtExpiration / 1000;
|
||||
long actualExpiration = response.expiresIn();
|
||||
assertThat(actualExpiration).isBetween(expectedExpiration - 5, expectedExpiration + 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Multiple logins should return different tokens")
|
||||
void testMultipleLoginsReturnDifferentTokens() throws Exception {
|
||||
LoginRequest request = new LoginRequest(validUsername, validPassword);
|
||||
|
||||
// First login
|
||||
MvcResult result1 = mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
String responseBody1 = result1.getResponse().getContentAsString();
|
||||
LoginResponse response1 = objectMapper.readValue(responseBody1, LoginResponse.class);
|
||||
|
||||
// Wait to ensure different iat claim (JWT timestamps are second-precision)
|
||||
Thread.sleep(1100);
|
||||
|
||||
// Second login
|
||||
MvcResult result2 = mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
String responseBody2 = result2.getResponse().getContentAsString();
|
||||
LoginResponse response2 = objectMapper.readValue(responseBody2, LoginResponse.class);
|
||||
|
||||
// Tokens should be different
|
||||
assertThat(response1.accessToken()).isNotEqualTo(response2.accessToken());
|
||||
}
|
||||
|
||||
// ==================== HELPER METHODS ====================
|
||||
|
||||
/**
|
||||
* Generates a test refresh token for testing refresh endpoint.
|
||||
* In production, refresh tokens are managed by SessionManager.
|
||||
*/
|
||||
private String generateTestRefreshToken(String userId, String username) {
|
||||
long now = System.currentTimeMillis();
|
||||
javax.crypto.SecretKey key = io.jsonwebtoken.security.Keys.hmacShaKeyFor(
|
||||
jwtSecret.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
return Jwts.builder()
|
||||
.subject(userId)
|
||||
.claim("username", username)
|
||||
.claim("type", "refresh")
|
||||
.issuedAt(new Date(now))
|
||||
.expiration(new Date(now + 7200000)) // 2 hours
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,620 @@
|
|||
package de.effigenix.infrastructure.usermanagement.web;
|
||||
|
||||
import de.effigenix.domain.usermanagement.RoleName;
|
||||
import de.effigenix.domain.usermanagement.UserStatus;
|
||||
import de.effigenix.infrastructure.audit.AuditLogEntity;
|
||||
import de.effigenix.infrastructure.audit.AuditLogJpaRepository;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.RoleJpaRepository;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.UserJpaRepository;
|
||||
import de.effigenix.infrastructure.usermanagement.web.dto.CreateUserRequest;
|
||||
import de.effigenix.infrastructure.usermanagement.web.dto.LoginRequest;
|
||||
import de.effigenix.infrastructure.usermanagement.web.dto.LoginResponse;
|
||||
import de.effigenix.infrastructure.usermanagement.web.dto.UpdateUserRequest;
|
||||
import de.effigenix.application.usermanagement.AuditEvent;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* Integration tests for Security and Authorization.
|
||||
*
|
||||
* Tests:
|
||||
* - Authorization (ADMIN-only endpoints reject non-admin users)
|
||||
* - Branch-based filtering
|
||||
* - Missing/expired JWT returns 401
|
||||
* - Audit logging for critical operations
|
||||
* - Verify audit logs contain actor, timestamp, IP address
|
||||
*
|
||||
* Uses:
|
||||
* - @SpringBootTest for full application context
|
||||
* - @AutoConfigureMockMvc for MockMvc HTTP testing
|
||||
* - @Transactional for test isolation
|
||||
* - H2 in-memory database (configured in application-test.yml)
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
@DisplayName("Security and Authorization Integration Tests")
|
||||
class SecurityIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private UserJpaRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private RoleJpaRepository roleRepository;
|
||||
|
||||
@Autowired
|
||||
private AuditLogJpaRepository auditLogRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String jwtSecret;
|
||||
|
||||
@Value("${jwt.expiration}")
|
||||
private long jwtExpiration;
|
||||
|
||||
private String adminToken;
|
||||
private String regularUserToken;
|
||||
private String expiredToken;
|
||||
private String malformedToken;
|
||||
|
||||
private String adminUserId;
|
||||
private String regularUserId;
|
||||
|
||||
private RoleEntity adminRole;
|
||||
private RoleEntity userRole;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// Create roles
|
||||
adminRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(),
|
||||
RoleName.ADMIN,
|
||||
Set.of(), // Empty permissions for testing
|
||||
"Admin role"
|
||||
);
|
||||
roleRepository.save(adminRole);
|
||||
|
||||
userRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(),
|
||||
RoleName.PRODUCTION_WORKER,
|
||||
Set.of(), // Empty permissions for testing
|
||||
"Production worker role"
|
||||
);
|
||||
roleRepository.save(userRole);
|
||||
|
||||
// Create admin user
|
||||
adminUserId = UUID.randomUUID().toString();
|
||||
UserEntity adminUser = new UserEntity(
|
||||
adminUserId,
|
||||
"security.admin",
|
||||
"admin@security.test",
|
||||
passwordEncoder.encode("AdminPass123"),
|
||||
Set.of(adminRole),
|
||||
"BRANCH-ADMIN",
|
||||
UserStatus.ACTIVE,
|
||||
LocalDateTime.now(),
|
||||
null
|
||||
);
|
||||
userRepository.save(adminUser);
|
||||
|
||||
// Create regular user
|
||||
regularUserId = UUID.randomUUID().toString();
|
||||
UserEntity regularUser = new UserEntity(
|
||||
regularUserId,
|
||||
"security.user",
|
||||
"user@security.test",
|
||||
passwordEncoder.encode("UserPass123"),
|
||||
Set.of(userRole),
|
||||
"BRANCH-USER",
|
||||
UserStatus.ACTIVE,
|
||||
LocalDateTime.now(),
|
||||
null
|
||||
);
|
||||
userRepository.save(regularUser);
|
||||
|
||||
// Generate tokens
|
||||
adminToken = generateTestJWT(adminUserId, "security.admin", true);
|
||||
regularUserToken = generateTestJWT(regularUserId, "security.user", false);
|
||||
expiredToken = generateExpiredToken(adminUserId, "security.admin");
|
||||
malformedToken = "not.a.valid.jwt.token";
|
||||
}
|
||||
|
||||
// ==================== AUTHORIZATION TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("ADMIN-only endpoint with admin token should return 201")
|
||||
void testAdminEndpointWithAdminRole() throws Exception {
|
||||
CreateUserRequest request = new CreateUserRequest(
|
||||
"test.new.user",
|
||||
"test.new@example.com",
|
||||
"SecurePass123!",
|
||||
Set.of(RoleName.PRODUCTION_WORKER),
|
||||
"BRANCH-001"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ADMIN-only endpoint with non-admin token should return 403 Forbidden")
|
||||
void testAdminEndpointWithoutAdminRole() throws Exception {
|
||||
CreateUserRequest request = new CreateUserRequest(
|
||||
"test.new.user",
|
||||
"test.new@example.com",
|
||||
"SecurePass123!",
|
||||
Set.of(RoleName.PRODUCTION_WORKER),
|
||||
"BRANCH-001"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.header("Authorization", "Bearer " + regularUserToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isForbidden())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Lock user endpoint with non-admin token should return 403 Forbidden")
|
||||
void testLockUserWithoutAdminRole() throws Exception {
|
||||
mockMvc.perform(post("/api/users/{id}/lock", regularUserId)
|
||||
.header("Authorization", "Bearer " + regularUserToken))
|
||||
.andExpect(status().isForbidden())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Unlock user endpoint with non-admin token should return 403 Forbidden")
|
||||
void testUnlockUserWithoutAdminRole() throws Exception {
|
||||
mockMvc.perform(post("/api/users/{id}/unlock", regularUserId)
|
||||
.header("Authorization", "Bearer " + regularUserToken))
|
||||
.andExpect(status().isForbidden())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Assign role endpoint with non-admin token should return 403 Forbidden")
|
||||
void testAssignRoleWithoutAdminRole() throws Exception {
|
||||
mockMvc.perform(post("/api/users/{id}/roles", regularUserId)
|
||||
.header("Authorization", "Bearer " + regularUserToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"roleName\":\"ADMIN\"}"))
|
||||
.andExpect(status().isForbidden())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Remove role endpoint with non-admin token should return 403 Forbidden")
|
||||
void testRemoveRoleWithoutAdminRole() throws Exception {
|
||||
mockMvc.perform(delete("/api/users/{id}/roles/{roleName}", regularUserId, "PRODUCTION_WORKER")
|
||||
.header("Authorization", "Bearer " + regularUserToken))
|
||||
.andExpect(status().isForbidden())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== AUTHENTICATED ENDPOINT TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Authenticated endpoint without token should return 401 Unauthorized")
|
||||
void testAuthenticatedEndpointWithoutToken() throws Exception {
|
||||
mockMvc.perform(get("/api/users"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Authenticated endpoint with invalid token should return 401 Unauthorized")
|
||||
void testAuthenticatedEndpointWithInvalidToken() throws Exception {
|
||||
mockMvc.perform(get("/api/users")
|
||||
.header("Authorization", "Bearer " + malformedToken))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Authenticated endpoint with expired token should return 401 Unauthorized")
|
||||
void testAuthenticatedEndpointWithExpiredToken() throws Exception {
|
||||
mockMvc.perform(get("/api/users")
|
||||
.header("Authorization", "Bearer " + expiredToken))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Authenticated endpoint with malformed Authorization header should return 401")
|
||||
void testAuthenticatedEndpointWithMalformedHeader() throws Exception {
|
||||
mockMvc.perform(get("/api/users")
|
||||
.header("Authorization", "InvalidHeaderFormat"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Authenticated endpoint with valid token should return success")
|
||||
void testAuthenticatedEndpointWithValidToken() throws Exception {
|
||||
mockMvc.perform(get("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== PUBLIC ENDPOINT TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Login endpoint without authentication should return 401 or 200 depending on credentials")
|
||||
void testLoginEndpointIsPublic() throws Exception {
|
||||
LoginRequest request = new LoginRequest("security.admin", "AdminPass123");
|
||||
|
||||
mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Refresh token endpoint without authentication should be public")
|
||||
void testRefreshEndpointIsPublic() throws Exception {
|
||||
// Generate a valid refresh token
|
||||
String refreshToken = generateTestRefreshToken(adminUserId, "security.admin");
|
||||
|
||||
// Endpoint is public (permitAll) - the 401 comes from unimplemented
|
||||
// refreshSession(), not from missing JWT authentication
|
||||
mockMvc.perform(post("/api/auth/refresh")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"refreshToken\":\"" + refreshToken + "\"}"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== BRANCH-BASED FILTERING TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Users should see data filtered by their branch (if filtering is implemented)")
|
||||
void testBranchBasedDataVisibility() throws Exception {
|
||||
// Create user in different branch
|
||||
String otherBranchUserId = UUID.randomUUID().toString();
|
||||
UserEntity otherBranchUser = new UserEntity(
|
||||
otherBranchUserId,
|
||||
"other.branch.user",
|
||||
"other@branch.test",
|
||||
passwordEncoder.encode("OtherPass123"),
|
||||
Set.of(userRole),
|
||||
"BRANCH-OTHER",
|
||||
UserStatus.ACTIVE,
|
||||
LocalDateTime.now(),
|
||||
null
|
||||
);
|
||||
userRepository.save(otherBranchUser);
|
||||
|
||||
// Regular user token for BRANCH-USER
|
||||
mockMvc.perform(get("/api/users")
|
||||
.header("Authorization", "Bearer " + regularUserToken))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
// Both branches should be visible in list (if no filtering is implemented)
|
||||
// This test documents the current behavior
|
||||
}
|
||||
|
||||
// ==================== AUDIT LOGGING TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Create user operation should create audit log entry")
|
||||
void testCreateUserAuditLogging() throws Exception {
|
||||
// Clear existing audit logs
|
||||
auditLogRepository.deleteAll();
|
||||
|
||||
CreateUserRequest request = new CreateUserRequest(
|
||||
"audit.test.user",
|
||||
"audit@test.example.com",
|
||||
"SecurePass123!",
|
||||
Set.of(RoleName.PRODUCTION_WORKER),
|
||||
"BRANCH-001"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
// Verify audit log was created
|
||||
List<AuditLogEntity> logs = auditLogRepository.findAll();
|
||||
assertThat(logs)
|
||||
.filteredOn(log -> log.getEvent() == AuditEvent.USER_CREATED)
|
||||
.isNotEmpty();
|
||||
|
||||
AuditLogEntity auditLog = logs.stream()
|
||||
.filter(log -> log.getEvent() == AuditEvent.USER_CREATED)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
assertThat(auditLog.getPerformedBy()).isEqualTo(adminUserId);
|
||||
assertThat(auditLog.getTimestamp()).isNotNull();
|
||||
assertThat(auditLog.getEvent()).isEqualTo(AuditEvent.USER_CREATED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Audit log should contain actor information")
|
||||
void testAuditLogContainsActor() throws Exception {
|
||||
auditLogRepository.deleteAll();
|
||||
|
||||
CreateUserRequest request = new CreateUserRequest(
|
||||
"audit.actor.test",
|
||||
"audit.actor@test.example.com",
|
||||
"SecurePass123!",
|
||||
Set.of(RoleName.PRODUCTION_WORKER),
|
||||
"BRANCH-001"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
List<AuditLogEntity> logs = auditLogRepository.findAll();
|
||||
AuditLogEntity auditLog = logs.stream()
|
||||
.filter(log -> log.getEvent() == AuditEvent.USER_CREATED)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
// Verify actor is the admin user
|
||||
assertThat(auditLog.getPerformedBy()).isEqualTo(adminUserId);
|
||||
assertThat(auditLog.getPerformedBy()).isNotBlank();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Audit log should contain timestamp")
|
||||
void testAuditLogContainsTimestamp() throws Exception {
|
||||
auditLogRepository.deleteAll();
|
||||
|
||||
CreateUserRequest request = new CreateUserRequest(
|
||||
"audit.timestamp.test",
|
||||
"audit.timestamp@test.example.com",
|
||||
"SecurePass123!",
|
||||
Set.of(RoleName.PRODUCTION_WORKER),
|
||||
"BRANCH-001"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
// Wait for async audit log to be persisted
|
||||
Thread.sleep(500);
|
||||
|
||||
List<AuditLogEntity> logs = auditLogRepository.findAll();
|
||||
AuditLogEntity auditLog = logs.stream()
|
||||
.filter(log -> log.getEvent() == AuditEvent.USER_CREATED)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
// Verify timestamp is present and recent (within last minute)
|
||||
assertThat(auditLog.getTimestamp()).isNotNull();
|
||||
assertThat(auditLog.getTimestamp()).isAfter(LocalDateTime.now().minusMinutes(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Lock user operation should create audit log entry")
|
||||
void testLockUserAuditLogging() throws Exception {
|
||||
auditLogRepository.deleteAll();
|
||||
|
||||
mockMvc.perform(post("/api/users/{id}/lock", regularUserId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
// Verify audit log was created
|
||||
List<AuditLogEntity> logs = auditLogRepository.findAll();
|
||||
assertThat(logs)
|
||||
.filteredOn(log -> log.getEvent() == AuditEvent.USER_LOCKED)
|
||||
.isNotEmpty();
|
||||
|
||||
AuditLogEntity auditLog = logs.stream()
|
||||
.filter(log -> log.getEvent() == AuditEvent.USER_LOCKED)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
assertThat(auditLog.getPerformedBy()).isEqualTo(adminUserId);
|
||||
assertThat(auditLog.getEntityId()).isEqualTo(regularUserId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Unlock user operation should create audit log entry")
|
||||
void testUnlockUserAuditLogging() throws Exception {
|
||||
auditLogRepository.deleteAll();
|
||||
|
||||
// First lock the user
|
||||
UserEntity user = userRepository.findById(regularUserId).orElseThrow();
|
||||
user.setStatus(UserStatus.LOCKED);
|
||||
userRepository.save(user);
|
||||
|
||||
// Then unlock
|
||||
mockMvc.perform(post("/api/users/{id}/unlock", regularUserId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
// Verify audit log was created
|
||||
List<AuditLogEntity> logs = auditLogRepository.findAll();
|
||||
assertThat(logs)
|
||||
.filteredOn(log -> log.getEvent() == AuditEvent.USER_UNLOCKED)
|
||||
.isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Update user operation should create audit log entry")
|
||||
void testUpdateUserAuditLogging() throws Exception {
|
||||
auditLogRepository.deleteAll();
|
||||
|
||||
UpdateUserRequest request = new UpdateUserRequest(
|
||||
"updated.audit@example.com",
|
||||
"BRANCH-UPDATED"
|
||||
);
|
||||
|
||||
mockMvc.perform(put("/api/users/{id}", regularUserId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
// Verify audit log was created
|
||||
List<AuditLogEntity> logs = auditLogRepository.findAll();
|
||||
assertThat(logs)
|
||||
.filteredOn(log -> log.getEvent() == AuditEvent.USER_UPDATED)
|
||||
.isNotEmpty();
|
||||
|
||||
AuditLogEntity auditLog = logs.stream()
|
||||
.filter(log -> log.getEvent() == AuditEvent.USER_UPDATED)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
assertThat(auditLog.getPerformedBy()).isEqualTo(adminUserId);
|
||||
assertThat(auditLog.getEntityId()).isEqualTo(regularUserId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Login success should create audit log entry")
|
||||
void testLoginSuccessAuditLogging() throws Exception {
|
||||
auditLogRepository.deleteAll();
|
||||
|
||||
LoginRequest request = new LoginRequest("security.admin", "AdminPass123");
|
||||
|
||||
mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
// Verify audit log was created
|
||||
List<AuditLogEntity> logs = auditLogRepository.findAll();
|
||||
assertThat(logs)
|
||||
.filteredOn(log -> log.getEvent() == AuditEvent.LOGIN_SUCCESS)
|
||||
.isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Login failure should create audit log entry")
|
||||
void testLoginFailureAuditLogging() throws Exception {
|
||||
auditLogRepository.deleteAll();
|
||||
|
||||
LoginRequest request = new LoginRequest("security.admin", "WrongPassword123");
|
||||
|
||||
mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
|
||||
// Verify audit log was created
|
||||
List<AuditLogEntity> logs = auditLogRepository.findAll();
|
||||
assertThat(logs)
|
||||
.filteredOn(log -> log.getEvent() == AuditEvent.LOGIN_FAILED)
|
||||
.isNotEmpty();
|
||||
}
|
||||
|
||||
// ==================== HELPER METHODS ====================
|
||||
|
||||
/**
|
||||
* Generates a test JWT token with admin permissions.
|
||||
*/
|
||||
private String generateTestJWT(String userId, String username, boolean isAdmin) {
|
||||
long now = System.currentTimeMillis();
|
||||
String permissions = isAdmin
|
||||
? "USER_READ,USER_WRITE,USER_DELETE,USER_LOCK,USER_UNLOCK,ROLE_READ,ROLE_WRITE,ROLE_ASSIGN,ROLE_REMOVE"
|
||||
: "USER_READ";
|
||||
javax.crypto.SecretKey key = io.jsonwebtoken.security.Keys.hmacShaKeyFor(
|
||||
jwtSecret.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
|
||||
return Jwts.builder()
|
||||
.subject(userId)
|
||||
.claim("username", username)
|
||||
.claim("permissions", permissions)
|
||||
.issuedAt(new Date(now))
|
||||
.expiration(new Date(now + jwtExpiration))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an expired JWT token for testing expired token handling.
|
||||
*/
|
||||
private String generateExpiredToken(String userId, String username) {
|
||||
long now = System.currentTimeMillis();
|
||||
javax.crypto.SecretKey key = io.jsonwebtoken.security.Keys.hmacShaKeyFor(
|
||||
jwtSecret.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
return Jwts.builder()
|
||||
.subject(userId)
|
||||
.claim("username", username)
|
||||
.claim("permissions", "")
|
||||
.issuedAt(new Date(now - 10000))
|
||||
.expiration(new Date(now - 5000)) // Expired 5 seconds ago
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a test refresh token.
|
||||
*/
|
||||
private String generateTestRefreshToken(String userId, String username) {
|
||||
long now = System.currentTimeMillis();
|
||||
javax.crypto.SecretKey key = io.jsonwebtoken.security.Keys.hmacShaKeyFor(
|
||||
jwtSecret.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
return Jwts.builder()
|
||||
.subject(userId)
|
||||
.claim("username", username)
|
||||
.claim("type", "refresh")
|
||||
.issuedAt(new Date(now))
|
||||
.expiration(new Date(now + 7200000))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,619 @@
|
|||
package de.effigenix.infrastructure.usermanagement.web;
|
||||
|
||||
import de.effigenix.application.usermanagement.dto.UserDTO;
|
||||
import de.effigenix.domain.usermanagement.RoleName;
|
||||
import de.effigenix.domain.usermanagement.UserStatus;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.RoleJpaRepository;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.repository.UserJpaRepository;
|
||||
import de.effigenix.infrastructure.usermanagement.web.dto.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* Integration tests for User Management Controller.
|
||||
*
|
||||
* Tests user management operations:
|
||||
* - Create user (success, validation errors, duplicates)
|
||||
* - List users
|
||||
* - Get user by ID
|
||||
* - Update user
|
||||
* - Lock/unlock user
|
||||
* - Change password
|
||||
*
|
||||
* Uses:
|
||||
* - @SpringBootTest for full application context
|
||||
* - @AutoConfigureMockMvc for MockMvc HTTP testing
|
||||
* - @Transactional for test isolation
|
||||
* - H2 in-memory database (configured in application-test.yml)
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
@DisplayName("User Controller Integration Tests")
|
||||
class UserControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private UserJpaRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private RoleJpaRepository roleRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String jwtSecret;
|
||||
|
||||
@Value("${jwt.expiration}")
|
||||
private long jwtExpiration;
|
||||
|
||||
private String adminToken;
|
||||
private String regularUserToken;
|
||||
private String adminUserId;
|
||||
private String regularUserId;
|
||||
|
||||
private RoleEntity adminRole;
|
||||
private RoleEntity userRole;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// Create roles
|
||||
adminRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(),
|
||||
RoleName.ADMIN,
|
||||
Set.of(), // Empty permissions for testing
|
||||
"Admin role"
|
||||
);
|
||||
roleRepository.save(adminRole);
|
||||
|
||||
userRole = new RoleEntity(
|
||||
UUID.randomUUID().toString(),
|
||||
RoleName.PRODUCTION_WORKER,
|
||||
Set.of(), // Empty permissions for testing
|
||||
"Production worker role"
|
||||
);
|
||||
roleRepository.save(userRole);
|
||||
|
||||
// Create admin user
|
||||
adminUserId = UUID.randomUUID().toString();
|
||||
UserEntity adminUser = new UserEntity(
|
||||
adminUserId,
|
||||
"admin.user",
|
||||
"admin@example.com",
|
||||
passwordEncoder.encode("AdminPass123"),
|
||||
Set.of(adminRole),
|
||||
"BRANCH-001",
|
||||
UserStatus.ACTIVE,
|
||||
LocalDateTime.now(),
|
||||
null
|
||||
);
|
||||
userRepository.save(adminUser);
|
||||
|
||||
// Create regular user
|
||||
regularUserId = UUID.randomUUID().toString();
|
||||
UserEntity regularUser = new UserEntity(
|
||||
regularUserId,
|
||||
"regular.user",
|
||||
"regular@example.com",
|
||||
passwordEncoder.encode("RegularPass123"),
|
||||
Set.of(userRole),
|
||||
"BRANCH-001",
|
||||
UserStatus.ACTIVE,
|
||||
LocalDateTime.now(),
|
||||
null
|
||||
);
|
||||
userRepository.save(regularUser);
|
||||
|
||||
// Generate JWT tokens
|
||||
adminToken = generateTestJWT(adminUserId, "admin.user", true);
|
||||
regularUserToken = generateTestJWT(regularUserId, "regular.user", false);
|
||||
}
|
||||
|
||||
// ==================== CREATE USER TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Create user with valid data should return 201 Created")
|
||||
void testCreateUserWithValidData() throws Exception {
|
||||
CreateUserRequest request = new CreateUserRequest(
|
||||
"new.user",
|
||||
"new.user@example.com",
|
||||
"SecurePass123!",
|
||||
Set.of(RoleName.PRODUCTION_WORKER),
|
||||
"BRANCH-001"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").isNotEmpty())
|
||||
.andExpect(jsonPath("$.username").value("new.user"))
|
||||
.andExpect(jsonPath("$.email").value("new.user@example.com"))
|
||||
.andExpect(jsonPath("$.status").value("ACTIVE"))
|
||||
.andExpect(jsonPath("$.createdAt").isNotEmpty())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Create user with duplicate username should return 409 Conflict")
|
||||
void testCreateUserWithDuplicateUsername() throws Exception {
|
||||
CreateUserRequest request = new CreateUserRequest(
|
||||
"admin.user", // Already exists
|
||||
"duplicate@example.com",
|
||||
"SecurePass123!",
|
||||
Set.of(RoleName.PRODUCTION_WORKER),
|
||||
"BRANCH-001"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isConflict())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Create user with duplicate email should return 409 Conflict")
|
||||
void testCreateUserWithDuplicateEmail() throws Exception {
|
||||
CreateUserRequest request = new CreateUserRequest(
|
||||
"different.user",
|
||||
"admin@example.com", // Already exists
|
||||
"SecurePass123!",
|
||||
Set.of(RoleName.PRODUCTION_WORKER),
|
||||
"BRANCH-001"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isConflict())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Create user with invalid email should return 400 Bad Request")
|
||||
void testCreateUserWithInvalidEmail() throws Exception {
|
||||
CreateUserRequest request = new CreateUserRequest(
|
||||
"new.user",
|
||||
"invalid-email",
|
||||
"SecurePass123!",
|
||||
Set.of(RoleName.PRODUCTION_WORKER),
|
||||
"BRANCH-001"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Create user with short password should return 400 Bad Request")
|
||||
void testCreateUserWithShortPassword() throws Exception {
|
||||
CreateUserRequest request = new CreateUserRequest(
|
||||
"new.user",
|
||||
"new.user@example.com",
|
||||
"Pass123", // Less than 8 characters
|
||||
Set.of(RoleName.PRODUCTION_WORKER),
|
||||
"BRANCH-001"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Create user with short username should return 400 Bad Request")
|
||||
void testCreateUserWithShortUsername() throws Exception {
|
||||
CreateUserRequest request = new CreateUserRequest(
|
||||
"ab", // Less than 3 characters
|
||||
"new.user@example.com",
|
||||
"SecurePass123!",
|
||||
Set.of(RoleName.PRODUCTION_WORKER),
|
||||
"BRANCH-001"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Create user with missing required fields should return 400 Bad Request")
|
||||
void testCreateUserWithMissingFields() throws Exception {
|
||||
mockMvc.perform(post("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Create user without authentication should return 401 Unauthorized")
|
||||
void testCreateUserWithoutAuthentication() throws Exception {
|
||||
CreateUserRequest request = new CreateUserRequest(
|
||||
"new.user",
|
||||
"new.user@example.com",
|
||||
"SecurePass123!",
|
||||
Set.of(RoleName.PRODUCTION_WORKER),
|
||||
"BRANCH-001"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/users")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== LIST USERS TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("List users should return 200 with array of users")
|
||||
void testListUsers() throws Exception {
|
||||
mockMvc.perform(get("/api/users")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(2))))
|
||||
.andExpect(jsonPath("$[0].id").isNotEmpty())
|
||||
.andExpect(jsonPath("$[0].username").isNotEmpty())
|
||||
.andExpect(jsonPath("$[0].email").isNotEmpty())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("List users without authentication should return 401 Unauthorized")
|
||||
void testListUsersWithoutAuthentication() throws Exception {
|
||||
mockMvc.perform(get("/api/users"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("List users with invalid token should return 401 Unauthorized")
|
||||
void testListUsersWithInvalidToken() throws Exception {
|
||||
mockMvc.perform(get("/api/users")
|
||||
.header("Authorization", "Bearer invalid.token.here"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== GET USER BY ID TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Get existing user by ID should return 200 with user data")
|
||||
void testGetExistingUserById() throws Exception {
|
||||
mockMvc.perform(get("/api/users/{id}", adminUserId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(adminUserId))
|
||||
.andExpect(jsonPath("$.username").value("admin.user"))
|
||||
.andExpect(jsonPath("$.email").value("admin@example.com"))
|
||||
.andExpect(jsonPath("$.status").value("ACTIVE"))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get non-existent user by ID should return 404 Not Found")
|
||||
void testGetNonExistentUserById() throws Exception {
|
||||
String nonExistentId = UUID.randomUUID().toString();
|
||||
|
||||
mockMvc.perform(get("/api/users/{id}", nonExistentId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNotFound())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get user by ID without authentication should return 401 Unauthorized")
|
||||
void testGetUserByIdWithoutAuthentication() throws Exception {
|
||||
mockMvc.perform(get("/api/users/{id}", adminUserId))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== UPDATE USER TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Update user with valid data should return 200 with updated user")
|
||||
void testUpdateUserWithValidData() throws Exception {
|
||||
UpdateUserRequest request = new UpdateUserRequest(
|
||||
"updated@example.com",
|
||||
"BRANCH-002"
|
||||
);
|
||||
|
||||
mockMvc.perform(put("/api/users/{id}", regularUserId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(regularUserId))
|
||||
.andExpect(jsonPath("$.email").value("updated@example.com"))
|
||||
.andExpect(jsonPath("$.branchId").value("BRANCH-002"))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Update user with duplicate email should return 409 Conflict")
|
||||
void testUpdateUserWithDuplicateEmail() throws Exception {
|
||||
UpdateUserRequest request = new UpdateUserRequest(
|
||||
"admin@example.com", // Already exists
|
||||
"BRANCH-002"
|
||||
);
|
||||
|
||||
mockMvc.perform(put("/api/users/{id}", regularUserId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isConflict())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Update non-existent user should return 404 Not Found")
|
||||
void testUpdateNonExistentUser() throws Exception {
|
||||
String nonExistentId = UUID.randomUUID().toString();
|
||||
UpdateUserRequest request = new UpdateUserRequest(
|
||||
"new@example.com",
|
||||
"BRANCH-002"
|
||||
);
|
||||
|
||||
mockMvc.perform(put("/api/users/{id}", nonExistentId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isNotFound())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Update user with invalid email should return 400 Bad Request")
|
||||
void testUpdateUserWithInvalidEmail() throws Exception {
|
||||
UpdateUserRequest request = new UpdateUserRequest(
|
||||
"invalid-email",
|
||||
"BRANCH-002"
|
||||
);
|
||||
|
||||
mockMvc.perform(put("/api/users/{id}", regularUserId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== LOCK USER TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Lock user should return 200 with LOCKED status")
|
||||
void testLockUser() throws Exception {
|
||||
mockMvc.perform(post("/api/users/{id}/lock", regularUserId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(regularUserId))
|
||||
.andExpect(jsonPath("$.status").value("LOCKED"))
|
||||
.andReturn();
|
||||
|
||||
// Verify user is actually locked in database
|
||||
UserEntity lockedUser = userRepository.findById(regularUserId).orElseThrow();
|
||||
assertThat(lockedUser.getStatus()).isEqualTo(UserStatus.LOCKED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Lock non-existent user should return 404 Not Found")
|
||||
void testLockNonExistentUser() throws Exception {
|
||||
String nonExistentId = UUID.randomUUID().toString();
|
||||
|
||||
mockMvc.perform(post("/api/users/{id}/lock", nonExistentId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNotFound())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Lock user without admin permission should return 403 Forbidden")
|
||||
void testLockUserWithoutAdminPermission() throws Exception {
|
||||
mockMvc.perform(post("/api/users/{id}/lock", regularUserId)
|
||||
.header("Authorization", "Bearer " + regularUserToken))
|
||||
.andExpect(status().isForbidden())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== UNLOCK USER TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Unlock locked user should return 200 with ACTIVE status")
|
||||
void testUnlockUser() throws Exception {
|
||||
// First lock the user
|
||||
UserEntity user = userRepository.findById(regularUserId).orElseThrow();
|
||||
user.setStatus(UserStatus.LOCKED);
|
||||
userRepository.save(user);
|
||||
|
||||
// Now unlock
|
||||
mockMvc.perform(post("/api/users/{id}/unlock", regularUserId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("ACTIVE"))
|
||||
.andReturn();
|
||||
|
||||
// Verify user is actually unlocked in database
|
||||
UserEntity unlockedUser = userRepository.findById(regularUserId).orElseThrow();
|
||||
assertThat(unlockedUser.getStatus()).isEqualTo(UserStatus.ACTIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Unlock non-existent user should return 404 Not Found")
|
||||
void testUnlockNonExistentUser() throws Exception {
|
||||
String nonExistentId = UUID.randomUUID().toString();
|
||||
|
||||
mockMvc.perform(post("/api/users/{id}/unlock", nonExistentId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNotFound())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Unlock user without admin permission should return 403 Forbidden")
|
||||
void testUnlockUserWithoutAdminPermission() throws Exception {
|
||||
mockMvc.perform(post("/api/users/{id}/unlock", regularUserId)
|
||||
.header("Authorization", "Bearer " + regularUserToken))
|
||||
.andExpect(status().isForbidden())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== CHANGE PASSWORD TESTS ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("Change password with correct current password should return 204 No Content")
|
||||
void testChangePasswordWithValidCurrentPassword() throws Exception {
|
||||
ChangePasswordRequest request = new ChangePasswordRequest(
|
||||
"RegularPass123", // Current password
|
||||
"NewSecurePass456!"
|
||||
);
|
||||
|
||||
mockMvc.perform(put("/api/users/{id}/password", regularUserId)
|
||||
.header("Authorization", "Bearer " + regularUserToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isNoContent())
|
||||
.andReturn();
|
||||
|
||||
// Verify password was actually changed
|
||||
UserEntity updatedUser = userRepository.findById(regularUserId).orElseThrow();
|
||||
assertThat(passwordEncoder.matches("NewSecurePass456!", updatedUser.getPasswordHash())).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Change password with incorrect current password should return 401 Unauthorized")
|
||||
void testChangePasswordWithInvalidCurrentPassword() throws Exception {
|
||||
ChangePasswordRequest request = new ChangePasswordRequest(
|
||||
"WrongPassword123", // Wrong current password
|
||||
"NewSecurePass456!"
|
||||
);
|
||||
|
||||
mockMvc.perform(put("/api/users/{id}/password", regularUserId)
|
||||
.header("Authorization", "Bearer " + regularUserToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Change password with new password too short should return 400 Bad Request")
|
||||
void testChangePasswordWithShortNewPassword() throws Exception {
|
||||
ChangePasswordRequest request = new ChangePasswordRequest(
|
||||
"RegularPass123",
|
||||
"Short1" // Less than 8 characters
|
||||
);
|
||||
|
||||
mockMvc.perform(put("/api/users/{id}/password", regularUserId)
|
||||
.header("Authorization", "Bearer " + regularUserToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Change password for non-existent user should return 404 Not Found")
|
||||
void testChangePasswordForNonExistentUser() throws Exception {
|
||||
String nonExistentId = UUID.randomUUID().toString();
|
||||
ChangePasswordRequest request = new ChangePasswordRequest(
|
||||
"SomePassword123",
|
||||
"NewSecurePass456!"
|
||||
);
|
||||
|
||||
mockMvc.perform(put("/api/users/{id}/password", nonExistentId)
|
||||
.header("Authorization", "Bearer " + regularUserToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isNotFound())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Change password without authentication should return 401 Unauthorized")
|
||||
void testChangePasswordWithoutAuthentication() throws Exception {
|
||||
ChangePasswordRequest request = new ChangePasswordRequest(
|
||||
"RegularPass123",
|
||||
"NewSecurePass456!"
|
||||
);
|
||||
|
||||
mockMvc.perform(put("/api/users/{id}/password", regularUserId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ==================== HELPER METHODS ====================
|
||||
|
||||
/**
|
||||
* Generates a test JWT token with USER_MANAGEMENT permission for admin.
|
||||
*/
|
||||
private String generateTestJWT(String userId, String username, boolean isAdmin) {
|
||||
long now = System.currentTimeMillis();
|
||||
javax.crypto.SecretKey key = io.jsonwebtoken.security.Keys.hmacShaKeyFor(
|
||||
jwtSecret.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
String permissions = isAdmin
|
||||
? "USER_READ,USER_WRITE,USER_DELETE,USER_LOCK,USER_UNLOCK,ROLE_READ,ROLE_WRITE,ROLE_ASSIGN,ROLE_REMOVE"
|
||||
: "USER_READ";
|
||||
return Jwts.builder()
|
||||
.subject(userId)
|
||||
.claim("username", username)
|
||||
.claim("permissions", permissions)
|
||||
.issuedAt(new Date(now))
|
||||
.expiration(new Date(now + jwtExpiration))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
}
|
||||
25
backend/src/test/resources/application-test.yml
Normal file
25
backend/src/test/resources/application-test.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.H2Dialect
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
show-sql: true
|
||||
|
||||
liquibase:
|
||||
enabled: false # Use Hibernate for test schema
|
||||
|
||||
jwt:
|
||||
secret: TestSecretKeyForUnitTestsMin256BitsLongForHS256AlgorithmSecurity
|
||||
expiration: 3600000 # 1 hour for tests
|
||||
refresh-expiration: 7200000 # 2 hours for tests
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
de.effigenix: DEBUG
|
||||
Loading…
Add table
Add a link
Reference in a new issue