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 :
| Type | Annotation | Quantité |
|---|---|---|
| Tests unitaires synchrones | #[test] | 239 |
| Tests asynchrones (Tokio) | #[tokio::test] | 299 |
| Total | 538 |
Stack de test :
tokio::testpour tout le code asynchrone (services SeaORM, handlers Axum).sea-ormMockDatabase pour les tests unitaires de services sans base réelle.axum::Router+tower::ServiceExt::oneshotpour 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 :
| Constante | URL | Domaine |
|---|---|---|
ALPHA | http://localhost:3001 | alpha.local |
BETA | http://localhost:3002 | beta.local |
GAMMA | http://localhost:3003 | gamma.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 lienselfAP.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
| Contexte | Recommandation |
|---|---|
| Tests unitaires de services métier | MockDatabase SeaORM — rapide, pas de dépendance externe |
| Tests de logique de migration | Base PostgreSQL réelle — les migrations doivent être rejouées |
| Tests d'intégration HTTP | MockDatabase suffit pour valider les handlers sans effet de bord |
| Tests e2e fédération | Base réelle + testbed Docker — le protocole AP nécessite des instances vraies |
| CI par défaut | MockDatabase — pas de Docker requis, tests en < 30 s |
| CI dédiée intégration | Base 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 featureredis-sessionderustwarden-corene compile pas actuellement. Voir la note dansdocs/sur la feature cassée.
Partie B — Observabilité
B.1 État actuel (2026-05-17)
| Aspect | Situation actuelle |
|---|---|
| Bibliothèque de traces | tracing utilisé (394 appels dans les crates, 5 dans app/) |
#[instrument] | 0 usage — aucun span structuré automatique |
| OpenTelemetry | Absent |
| Métriques Prometheus | Absentes |
Endpoint /health | Présent, retourne {"status":"ok"} sans vérifier la DB ni le worker |
| Format des logs | Texte brut (format tracing_subscriber par défaut) |
| Logs JSON | Absents |
| Audit trail | auth_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) :
AuthService::login,AuthService::logout,AuthService::refresh_tokenJwtService::mint,JwtService::validateTeamService::add_member,TeamService::remove_memberTotpService::verifyFediverseAuthService::initiate,FediverseAuthService::consumeFediverseOutboundService::drain_one
Niveaux recommandés :
| Niveau | Usage |
|---|---|
ERROR | Panic-worthy : corruption de données, secret manquant, DB unreachable |
WARN | Anomalie récupérable : signature échouée, retry outbound, token expiré |
INFO | Transactions métier : login réussi, invitation créée, équipe modifiée |
DEBUG | Détails de flux : état du challenge, payload sérialisé, clé résolue |
TRACE | Verbose : 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 :
| Endpoint | Type | Comportement |
|---|---|---|
/healthz | Liveness | Retourne 200 si le processus répond. Aucune vérification de dépendance. |
/readyz | Readiness | Vé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 :
| Champ | Type | Note |
|---|---|---|
timestamp | RFC3339 | Généré par tracing_subscriber |
level | string | ERROR/WARN/INFO/DEBUG/TRACE |
target | string | Module Rust (rustwarden::auth::service) |
message | string | Message lisible |
request_id | UUID | Injecté par middleware en tête de requête |
user_id | UUID (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
| Besoin | Crate |
|---|---|
| Traces structurées | tracing + tracing-subscriber |
| Export OTLP vers Collector | tracing-opentelemetry + opentelemetry-otlp |
| Métriques Prometheus | metrics + metrics-exporter-prometheus |
| Logs JSON | tracing-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énement | Table | Déclencheur |
|---|---|---|
| Création d'utilisateur | audit_logs | UserService::create |
| Modification de rôle | audit_logs | RbacService::assign_role |
| Suppression d'utilisateur | audit_logs | UserService::delete |
| Ajout à une équipe | audit_logs | TeamService::add_member |
| Révocation d'un token | audit_logs | JwtBlacklistService::blacklist |
| Delete{Actor} AP reçu | audit_logs | FediverseSignalService::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 :
- Instrumenter
AuthServiceetJwtServiceavec#[instrument]. - Activer les logs JSON en production via variable d'environnement
RUST_LOG_FORMAT=json. - Implémenter
/readyzavec vérification DB. - Ajouter les métriques Prometheus sur le chemin d'authentification.
- Brancher
tracing-opentelemetry+ Collector OTLP. - Compléter les audit logs sur les actions admin.
GitRust