test(sprint-2): add integration tests for Auth + Compliance controllers

- AuthControllerIntegrationTest: 7 tests (login, refresh, error cases)
- ComplianceControllerIntegrationTest: 5 tests (quota, auth, 404)
- Fix Boot 4.0 @EntityScan relocation (boot.persistence.autoconfigure)
- Fix BCrypt 72-byte limit for refresh tokens (use SHA-256 instead)
- Configure H2 test DB with NON_KEYWORDS for reserved words (month/year)
This commit is contained in:
Patrick Plate
2026-06-11 13:30:07 +02:00
parent 2ede872d11
commit a1ddec37da
5 changed files with 408 additions and 8 deletions
@@ -2,6 +2,7 @@ package de.cannamanage.api;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.persistence.autoconfigure.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
@@ -11,10 +12,11 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
* Multi-module scanning:
* - scanBasePackages: component scanning (controllers, services)
* - EnableJpaRepositories: Spring Data JPA repository interfaces
* - Entity scanning configured via spring.jpa properties
* - EntityScan: JPA entity detection across modules (Boot 4.0 relocated package)
*/
@SpringBootApplication(scanBasePackages = "de.cannamanage")
@EnableJpaRepositories(basePackages = "de.cannamanage.service.repository")
@EntityScan(basePackages = "de.cannamanage.domain.entity")
public class CannaManageApplication {
public static void main(String[] args) {
@@ -12,7 +12,11 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HexFormat;
import java.util.UUID;
/**
@@ -48,8 +52,8 @@ public class AuthService {
user.getId(), user.getTenantId(), roleName, user.getEmail());
String refreshToken = jwtService.generateRefreshToken(user.getId(), user.getTenantId());
// Store hashed refresh token for revocation
user.setRefreshTokenHash(passwordEncoder.encode(refreshToken));
// Store SHA-256 hashed refresh token for revocation (BCrypt can't handle >72 bytes)
user.setRefreshTokenHash(sha256(refreshToken));
user.setLastLogin(Instant.now());
userRepository.save(user);
@@ -76,7 +80,7 @@ public class AuthService {
// Verify the refresh token matches stored hash (revocation check)
if (user.getRefreshTokenHash() == null ||
!passwordEncoder.matches(token, user.getRefreshTokenHash())) {
!sha256(token).equals(user.getRefreshTokenHash())) {
throw new AuthenticationException("Refresh token has been revoked");
}
@@ -86,12 +90,27 @@ public class AuthService {
user.getId(), user.getTenantId(), roleName, user.getEmail());
String newRefreshToken = jwtService.generateRefreshToken(user.getId(), user.getTenantId());
user.setRefreshTokenHash(passwordEncoder.encode(newRefreshToken));
user.setRefreshTokenHash(sha256(newRefreshToken));
userRepository.save(user);
return new LoginResponse(newAccessToken, newRefreshToken, 3600L, roleName);
}
/**
* SHA-256 hash for refresh token storage.
* JWTs exceed BCrypt's 72-byte limit (enforced in Spring Security 7+).
* SHA-256 is appropriate here: refresh tokens are already high-entropy random strings.
*/
private String sha256(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 not available", e);
}
}
/**
* Custom authentication exception — caught by GlobalExceptionHandler.
*/