middleware_authconfig_fallback.rs 115 lignes · 4449 octets
//! RFC 0022 + RFC 0031 — anti-régression : `AuthService` /
//! `AdminService` / `admin_helper` doivent retomber sur
//! `AuthConfig::from_env()` quand le consommateur n'injecte pas
//! `Extension(Arc<AuthConfig>)` *avant* l'auth middleware dans son
//! `ServiceBuilder`. Le commit `d4d6828` avait introduit un `500`
//! silencieux dans la branche **protected** (corrigé par RFC 0022) ;
//! la branche **optional auth** restait sans fallback jusqu'à la
//! RFC 0031, ce qui produisait des `claims = None` silencieux sur
//! toutes les routes hors `protected_prefixes`.
//!
//! Ce fichier vérifie le pré-requis partagé par les deux branches :
//! `AuthConfig::from_env()` tourne avec un set minimal de variables
//! d'environnement (comme en prod), et la config rechargée reste
//! fonctionnellement cohérente (sign → validate roundtrip). Si l'une
//! de ces assertions casse, le fallback de `AuthService::call` est
//! inutile dans les deux branches.

use std::sync::Mutex;

use rustwarden_core::auth::jwt::JwtService;
use rustwarden_core::config::auth::AuthConfig;
use uuid::Uuid;

static ENV_LOCK: Mutex<()> = Mutex::new(());

fn set_minimal_env() {
    // SAFETY: tests under #[tokio::test] run on the same process; the
    // mutex above serialises mutations.
    unsafe {
        std::env::set_var(
            "JWT_SECRET",
            "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
        );
        // Permit non-HTTPS cookies in dev so `from_env` validates without
        // requiring a TLS endpoint configured.
        std::env::set_var("APP_DEBUG", "true");
    }
}

fn clear_env() {
    // SAFETY: see set_minimal_env.
    unsafe {
        std::env::remove_var("JWT_SECRET");
        std::env::remove_var("APP_DEBUG");
    }
}

#[test]
fn auth_config_from_env_succeeds_with_minimal_vars() {
    let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    set_minimal_env();
    let cfg = AuthConfig::from_env();
    assert!(
        cfg.is_ok(),
        "AuthConfig::from_env must succeed with JWT_SECRET set: got {cfg:?}",
    );
}

#[test]
fn auth_config_from_env_fails_without_jwt_secret() {
    let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    clear_env();
    // Without JWT_SECRET, from_env must surface an error rather than
    // silently using a default — the fallback in AuthService surfaces
    // this as a `tracing::error!` line and a 500.
    let cfg = AuthConfig::from_env();
    assert!(
        cfg.is_err(),
        "AuthConfig::from_env must reject missing JWT_SECRET: got {cfg:?}",
    );
}

#[test]
fn auth_config_from_env_is_idempotent() {
    let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    set_minimal_env();
    let a = AuthConfig::from_env().expect("first load");
    let b = AuthConfig::from_env().expect("second load");
    // Two consecutive calls must yield equal jwt_expiration_minutes —
    // the trait-less smoke check that the env-driven loader is stable.
    assert_eq!(
        a.jwt_expiration_minutes, b.jwt_expiration_minutes,
        "AuthConfig::from_env must be deterministic across calls",
    );
}

#[test]
fn auth_config_from_env_yields_signature_consistent_loader() {
    // RFC 0031 — la branche optional auth de `AuthService` signe et
    // valide les JWT avec un `AuthConfig::from_env()` rechargé. Cette
    // assertion garantit que deux chargements successifs produisent des
    // clefs HMAC fonctionnellement équivalentes : un token émis avec
    // `from_env()` doit pouvoir être validé par un autre `from_env()`
    // tournant dans le même process. Sans cette propriété, le fallback
    // de RFC 0031 émettrait silencieusement `claims = None`.
    let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    set_minimal_env();

    let signer = AuthConfig::from_env().expect("signer load");
    let validator = AuthConfig::from_env().expect("validator load");

    let user_id = Uuid::new_v4();
    let token =
        JwtService::generate_token(&signer, user_id, "fallback_user", Vec::new(), Vec::new())
            .expect("token signing must succeed");

    let claims = JwtService::validate_token(&validator, &token)
        .expect("token signed by from_env() must validate against another from_env()");

    assert_eq!(
        claims.sub, user_id,
        "validated claims must round-trip the user id"
    );
    assert_eq!(claims.username, "fallback_user");
}