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()} :
*
- * - securityIdentity anonyme → pas de propagation
+ * - {@code securityIdentity == null} → log warn, pas de header (branche L41)
+ * - identité anonyme → pas de propagation
* - OidcJwtCallerPrincipal avec token valide → header Authorization propagé
- * - OidcJwtCallerPrincipal avec token vide/blank → pas de propagation
+ * - OidcJwtCallerPrincipal avec token null/blank → pas de propagation
* - JsonWebToken (non OidcJwtCallerPrincipal) avec token valide → header propagé
+ * - JsonWebToken avec token null/blank → pas de propagation
* - Principal générique (ni OidcJwtCallerPrincipal ni JsonWebToken) → log warn, pas de header
*
*/
-@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();
+ }
}