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:
@@ -2,6 +2,7 @@ package de.cannamanage.api;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.persistence.autoconfigure.EntityScan;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,10 +12,11 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
|||||||
* Multi-module scanning:
|
* Multi-module scanning:
|
||||||
* - scanBasePackages: component scanning (controllers, services)
|
* - scanBasePackages: component scanning (controllers, services)
|
||||||
* - EnableJpaRepositories: Spring Data JPA repository interfaces
|
* - 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")
|
@SpringBootApplication(scanBasePackages = "de.cannamanage")
|
||||||
@EnableJpaRepositories(basePackages = "de.cannamanage.service.repository")
|
@EnableJpaRepositories(basePackages = "de.cannamanage.service.repository")
|
||||||
|
@EntityScan(basePackages = "de.cannamanage.domain.entity")
|
||||||
public class CannaManageApplication {
|
public class CannaManageApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
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.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
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.time.Instant;
|
||||||
|
import java.util.HexFormat;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,8 +52,8 @@ public class AuthService {
|
|||||||
user.getId(), user.getTenantId(), roleName, user.getEmail());
|
user.getId(), user.getTenantId(), roleName, user.getEmail());
|
||||||
String refreshToken = jwtService.generateRefreshToken(user.getId(), user.getTenantId());
|
String refreshToken = jwtService.generateRefreshToken(user.getId(), user.getTenantId());
|
||||||
|
|
||||||
// Store hashed refresh token for revocation
|
// Store SHA-256 hashed refresh token for revocation (BCrypt can't handle >72 bytes)
|
||||||
user.setRefreshTokenHash(passwordEncoder.encode(refreshToken));
|
user.setRefreshTokenHash(sha256(refreshToken));
|
||||||
user.setLastLogin(Instant.now());
|
user.setLastLogin(Instant.now());
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@ public class AuthService {
|
|||||||
|
|
||||||
// Verify the refresh token matches stored hash (revocation check)
|
// Verify the refresh token matches stored hash (revocation check)
|
||||||
if (user.getRefreshTokenHash() == null ||
|
if (user.getRefreshTokenHash() == null ||
|
||||||
!passwordEncoder.matches(token, user.getRefreshTokenHash())) {
|
!sha256(token).equals(user.getRefreshTokenHash())) {
|
||||||
throw new AuthenticationException("Refresh token has been revoked");
|
throw new AuthenticationException("Refresh token has been revoked");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,12 +90,27 @@ public class AuthService {
|
|||||||
user.getId(), user.getTenantId(), roleName, user.getEmail());
|
user.getId(), user.getTenantId(), roleName, user.getEmail());
|
||||||
String newRefreshToken = jwtService.generateRefreshToken(user.getId(), user.getTenantId());
|
String newRefreshToken = jwtService.generateRefreshToken(user.getId(), user.getTenantId());
|
||||||
|
|
||||||
user.setRefreshTokenHash(passwordEncoder.encode(newRefreshToken));
|
user.setRefreshTokenHash(sha256(newRefreshToken));
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
|
||||||
return new LoginResponse(newAccessToken, newRefreshToken, 3600L, roleName);
|
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.
|
* Custom authentication exception — caught by GlobalExceptionHandler.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+196
@@ -0,0 +1,196 @@
|
|||||||
|
package de.cannamanage.api.controller;
|
||||||
|
|
||||||
|
import de.cannamanage.api.dto.auth.LoginRequest;
|
||||||
|
import de.cannamanage.api.dto.auth.LoginResponse;
|
||||||
|
import de.cannamanage.api.dto.auth.RefreshRequest;
|
||||||
|
import de.cannamanage.domain.entity.TenantContext;
|
||||||
|
import de.cannamanage.domain.entity.User;
|
||||||
|
import de.cannamanage.domain.enums.UserRole;
|
||||||
|
import de.cannamanage.service.repository.UserRepository;
|
||||||
|
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.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.web.client.HttpClientErrorException;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for {@link AuthController}.
|
||||||
|
* Boots the full Spring context with H2 in-memory DB and a real HTTP server.
|
||||||
|
*/
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class AuthControllerIntegrationTest {
|
||||||
|
|
||||||
|
@LocalServerPort
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
private RestClient restClient;
|
||||||
|
|
||||||
|
private static final UUID TENANT_ID = UUID.fromString("00000000-0000-0000-0000-000000000001");
|
||||||
|
private static final String TEST_EMAIL = "admin@test.club";
|
||||||
|
private static final String TEST_PASSWORD = "SecurePass123!";
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
restClient = RestClient.builder()
|
||||||
|
.baseUrl("http://localhost:" + port)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
userRepository.deleteAll();
|
||||||
|
|
||||||
|
// Set TenantContext so @PrePersist can pick up tenantId
|
||||||
|
TenantContext.setCurrentTenant(TENANT_ID);
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
user.setEmail(TEST_EMAIL);
|
||||||
|
user.setPasswordHash(passwordEncoder.encode(TEST_PASSWORD));
|
||||||
|
user.setRole(UserRole.ROLE_ADMIN);
|
||||||
|
user.setActive(true);
|
||||||
|
userRepository.saveAndFlush(user);
|
||||||
|
|
||||||
|
TenantContext.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /api/v1/auth/login — valid credentials returns tokens")
|
||||||
|
void login_withValidCredentials_returnsTokens() throws Exception {
|
||||||
|
LoginRequest request = new LoginRequest(TEST_EMAIL, TEST_PASSWORD);
|
||||||
|
|
||||||
|
LoginResponse response = restClient.post()
|
||||||
|
.uri("/api/v1/auth/login")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(request)
|
||||||
|
.retrieve()
|
||||||
|
.body(LoginResponse.class);
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response.accessToken()).isNotBlank();
|
||||||
|
assertThat(response.refreshToken()).isNotBlank();
|
||||||
|
assertThat(response.expiresIn()).isEqualTo(3600L);
|
||||||
|
assertThat(response.role()).isEqualTo("ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /api/v1/auth/login — wrong password returns 401")
|
||||||
|
void login_withWrongPassword_returns401() {
|
||||||
|
LoginRequest request = new LoginRequest(TEST_EMAIL, "WrongPassword!");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> restClient.post()
|
||||||
|
.uri("/api/v1/auth/login")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(request)
|
||||||
|
.retrieve()
|
||||||
|
.body(String.class))
|
||||||
|
.isInstanceOf(HttpClientErrorException.Unauthorized.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /api/v1/auth/login — unknown email returns 401")
|
||||||
|
void login_withUnknownEmail_returns401() {
|
||||||
|
LoginRequest request = new LoginRequest("nobody@test.club", TEST_PASSWORD);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> restClient.post()
|
||||||
|
.uri("/api/v1/auth/login")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(request)
|
||||||
|
.retrieve()
|
||||||
|
.body(String.class))
|
||||||
|
.isInstanceOf(HttpClientErrorException.Unauthorized.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /api/v1/auth/login — disabled user returns 401")
|
||||||
|
void login_withDisabledUser_returns401() {
|
||||||
|
// Disable the test user
|
||||||
|
TenantContext.setCurrentTenant(TENANT_ID);
|
||||||
|
User user = userRepository.findByEmail(TEST_EMAIL).orElseThrow();
|
||||||
|
user.setActive(false);
|
||||||
|
userRepository.saveAndFlush(user);
|
||||||
|
TenantContext.clear();
|
||||||
|
|
||||||
|
LoginRequest request = new LoginRequest(TEST_EMAIL, TEST_PASSWORD);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> restClient.post()
|
||||||
|
.uri("/api/v1/auth/login")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(request)
|
||||||
|
.retrieve()
|
||||||
|
.body(String.class))
|
||||||
|
.isInstanceOf(HttpClientErrorException.Unauthorized.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /api/v1/auth/login — missing email returns 400")
|
||||||
|
void login_withMissingEmail_returns400() {
|
||||||
|
assertThatThrownBy(() -> restClient.post()
|
||||||
|
.uri("/api/v1/auth/login")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body("{\"password\": \"test123\"}")
|
||||||
|
.retrieve()
|
||||||
|
.body(String.class))
|
||||||
|
.isInstanceOf(HttpClientErrorException.BadRequest.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /api/v1/auth/refresh — valid refresh token returns new token pair")
|
||||||
|
void refresh_withValidToken_returnsNewTokens() throws Exception {
|
||||||
|
// First login to get a refresh token
|
||||||
|
LoginRequest loginRequest = new LoginRequest(TEST_EMAIL, TEST_PASSWORD);
|
||||||
|
|
||||||
|
LoginResponse loginResponse = restClient.post()
|
||||||
|
.uri("/api/v1/auth/login")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(loginRequest)
|
||||||
|
.retrieve()
|
||||||
|
.body(LoginResponse.class);
|
||||||
|
|
||||||
|
assertThat(loginResponse).isNotNull();
|
||||||
|
String refreshToken = loginResponse.refreshToken();
|
||||||
|
|
||||||
|
// Use the refresh token
|
||||||
|
RefreshRequest refreshRequest = new RefreshRequest(refreshToken);
|
||||||
|
|
||||||
|
LoginResponse refreshResponse = restClient.post()
|
||||||
|
.uri("/api/v1/auth/refresh")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(refreshRequest)
|
||||||
|
.retrieve()
|
||||||
|
.body(LoginResponse.class);
|
||||||
|
|
||||||
|
assertThat(refreshResponse).isNotNull();
|
||||||
|
assertThat(refreshResponse.accessToken()).isNotBlank();
|
||||||
|
assertThat(refreshResponse.refreshToken()).isNotBlank();
|
||||||
|
assertThat(refreshResponse.role()).isEqualTo("ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /api/v1/auth/refresh — invalid token returns 401")
|
||||||
|
void refresh_withInvalidToken_returns401() {
|
||||||
|
RefreshRequest request = new RefreshRequest("invalid.jwt.token");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> restClient.post()
|
||||||
|
.uri("/api/v1/auth/refresh")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(request)
|
||||||
|
.retrieve()
|
||||||
|
.body(String.class))
|
||||||
|
.isInstanceOf(HttpClientErrorException.Unauthorized.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
package de.cannamanage.api.controller;
|
||||||
|
|
||||||
|
import de.cannamanage.api.dto.compliance.QuotaResponse;
|
||||||
|
import de.cannamanage.api.security.JwtService;
|
||||||
|
import de.cannamanage.domain.entity.Member;
|
||||||
|
import de.cannamanage.domain.entity.TenantContext;
|
||||||
|
import de.cannamanage.domain.enums.MemberStatus;
|
||||||
|
import de.cannamanage.service.repository.MemberRepository;
|
||||||
|
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.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.web.client.HttpClientErrorException;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for {@link ComplianceController}.
|
||||||
|
* Boots the full Spring context with H2 in-memory DB.
|
||||||
|
* Tests quota status endpoint with JWT authentication.
|
||||||
|
*/
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class ComplianceControllerIntegrationTest {
|
||||||
|
|
||||||
|
@LocalServerPort
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MemberRepository memberRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JwtService jwtService;
|
||||||
|
|
||||||
|
private RestClient restClient;
|
||||||
|
|
||||||
|
private static final UUID TENANT_ID = UUID.fromString("00000000-0000-0000-0000-000000000001");
|
||||||
|
private static final UUID USER_ID = UUID.fromString("00000000-0000-0000-0000-000000000099");
|
||||||
|
|
||||||
|
private UUID memberId;
|
||||||
|
private String adminToken;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
memberRepository.deleteAll();
|
||||||
|
|
||||||
|
TenantContext.setCurrentTenant(TENANT_ID);
|
||||||
|
|
||||||
|
// Create a test member (adult, 25 years old)
|
||||||
|
Member member = new Member();
|
||||||
|
member.setClubId(UUID.fromString("00000000-0000-0000-0000-000000000010"));
|
||||||
|
member.setFirstName("Max");
|
||||||
|
member.setLastName("Mustermann");
|
||||||
|
member.setEmail("max@test.club");
|
||||||
|
member.setDateOfBirth(LocalDate.now().minusYears(25));
|
||||||
|
member.setMembershipDate(LocalDate.now().minusMonths(6));
|
||||||
|
member.setMembershipNumber("CM-2025-001");
|
||||||
|
member.setStatus(MemberStatus.ACTIVE);
|
||||||
|
member.setUnder21(false);
|
||||||
|
member = memberRepository.saveAndFlush(member);
|
||||||
|
memberId = member.getId();
|
||||||
|
|
||||||
|
TenantContext.clear();
|
||||||
|
|
||||||
|
// Generate a JWT token for authentication
|
||||||
|
adminToken = jwtService.generateAccessToken(USER_ID, TENANT_ID, "ADMIN", "admin@test.club");
|
||||||
|
|
||||||
|
restClient = RestClient.builder()
|
||||||
|
.baseUrl("http://localhost:" + port)
|
||||||
|
.defaultHeader("Authorization", "Bearer " + adminToken)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("GET /api/v1/compliance/quota/{memberId} — returns quota for adult member")
|
||||||
|
void getQuotaStatus_adultMember_returnsQuota() {
|
||||||
|
QuotaResponse response = restClient.get()
|
||||||
|
.uri("/api/v1/compliance/quota/{memberId}", memberId)
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.retrieve()
|
||||||
|
.body(QuotaResponse.class);
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response.totalAllowed()).isEqualByComparingTo("50");
|
||||||
|
assertThat(response.totalUsed()).isEqualByComparingTo("0");
|
||||||
|
assertThat(response.remaining()).isEqualByComparingTo("50");
|
||||||
|
assertThat(response.under21()).isFalse();
|
||||||
|
assertThat(response.year()).isEqualTo(LocalDate.now().getYear());
|
||||||
|
assertThat(response.month()).isEqualTo(LocalDate.now().getMonthValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("GET /api/v1/compliance/quota/{memberId} — under-21 member gets reduced limit")
|
||||||
|
void getQuotaStatus_under21Member_returnsReducedLimit() {
|
||||||
|
// Create under-21 member
|
||||||
|
TenantContext.setCurrentTenant(TENANT_ID);
|
||||||
|
Member youngMember = new Member();
|
||||||
|
youngMember.setClubId(UUID.fromString("00000000-0000-0000-0000-000000000010"));
|
||||||
|
youngMember.setFirstName("Jung");
|
||||||
|
youngMember.setLastName("Mitglied");
|
||||||
|
youngMember.setEmail("jung@test.club");
|
||||||
|
youngMember.setDateOfBirth(LocalDate.now().minusYears(19));
|
||||||
|
youngMember.setMembershipDate(LocalDate.now().minusMonths(3));
|
||||||
|
youngMember.setMembershipNumber("CM-2025-002");
|
||||||
|
youngMember.setStatus(MemberStatus.ACTIVE);
|
||||||
|
youngMember.setUnder21(true);
|
||||||
|
youngMember = memberRepository.saveAndFlush(youngMember);
|
||||||
|
UUID youngMemberId = youngMember.getId();
|
||||||
|
TenantContext.clear();
|
||||||
|
|
||||||
|
QuotaResponse response = restClient.get()
|
||||||
|
.uri("/api/v1/compliance/quota/{memberId}", youngMemberId)
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.retrieve()
|
||||||
|
.body(QuotaResponse.class);
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response.totalAllowed()).isEqualByComparingTo("30");
|
||||||
|
assertThat(response.under21()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("GET /api/v1/compliance/quota/{memberId} — non-existent member returns 404")
|
||||||
|
void getQuotaStatus_nonExistentMember_returns404() {
|
||||||
|
UUID nonExistentId = UUID.fromString("00000000-0000-0000-0000-999999999999");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> restClient.get()
|
||||||
|
.uri("/api/v1/compliance/quota/{memberId}", nonExistentId)
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.retrieve()
|
||||||
|
.body(String.class))
|
||||||
|
.isInstanceOf(HttpClientErrorException.NotFound.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("GET /api/v1/compliance/quota/{memberId} — no auth returns 401/403")
|
||||||
|
void getQuotaStatus_noAuth_returnsUnauthorized() {
|
||||||
|
RestClient unauthClient = RestClient.builder()
|
||||||
|
.baseUrl("http://localhost:" + port)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> unauthClient.get()
|
||||||
|
.uri("/api/v1/compliance/quota/{memberId}", memberId)
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.retrieve()
|
||||||
|
.body(String.class))
|
||||||
|
.isInstanceOf(HttpClientErrorException.class)
|
||||||
|
.satisfies(ex -> {
|
||||||
|
int status = ((HttpClientErrorException) ex).getStatusCode().value();
|
||||||
|
assertThat(status).isBetween(401, 403);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("GET /api/v1/compliance/quota/{memberId} — invalid token returns 401/403")
|
||||||
|
void getQuotaStatus_invalidToken_returnsUnauthorized() {
|
||||||
|
RestClient badTokenClient = RestClient.builder()
|
||||||
|
.baseUrl("http://localhost:" + port)
|
||||||
|
.defaultHeader("Authorization", "Bearer invalid.jwt.token")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> badTokenClient.get()
|
||||||
|
.uri("/api/v1/compliance/quota/{memberId}", memberId)
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.retrieve()
|
||||||
|
.body(String.class))
|
||||||
|
.isInstanceOf(HttpClientErrorException.class)
|
||||||
|
.satisfies(ex -> {
|
||||||
|
int status = ((HttpClientErrorException) ex).getStatusCode().value();
|
||||||
|
assertThat(status).isBetween(401, 403);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
spring.application.name=cannamanage-test
|
spring.application.name=cannamanage-test
|
||||||
spring.datasource.url=jdbc:h2:mem:cannamanage_test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
|
spring.datasource.url=jdbc:h2:mem:cannamanage_test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL;NON_KEYWORDS=MONTH,YEAR
|
||||||
spring.datasource.username=sa
|
spring.datasource.username=sa
|
||||||
spring.datasource.password=
|
spring.datasource.password=
|
||||||
spring.datasource.driver-class-name=org.h2.Driver
|
spring.datasource.driver-class-name=org.h2.Driver
|
||||||
|
|
||||||
# Let Hibernate create schema from entities (H2 doesn't support all Postgres DDL)
|
# Let Hibernate create schema from entities
|
||||||
spring.jpa.hibernate.ddl-auto=create-drop
|
spring.jpa.hibernate.ddl-auto=create-drop
|
||||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
spring.jpa.open-in-view=false
|
||||||
|
spring.jpa.show-sql=true
|
||||||
spring.flyway.enabled=false
|
spring.flyway.enabled=false
|
||||||
|
|
||||||
# JWT test secret
|
# JWT test secret
|
||||||
|
|||||||
Reference in New Issue
Block a user