diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index fa21095..8fa51ba 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -23,13 +23,21 @@ quarkus.flyway.baseline-on-migrate=false # Configuration Keycloak pour tests (désactivé) quarkus.oidc.tenant-enabled=false quarkus.keycloak.policy-enforcer.enabled=false -# Désactivation globale de TOUS les DevServices en mode test : -# - DataSource : H2 in-memory configuré en dur (pas besoin de Postgres DevService) -# - Keycloak : OIDC désactivé (pas besoin de container KC, qui prenait 50s+ à boot) -# Dev mode (mvn quarkus:dev) : DevServices restent actifs (utiles). -quarkus.devservices.enabled=false +# Keycloak DevService désactivé : OIDC tenant-enabled=false ⇒ aucun besoin de KC en test. +# (Évite le démarrage d'un container KC parasite de 50s+ ignorant l'instance locale 8180.) +# NB : on NE désactive PAS quarkus.devservices.enabled globalement — laisse le mécanisme +# disponible si un futur test active un Postgres Testcontainer (cf. dette H2→Testcontainers). quarkus.keycloak.devservices.enabled=false +# DETTE TECHNIQUE — H2 vs Postgres réel +# Les tests utilisent H2 en mode PostgreSQL pour rapidité, mais le projet utilise massivement : +# - JSONB (audit_trail, payload_jsonb, etc.) → mappé en VARCHAR sur H2 (faux positif possible) +# - RLS (Row Level Security PostgreSQL) → non supporté par H2 +# - Fonctions PG natives (gen_random_uuid, citext, etc.) → comportement divergent +# TODO sprint dédié : migrer vers Testcontainers Postgres avec reuse +# (testcontainers.reuse.enable=true) pour fidélité prod + démarrage rapide après warm-up. +# Cf. memory : project_test_infrastructure_debt.md + # Configuration HTTP pour tests quarkus.http.port=0 quarkus.http.test-port=0 diff --git a/src/test/java/dev/lions/unionflow/server/client/JwtPropagationFilterNullIdentityTest.java b/src/test/java/dev/lions/unionflow/server/client/JwtPropagationFilterNullIdentityTest.java deleted file mode 100644 index 565bcdf..0000000 --- a/src/test/java/dev/lions/unionflow/server/client/JwtPropagationFilterNullIdentityTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package dev.lions.unionflow.server.client; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import jakarta.ws.rs.client.ClientRequestContext; -import jakarta.ws.rs.core.MultivaluedHashMap; -import jakarta.ws.rs.core.MultivaluedMap; -import java.io.IOException; -import java.lang.reflect.Field; -import java.net.URI; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -/** - * Test SANS @QuarkusTest pour couvrir la branche {@code securityIdentity == null} - * dans {@link JwtPropagationFilter#filter} (L29). - * - *

En contexte CDI, {@code securityIdentity} est toujours un proxy non-null. - * Cette branche n'est atteignable qu'en instanciant {@link JwtPropagationFilter} directement - * et en laissant le champ à {@code null} (valeur par défaut Java). - */ -class JwtPropagationFilterNullIdentityTest { - - private ClientRequestContext buildMockContext() { - MultivaluedMap headers = new MultivaluedHashMap<>(); - ClientRequestContext ctx = mock(ClientRequestContext.class); - when(ctx.getHeaders()).thenReturn(headers); - when(ctx.getUri()).thenReturn(URI.create("http://localhost/api/test")); - return ctx; - } - - @Test - @DisplayName("filter : securityIdentity null → warn 'Pas de SecurityIdentity', pas de header Authorization (branche null L29)") - void filter_securityIdentityNull_doesNotPropagate() throws Exception { - // Instanciation directe — securityIdentity reste null (champ non injecté) - JwtPropagationFilter filter = new JwtPropagationFilter(); - // securityIdentity est null par défaut (pas d'injection CDI) - - // Vérifie que le champ est bien null - Field siField = JwtPropagationFilter.class.getDeclaredField("securityIdentity"); - siField.setAccessible(true); - assertThat(siField.get(filter)).isNull(); - - ClientRequestContext ctx = buildMockContext(); - filter.filter(ctx); // ne doit pas lever d'exception - - // Pas de header Authorization ajouté - assertThat(ctx.getHeaders().getFirst("Authorization")).isNull(); - } -} diff --git a/src/test/java/dev/lions/unionflow/server/client/JwtPropagationFilterTest.java b/src/test/java/dev/lions/unionflow/server/client/JwtPropagationFilterTest.java index d6eea5c..9a31c84 100644 --- a/src/test/java/dev/lions/unionflow/server/client/JwtPropagationFilterTest.java +++ b/src/test/java/dev/lions/unionflow/server/client/JwtPropagationFilterTest.java @@ -1,44 +1,54 @@ package dev.lions.unionflow.server.client; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import io.quarkus.oidc.runtime.OidcJwtCallerPrincipal; import io.quarkus.security.identity.SecurityIdentity; -import io.quarkus.test.InjectMock; -import io.quarkus.test.junit.QuarkusTest; -import jakarta.inject.Inject; import jakarta.ws.rs.client.ClientRequestContext; import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.URI; +import java.security.Principal; import org.eclipse.microprofile.jwt.JsonWebToken; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.io.IOException; -import java.net.URI; -import java.security.Principal; - /** - * Tests pour {@link JwtPropagationFilter}. + * Tests pour {@link JwtPropagationFilter} — pur Mockito (pas de @QuarkusTest). + * + *

Le filtre est instancié directement et {@code securityIdentity} est injecté + * par réflexion. Cela évite le démarrage Quarkus complet (Postgres/OIDC) pour + * un simple test unitaire de filtre REST Client. * *

Couvre toutes les branches de {@code filter()} : *

*/ -@QuarkusTest class JwtPropagationFilterTest { - @Inject - JwtPropagationFilter filter; + private JwtPropagationFilter filter; + private SecurityIdentity securityIdentity; - @InjectMock - SecurityIdentity securityIdentity; + @BeforeEach + void setUp() throws Exception { + filter = new JwtPropagationFilter(); + securityIdentity = mock(SecurityIdentity.class); + Field f = JwtPropagationFilter.class.getDeclaredField("securityIdentity"); + f.setAccessible(true); + f.set(filter, securityIdentity); + } private ClientRequestContext buildMockContext() { MultivaluedMap headers = new MultivaluedHashMap<>(); @@ -48,6 +58,22 @@ class JwtPropagationFilterTest { return ctx; } + // ─── Branch: securityIdentity == null ──────────────────────────────────── + + @Test + @DisplayName("filter : securityIdentity null → warn, pas de header (branche L41)") + void filter_securityIdentityNull_doesNotPropagate() throws Exception { + // Remettre le champ à null pour cette branche spécifique + Field f = JwtPropagationFilter.class.getDeclaredField("securityIdentity"); + f.setAccessible(true); + f.set(filter, null); + + ClientRequestContext ctx = buildMockContext(); + filter.filter(ctx); + + assertThat(ctx.getHeaders().getFirst("Authorization")).isNull(); + } + // ─── Branch: securityIdentity.isAnonymous() = true → skip ──────────────── @Test @@ -97,12 +123,28 @@ class JwtPropagationFilterTest { assertThat(ctx.getHeaders().getFirst("Authorization")).isNull(); } + // ─── Branch: OidcJwtCallerPrincipal avec token null ────────────────────── + + @Test + @DisplayName("filter : OidcJwtCallerPrincipal avec token null → pas de propagation") + void filter_oidcPrincipalWithNullToken_doesNotPropagate() throws IOException { + OidcJwtCallerPrincipal principal = mock(OidcJwtCallerPrincipal.class); + when(principal.getRawToken()).thenReturn(null); + + when(securityIdentity.isAnonymous()).thenReturn(false); + when(securityIdentity.getPrincipal()).thenReturn(principal); + + ClientRequestContext ctx = buildMockContext(); + filter.filter(ctx); + + assertThat(ctx.getHeaders().getFirst("Authorization")).isNull(); + } + // ─── Branch: JsonWebToken (NOT OidcJwtCallerPrincipal) ─────────────────── @Test @DisplayName("filter : JsonWebToken principal (non-OIDC) avec token valide → Authorization propagé") void filter_jsonWebTokenPrincipalWithValidToken_propagatesToken() throws IOException { - // JsonWebToken mock n'est PAS OidcJwtCallerPrincipal → branche else-if JsonWebToken jwt = mock(JsonWebToken.class); when(jwt.getRawToken()).thenReturn("valid-jwt-from-JsonWebToken"); @@ -117,41 +159,6 @@ class JwtPropagationFilterTest { assertThat(authHeader.toString()).isEqualTo("Bearer valid-jwt-from-JsonWebToken"); } - // ─── Branch: principal ni OidcJwtCallerPrincipal ni JsonWebToken ───────── - - @Test - @DisplayName("filter : principal inconnu (ni OIDC ni JWT) → log warn, pas de header Authorization") - void filter_unknownPrincipalType_doesNotPropagate() throws IOException { - // Principal générique — ni OidcJwtCallerPrincipal ni JsonWebToken → branche else - Principal genericPrincipal = mock(Principal.class); - when(genericPrincipal.getName()).thenReturn("some-user"); - - when(securityIdentity.isAnonymous()).thenReturn(false); - when(securityIdentity.getPrincipal()).thenReturn(genericPrincipal); - - ClientRequestContext ctx = buildMockContext(); - filter.filter(ctx); - - assertThat(ctx.getHeaders().getFirst("Authorization")).isNull(); - } - - // ─── Branch: OidcJwtCallerPrincipal avec token null → pas de propagation ─ - - @Test - @DisplayName("filter : OidcJwtCallerPrincipal avec token null → pas de propagation (branche token==null L35)") - void filter_oidcPrincipalWithNullToken_doesNotPropagate() throws IOException { - OidcJwtCallerPrincipal principal = mock(OidcJwtCallerPrincipal.class); - when(principal.getRawToken()).thenReturn(null); // null → condition false - - when(securityIdentity.isAnonymous()).thenReturn(false); - when(securityIdentity.getPrincipal()).thenReturn(principal); - - ClientRequestContext ctx = buildMockContext(); - filter.filter(ctx); - - assertThat(ctx.getHeaders().getFirst("Authorization")).isNull(); - } - @Test @DisplayName("filter : JsonWebToken principal avec token null → pas de propagation") void filter_jsonWebTokenWithNullToken_doesNotPropagate() throws IOException { @@ -181,4 +188,21 @@ class JwtPropagationFilterTest { assertThat(ctx.getHeaders().getFirst("Authorization")).isNull(); } + + // ─── Branch: principal ni OidcJwtCallerPrincipal ni JsonWebToken ───────── + + @Test + @DisplayName("filter : principal inconnu (ni OIDC ni JWT) → log warn, pas de header Authorization") + void filter_unknownPrincipalType_doesNotPropagate() throws IOException { + Principal genericPrincipal = mock(Principal.class); + when(genericPrincipal.getName()).thenReturn("some-user"); + + when(securityIdentity.isAnonymous()).thenReturn(false); + when(securityIdentity.getPrincipal()).thenReturn(genericPrincipal); + + ClientRequestContext ctx = buildMockContext(); + filter.filter(ctx); + + assertThat(ctx.getHeaders().getFirst("Authorization")).isNull(); + } }