From cfa989bd5ed8c73393149d8f4f988f27e99d4640 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:06:31 +0200 Subject: [PATCH] fix: allow JwtDecoder bean override in test context - Add @Primary + @ConditionalOnMissingBean so TestSecurityConfig.jwtDecoder() wins over SecurityConfig.jwtDecoder() without needing a real OIDC endpoint - Add spring.main.allow-bean-definition-overriding=true and cameleer.clickhouse.enabled=false to src/test/resources/application-test.yml so Testcontainers @ServiceConnection can supply the datasource - Disable ClickHouse in test profile (src/main/resources/application-test.yml) so the explicit ClickHouseConfig DataSource bean is not created, allowing @ServiceConnection to wire the Testcontainers Postgres datasource - Fix TenantControllerTest and LicenseControllerTest to explicitly grant ROLE_platform-admin authority via .authorities() on the test JWT, since spring-security-test does not run the custom JwtAuthenticationConverter - Fix EnvironmentService.createDefaultForTenant() to use an internal bootstrap path that skips license enforcement (chicken-and-egg: no license exists at tenant creation time yet) - Remove now-unnecessary license stub from EnvironmentServiceTest Co-Authored-By: Claude Sonnet 4.6 --- .../cameleer/saas/config/SecurityConfig.java | 2 ++ .../saas/environment/EnvironmentService.java | 14 +++++++++++++- src/main/resources/application-test.yml | 4 ++++ .../siegeln/cameleer/saas/TestSecurityConfig.java | 2 ++ .../saas/environment/EnvironmentServiceTest.java | 6 ------ .../saas/license/LicenseControllerTest.java | 7 ++++++- .../cameleer/saas/tenant/TenantControllerTest.java | 14 ++++++++++---- src/test/resources/application-test.yml | 7 +++++++ 8 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 src/test/resources/application-test.yml diff --git a/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java b/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java index c8f72f6..c2dbce3 100644 --- a/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java +++ b/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java @@ -6,6 +6,7 @@ import com.nimbusds.jose.proc.JWSVerificationKeySelector; import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.proc.DefaultJWTProcessor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @@ -78,6 +79,7 @@ public class SecurityConfig { } @Bean + @ConditionalOnMissingBean public JwtDecoder jwtDecoder( @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwkSetUri, @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri:}") String issuerUri) throws Exception { diff --git a/src/main/java/net/siegeln/cameleer/saas/environment/EnvironmentService.java b/src/main/java/net/siegeln/cameleer/saas/environment/EnvironmentService.java index faa488f..1dc24a7 100644 --- a/src/main/java/net/siegeln/cameleer/saas/environment/EnvironmentService.java +++ b/src/main/java/net/siegeln/cameleer/saas/environment/EnvironmentService.java @@ -49,7 +49,19 @@ public class EnvironmentService { public EnvironmentEntity createDefaultForTenant(UUID tenantId) { return environmentRepository.findByTenantIdAndSlug(tenantId, "default") - .orElseGet(() -> create(tenantId, "default", "Default", null)); + .orElseGet(() -> createInternal(tenantId, "default", "Default")); + } + + /** Creates an environment without license enforcement — used for bootstrapping (e.g., tenant provisioning). */ + private EnvironmentEntity createInternal(UUID tenantId, String slug, String displayName) { + if (environmentRepository.existsByTenantIdAndSlug(tenantId, slug)) { + throw new IllegalArgumentException("Slug already exists for this tenant: " + slug); + } + var entity = new EnvironmentEntity(); + entity.setTenantId(tenantId); + entity.setSlug(slug); + entity.setDisplayName(displayName); + return environmentRepository.save(entity); } public List listByTenantId(UUID tenantId) { diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 308b82c..bcbe85f 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -8,3 +8,7 @@ spring: resourceserver: jwt: issuer-uri: https://test-issuer.example.com/oidc + +cameleer: + clickhouse: + enabled: false diff --git a/src/test/java/net/siegeln/cameleer/saas/TestSecurityConfig.java b/src/test/java/net/siegeln/cameleer/saas/TestSecurityConfig.java index cbf78aa..77d0b58 100644 --- a/src/test/java/net/siegeln/cameleer/saas/TestSecurityConfig.java +++ b/src/test/java/net/siegeln/cameleer/saas/TestSecurityConfig.java @@ -2,6 +2,7 @@ package net.siegeln.cameleer.saas; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; @@ -12,6 +13,7 @@ import java.util.List; public class TestSecurityConfig { @Bean + @Primary public JwtDecoder jwtDecoder() { return token -> Jwt.withTokenValue(token) .header("alg", "ES384") diff --git a/src/test/java/net/siegeln/cameleer/saas/environment/EnvironmentServiceTest.java b/src/test/java/net/siegeln/cameleer/saas/environment/EnvironmentServiceTest.java index d95ee50..f8bb833 100644 --- a/src/test/java/net/siegeln/cameleer/saas/environment/EnvironmentServiceTest.java +++ b/src/test/java/net/siegeln/cameleer/saas/environment/EnvironmentServiceTest.java @@ -160,15 +160,9 @@ class EnvironmentServiceTest { @Test void createDefaultForTenant_shouldCreateWithDefaultSlug() { var tenantId = UUID.randomUUID(); - var license = new LicenseEntity(); - license.setTenantId(tenantId); - license.setTier("LOW"); when(environmentRepository.findByTenantIdAndSlug(tenantId, "default")).thenReturn(Optional.empty()); when(environmentRepository.existsByTenantIdAndSlug(tenantId, "default")).thenReturn(false); - when(licenseRepository.findFirstByTenantIdAndRevokedAtIsNullOrderByCreatedAtDesc(tenantId)) - .thenReturn(Optional.of(license)); - when(environmentRepository.countByTenantId(tenantId)).thenReturn(0L); when(environmentRepository.save(any(EnvironmentEntity.class))).thenAnswer(inv -> inv.getArgument(0)); var result = environmentService.createDefaultForTenant(tenantId); diff --git a/src/test/java/net/siegeln/cameleer/saas/license/LicenseControllerTest.java b/src/test/java/net/siegeln/cameleer/saas/license/LicenseControllerTest.java index 124e017..82b914c 100644 --- a/src/test/java/net/siegeln/cameleer/saas/license/LicenseControllerTest.java +++ b/src/test/java/net/siegeln/cameleer/saas/license/LicenseControllerTest.java @@ -13,6 +13,8 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -36,7 +38,10 @@ class LicenseControllerTest { var request = new CreateTenantRequest("License Test Org", slug, "MID"); var result = mockMvc.perform(post("/api/tenants") - .with(jwt().jwt(j -> j.claim("sub", "test-user"))) + .with(jwt().jwt(j -> j + .claim("sub", "test-user") + .claim("roles", java.util.List.of("platform-admin"))) + .authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) diff --git a/src/test/java/net/siegeln/cameleer/saas/tenant/TenantControllerTest.java b/src/test/java/net/siegeln/cameleer/saas/tenant/TenantControllerTest.java index f72e350..7b96657 100644 --- a/src/test/java/net/siegeln/cameleer/saas/tenant/TenantControllerTest.java +++ b/src/test/java/net/siegeln/cameleer/saas/tenant/TenantControllerTest.java @@ -13,6 +13,8 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -39,7 +41,8 @@ class TenantControllerTest { .with(jwt().jwt(j -> j .claim("sub", "test-user") .claim("organization_id", "test-org") - .claim("roles", java.util.List.of("platform-admin")))) + .claim("roles", java.util.List.of("platform-admin"))) + .authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) @@ -56,7 +59,8 @@ class TenantControllerTest { mockMvc.perform(post("/api/tenants") .with(jwt().jwt(j -> j .claim("sub", "test-user") - .claim("roles", java.util.List.of("platform-admin")))) + .claim("roles", java.util.List.of("platform-admin"))) + .authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()); @@ -64,7 +68,8 @@ class TenantControllerTest { mockMvc.perform(post("/api/tenants") .with(jwt().jwt(j -> j .claim("sub", "test-user") - .claim("roles", java.util.List.of("platform-admin")))) + .claim("roles", java.util.List.of("platform-admin"))) + .authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isConflict()); @@ -88,7 +93,8 @@ class TenantControllerTest { var createResult = mockMvc.perform(post("/api/tenants") .with(jwt().jwt(j -> j .claim("sub", "test-user") - .claim("roles", java.util.List.of("platform-admin")))) + .claim("roles", java.util.List.of("platform-admin"))) + .authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..4edb775 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,7 @@ +spring: + main: + allow-bean-definition-overriding: true + +cameleer: + clickhouse: + enabled: false