10-testing-and-observability.md 19061 octets

Tests et observabilité

Public visé : développeurs qui contribuent au framework Rustwarden ou qui l'intègrent dans un projet et souhaitent comprendre la stratégie de test, l'état actuel de l'observabilité et les recommandations V2.

Type Diátaxis : explication + guide pratique (comprendre la stratégie, savoir exécuter les tests).


Partie A — Tests

A.1 Panorama

Le workspace Rustwarden compte 538 tests automatisés au 17 mai 2026 :

TypeAnnotationQuantité
Tests unitaires synchrones#[test]239
Tests asynchrones (Tokio)#[tokio::test]299
Total538

Stack de test :

  • tokio::test pour tout le code asynchrone (services SeaORM, handlers Axum).
  • sea-orm MockDatabase pour les tests unitaires de services sans base réelle.
  • axum::Router + tower::ServiceExt::oneshot pour les tests d'intégration HTTP.
  • reqwest + testbed Docker pour les tests e2e fédération.
  • wiremock (RFC 0020) pour les tests d'interopérabilité AP avec un faux serveur distant.

Commande pour tout exécuter :

cargo test --workspace

Les tests e2e fédération s'auto-ignorent si le testbed n'est pas démarré — ils n'échouent pas, ils s'omettent avec un message d'explication.


A.2 Pyramide de tests

flowchart TB
    E2E["Tests e2e fédération\n(testbed Docker 3 instances)\ncrates/rustwarden-fediverse/tests/e2e_fediverse.rs\n~2000 lignes"]
    INTEG["Tests d'intégration HTTP\n(axum::Router + tower::oneshot)\npar module de handler"]
    UNIT["Tests unitaires\n(services, modèles, utilitaires)\n#[test] + #[tokio::test]"]

    E2E --> INTEG --> UNIT
  • Unitaires : testent un service ou une fonction isolément, avec MockDatabase SeaORM ou des données en mémoire. Rapides, sans dépendance externe.
  • Intégration : testent un handler HTTP complet avec la stack Axum, mais sans base PostgreSQL réelle (MockDatabase ou DB en mémoire SQLite pour certains cas).
  • E2E fédération : testent deux ou trois instances Rustwarden réelles communiquant via ActivityPub sur des ports localhost. Nécessitent le testbed Docker.

A.3 Tests unitaires de services

Le pattern recommandé utilise sea_orm::MockDatabase pour simuler la couche de persistance sans lancer PostgreSQL.

use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult};
use rustwarden_fediverse::services::fediverse_auth_service::{
    FediverseAuthService, ChallengeStatus, InitiateRequest,
};
use std::time::Duration;

#[tokio::test]
async fn test_initiate_creates_pending_challenge() {
    let db = MockDatabase::new(DatabaseBackend::Postgres)
        .append_exec_results([MockExecResult {
            last_insert_id: 1,
            rows_affected: 1,
        }])
        .into_connection();

    let result = FediverseAuthService::initiate(
        &db,
        InitiateRequest {
            handle: "@alice@a.tld".to_string(),
            redirect_url: None,
        },
        Duration::from_secs(300),
    )
    .await;

    assert!(result.is_ok());
}

Pour les fonctions pures (parsing, validation) — pas de mock nécessaire :

#[test]
fn test_parse_signature_header_rejects_missing_key_id() {
    use rustwarden_fediverse::http_signatures::parse_signature_header;
    let result = parse_signature_header(r#"algorithm="rsa-sha256",signature="abc""#);
    assert!(result.is_err());
}

A.4 Tests d'intégration HTTP

Le pattern tower::ServiceExt::oneshot permet de tester un handler Axum sans lancer de serveur TCP :

use axum::{
    body::Body,
    http::{Request, StatusCode},
};
use tower::ServiceExt;

#[tokio::test]
async fn test_initiate_returns_201() {
    // Construire le router avec un MockDatabase injecté.
    let app = build_test_router(mock_db());

    let req = Request::builder()
        .method("POST")
        .uri("/api/v1/auth/login/fediverse/initiate")
        .header("content-type", "application/json")
        .body(Body::from(r#"{"handle":"@alice@a.tld"}"#))
        .unwrap();

    let response = app.oneshot(req).await.unwrap();
    assert_eq!(response.status(), StatusCode::CREATED);
}

Ce pattern évite de démarrer un port TCP et reste rapide. Il convient pour valider la structure des réponses, les codes de statut HTTP et la sérialisation JSON.


A.5 Tests e2e fédération

Fichier : crates/rustwarden-fediverse/tests/e2e_fediverse.rs (~2000 lignes)

Ce fichier couvre les scénarios complets entre trois instances :

ConstanteURLDomaine
ALPHAhttp://localhost:3001alpha.local
BETAhttp://localhost:3002beta.local
GAMMAhttp://localhost:3003gamma.local

Comportement en l'absence du testbed : chaque test appelle testbed_or_skip() qui sonde /health sur les trois instances. Si l'une est inaccessible, le test s'arrête proprement (return) sans panic!. Les tests sont donc transparents dans une CI standard.

Démarrer le testbed :

# Depuis la racine du projet
cd tests/fediverse-testbed
./scripts/setup.sh

# Puis lancer les tests e2e
cargo test -p rustwarden-fediverse --test e2e_fediverse

Exemples de scénarios couverts :

  • webfinger_resolves_local_user_on_each_instance — WebFinger retourne un JRD valide avec un lien self AP.
  • auth_challenge_full_flow — initiate → auth/inbox → consent → status → consume → JWT valide.
  • cross_instance_team_invite — invite depuis ALPHA vers @bob@beta.local → accept → membership créé.
  • inbox_rejects_invalid_signature — POST /inbox sans signature → 401.
  • inbox_rejects_oversized_body — corps > 1 MiB → 413.

A.6 Prérequis base de données pour les tests

Certains tests de rustwarden-core accèdent à une vraie base PostgreSQL. Sans base lancée, 31 tests environ échouent avec une erreur de connexion.

Procédure :

# Démarrer PostgreSQL via Docker Compose
cd /home/monsieurb/Documents/devel/Rustwarden/database
docker compose up -d

# Vérifier que la DB est prête
docker compose ps

# Lancer les migrations
cargo run --bin migrate

# Lancer les tests
cargo test --workspace

La DATABASE_URL doit être définie dans l'environnement ou dans un fichier .env à la racine :

DATABASE_URL=postgres://rustwarden:password@localhost:5432/rustwarden

Les tests qui n'utilisent que MockDatabase (majoritaires dans rustwarden-fediverse) fonctionnent sans base réelle.


A.7 Mocks vs base de données réelle

ContexteRecommandation
Tests unitaires de services métierMockDatabase SeaORM — rapide, pas de dépendance externe
Tests de logique de migrationBase PostgreSQL réelle — les migrations doivent être rejouées
Tests d'intégration HTTPMockDatabase suffit pour valider les handlers sans effet de bord
Tests e2e fédérationBase réelle + testbed Docker — le protocole AP nécessite des instances vraies
CI par défautMockDatabase — pas de Docker requis, tests en < 30 s
CI dédiée intégrationBase PostgreSQL dans service Docker Compose

A.8 Convention de nommage des tests

Le projet suit le pattern test_<feature>_<scénario>_<attente> :

// Bon : précis, lisible sans contexte
#[tokio::test]
async fn test_consume_returns_error_when_challenge_not_approved() { }

#[tokio::test]
async fn test_signature_header_rejects_missing_algorithm() { }

#[test]
fn test_trust_mode_parse_defaults_to_allowlist_on_unknown_value() { }
// À éviter : trop vague
#[tokio::test]
async fn test_consume() { }

A.9 Couverture de code

Aucun seuil de couverture n'est imposé actuellement. Pour mesurer :

# Avec cargo-tarpaulin (Linux uniquement)
cargo install cargo-tarpaulin
cargo tarpaulin --workspace --out Html

# Avec cargo-llvm-cov (multi-plateforme)
cargo install cargo-llvm-cov
cargo llvm-cov --workspace --html

L'objectif V2 est de définir un seuil minimum par crate (proposition : 70 % pour rustwarden-core, 60 % pour rustwarden-fediverse).


A.10 CI

La chaîne CI standard exécute :

# Compilation sans erreur
cargo build --workspace

# Tests (sans la feature redis-session — elle ne compile pas)
cargo test --workspace

# Lint zéro warning
cargo clippy --workspace -- -D warnings

# Documentation compilable
cargo doc --workspace --no-deps -D warnings

Attention : ne pas utiliser --all-features. La feature redis-session de rustwarden-core ne compile pas actuellement. Voir la note dans docs/ sur la feature cassée.


Partie B — Observabilité

B.1 État actuel (2026-05-17)

AspectSituation actuelle
Bibliothèque de tracestracing utilisé (394 appels dans les crates, 5 dans app/)
#[instrument]0 usage — aucun span structuré automatique
OpenTelemetryAbsent
Métriques PrometheusAbsentes
Endpoint /healthPrésent, retourne {"status":"ok"} sans vérifier la DB ni le worker
Format des logsTexte brut (format tracing_subscriber par défaut)
Logs JSONAbsents
Audit trailauth_logging service du core — présent pour les événements d'authentification

Les 394 appels tracing:: couvrent des cas utiles (tracing::warn! dans les services clés, tracing::info! sur les livraisons outbound) mais sans spans imbriqués, la corrélation entre une requête entrante et ses effets de bord est impossible.


B.2 Architecture cible V2

flowchart LR
    APP["Application Rustwarden\n(tracing-subscriber\n+ tracing-opentelemetry)"]

    APP -->|OTLP gRPC| OTEL_COL["OpenTelemetry Collector"]

    OTEL_COL -->|traces| TEMPO["Tempo"]
    OTEL_COL -->|logs| LOKI["Loki"]
    OTEL_COL -->|métriques| MIMIR["Mimir / Prometheus"]

    TEMPO --> GRAFANA["Grafana"]
    LOKI --> GRAFANA
    MIMIR --> GRAFANA

    APP -->|/metrics scrape| PROM["Prometheus (scrape direct)"]
    PROM --> GRAFANA

B.3 Recommandations V2 — Tracing structuré

Priorité 1 : ajouter #[tracing::instrument] sur les méthodes de services critiques.

use tracing::instrument;

impl AuthService {
    #[instrument(
        skip(db, password),          // Ne jamais tracer les secrets
        fields(user_id, ip_address)  // Champs enrichis dynamiquement
    )]
    pub async fn login(
        db: &DatabaseConnection,
        username: &str,
        password: &str,
    ) -> Result<LoginResult, AppError> {
        // tracing::Span::current().record("user_id", &id.to_string());
        todo!()
    }
}

Services prioritaires (dans l'ordre) :

  1. AuthService::login, AuthService::logout, AuthService::refresh_token
  2. JwtService::mint, JwtService::validate
  3. TeamService::add_member, TeamService::remove_member
  4. TotpService::verify
  5. FediverseAuthService::initiate, FediverseAuthService::consume
  6. FediverseOutboundService::drain_one

Niveaux recommandés :

NiveauUsage
ERRORPanic-worthy : corruption de données, secret manquant, DB unreachable
WARNAnomalie récupérable : signature échouée, retry outbound, token expiré
INFOTransactions métier : login réussi, invitation créée, équipe modifiée
DEBUGDétails de flux : état du challenge, payload sérialisé, clé résolue
TRACEVerbose : chaque header inspecté, chaque champ DB lu

B.4 Recommandations V2 — Métriques Prometheus

Compteurs :

rustwarden_auth_login_total{result="ok|fail|2fa_required"}
rustwarden_auth_logout_total
rustwarden_jwt_blacklist_hits_total
rustwarden_totp_attempts_total{result="ok|fail"}
rustwarden_oauth_callback_total{provider, result}
rustwarden_fediverse_inbox_total{type="Update|Delete|Revoke|unknown", result}
rustwarden_fediverse_outbound_delivered_total{domain}
rustwarden_fediverse_outbound_dead_total{domain}
rustwarden_fediverse_signature_failures_total{domain, reason}
rustwarden_fediverse_auth_challenge_total{result="consumed|rejected|expired"}

Histogrammes :

rustwarden_http_request_duration_seconds{route, method, status}
rustwarden_fediverse_outbound_delivery_duration_seconds{domain}

Jauges :

rustwarden_active_sessions
rustwarden_email_queue_depth
rustwarden_fediverse_outbound_queue_depth{status="pending|failed"}
rustwarden_fediverse_trusted_domains_count

Exposition : nouvel endpoint /metrics (format texte Prometheus), protégé par middleware admin ou par allowlist IP. Ne jamais exposer /metrics sur l'interface publique sans authentification.

Crates recommandées :

[dependencies]
metrics = "0.23"
metrics-exporter-prometheus = "0.15"

B.5 Recommandations V2 — Health endpoints

Remplacer le /health minimal par deux endpoints distincts :

EndpointTypeComportement
/healthzLivenessRetourne 200 si le processus répond. Aucune vérification de dépendance.
/readyzReadinessVérifie DB (ping SeaORM), session store, worker outbound actif. Retourne 503 si une dépendance est KO.

Format JSON cible pour /readyz :

{
  "status": "ok",
  "checks": {
    "database": { "status": "ok", "latency_ms": 3 },
    "session_store": { "status": "ok" },
    "outbound_worker": { "status": "ok", "queue_depth": 0 },
    "email_queue": { "status": "ok", "depth": 2 }
  }
}

En cas de dépendance KO :

{
  "status": "degraded",
  "checks": {
    "database": { "status": "error", "error": "connection refused" },
    "session_store": { "status": "ok" }
  }
}

B.6 Recommandations V2 — Logs JSON structurés

En production, activer le format JSON dans tracing_subscriber :

use tracing_subscriber::fmt;

fn init_logging() {
    if std::env::var("RUST_LOG_FORMAT").as_deref() == Ok("json") {
        fmt()
            .json()
            .with_target(true)
            .with_thread_ids(false)
            .init();
    } else {
        fmt().compact().init();
    }
}

Champs obligatoires par ligne de log :

ChampTypeNote
timestampRFC3339Généré par tracing_subscriber
levelstringERROR/WARN/INFO/DEBUG/TRACE
targetstringModule Rust (rustwarden::auth::service)
messagestringMessage lisible
request_idUUIDInjecté par middleware en tête de requête
user_idUUID (opt.)Uniquement si l'utilisateur est authentifié

Règle PII : ne jamais logger d'adresse email en clair, de mot de passe, de token JWT ou de clé privée. Les champs sensibles sont soit omis, soit tronqués (4 premiers caractères + ***).


B.7 Trace d'une requête login avec OTel

sequenceDiagram
    participant MW as Middleware (span racine)
    participant HANDLER as Handler POST /login
    participant AUTH as AuthService::login
    participant BCRYPT as bcrypt::verify
    participant DB as SeaORM query
    participant HOOKS as RustwardenHooks::on_login

    MW->>HANDLER: span "http.server" {method, route, request_id}
    HANDLER->>AUTH: span "auth.login" {username_hash}
    AUTH->>DB: span "db.query" {table="users", op="find_by_username"}
    DB-->>AUTH: UserModel
    AUTH->>BCRYPT: span "crypto.bcrypt_verify"
    BCRYPT-->>AUTH: ok
    AUTH->>DB: span "db.query" {table="auth_logs", op="insert"}
    AUTH->>HOOKS: span "hooks.on_login" {user_id}
    HOOKS-->>AUTH: ok
    AUTH-->>HANDLER: LoginResult {jwt, refresh_token}
    HANDLER-->>MW: 200 OK
    Note over MW: Durée totale enregistrée dans l'histogramme<br/>rustwarden_http_request_duration_seconds

Chaque span enfant hérite du trace_id de la span racine HTTP. Dans Grafana Tempo, cela permet de naviguer depuis une ligne de log vers la trace complète.


B.8 Outils recommandés

BesoinCrate
Traces structuréestracing + tracing-subscriber
Export OTLP vers Collectortracing-opentelemetry + opentelemetry-otlp
Métriques Prometheusmetrics + metrics-exporter-prometheus
Logs JSONtracing-subscriber avec .json()

Exemple de configuration V2 :

[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing-opentelemetry = "0.24"
opentelemetry-otlp = { version = "0.17", features = ["grpc-tonic"] }
metrics = "0.23"
metrics-exporter-prometheus = "0.15"
use opentelemetry_otlp::WithExportConfig;
use tracing_subscriber::{layer::SubscriberExt, Registry};

fn init_telemetry(otlp_endpoint: &str) -> anyhow::Result<()> {
    let tracer = opentelemetry_otlp::new_pipeline()
        .tracing()
        .with_exporter(
            opentelemetry_otlp::new_exporter()
                .tonic()
                .with_endpoint(otlp_endpoint),
        )
        .install_batch(opentelemetry_sdk::runtime::Tokio)?;

    let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
    let fmt_layer = tracing_subscriber::fmt::layer().json();

    let subscriber = Registry::default()
        .with(tracing_subscriber::EnvFilter::from_default_env())
        .with(otel_layer)
        .with(fmt_layer);

    tracing::subscriber::set_global_default(subscriber)?;
    Ok(())
}

B.9 Audit et logs de conformité

Le service auth_logging de rustwarden-core est déjà opérationnel. Il enregistre les événements d'authentification (login, logout, échec, 2FA) dans la table auth_logs.

Événements à ajouter en V2 pour couvrir les actions admin :

ÉvénementTableDéclencheur
Création d'utilisateuraudit_logsUserService::create
Modification de rôleaudit_logsRbacService::assign_role
Suppression d'utilisateuraudit_logsUserService::delete
Ajout à une équipeaudit_logsTeamService::add_member
Révocation d'un tokenaudit_logsJwtBlacklistService::blacklist
Delete{Actor} AP reçuaudit_logsFediverseSignalService::apply_delete_actor_observed

Le format cible est une table audit_logs avec colonnes id, actor_user_id, action, target_type, target_id, details_json, created_at.


B.10 Référence roadmap

Les améliorations d'observabilité sont suivies dans docs/V2_ROADMAP.md.

La priorité recommandée est :

  1. Instrumenter AuthService et JwtService avec #[instrument].
  2. Activer les logs JSON en production via variable d'environnement RUST_LOG_FORMAT=json.
  3. Implémenter /readyz avec vérification DB.
  4. Ajouter les métriques Prometheus sur le chemin d'authentification.
  5. Brancher tracing-opentelemetry + Collector OTLP.
  6. Compléter les audit logs sur les actions admin.