1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 19:00:23 +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:
Sebastian Frick 2026-02-17 22:08:51 +01:00
parent ec9114aa0a
commit c2c48a03e8
141 changed files with 734 additions and 9 deletions

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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");
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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
);
}
}

View file

@ -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();
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}