fix(sprint-17 backend): JwtPropagationFilterTest pur Mockito + revert DevServices global trop agressif
- Refactor JwtPropagationFilterTest : @QuarkusTest → pur Mockito (instanciation directe + champ securityIdentity injecté par réflexion). 9 tests en 2s vs boot Quarkus complet. - Fusion de JwtPropagationFilterNullIdentityTest dans le test principal (branche securityIdentity == null couverte via setAccessible). - Revert quarkus.devservices.enabled=false (global trop agressif, violait la règle 'ne pas appauvrir pour fixer'). - Conserve quarkus.keycloak.devservices.enabled=false (légitime : OIDC tenant-enabled=false ⇒ KC inutile). - Documente la dette H2 → Testcontainers (JSONB/RLS/fonctions PG masqués) inline + memory dédiée.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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).
|
||||
*
|
||||
* <p>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<String, Object> 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();
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Couvre toutes les branches de {@code filter()} :
|
||||
* <ul>
|
||||
* <li>securityIdentity anonyme → pas de propagation</li>
|
||||
* <li>{@code securityIdentity == null} → log warn, pas de header (branche L41)</li>
|
||||
* <li>identité anonyme → pas de propagation</li>
|
||||
* <li>OidcJwtCallerPrincipal avec token valide → header Authorization propagé</li>
|
||||
* <li>OidcJwtCallerPrincipal avec token vide/blank → pas de propagation</li>
|
||||
* <li>OidcJwtCallerPrincipal avec token null/blank → pas de propagation</li>
|
||||
* <li>JsonWebToken (non OidcJwtCallerPrincipal) avec token valide → header propagé</li>
|
||||
* <li>JsonWebToken avec token null/blank → pas de propagation</li>
|
||||
* <li>Principal générique (ni OidcJwtCallerPrincipal ni JsonWebToken) → log warn, pas de header</li>
|
||||
* </ul>
|
||||
*/
|
||||
@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<String, Object> 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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user