use std::sync::Arc;
use std::time::Duration;
use gradatum_acl_policy::AclEngine;
use gradatum_auth::jwt::{JwtService, TokenScope};
use gradatum_core::scope::VaultId;
use gradatum_curator::CuratorPipeline;
use gradatum_db_sqlite::{run_migrations, SqliteQueueStore};
use gradatum_queue::{Queue as _, SqliteQueue};
use gradatum_server::audit_jsonl::JsonlFileSink;
use gradatum_server::middleware::auth_middleware;
use gradatum_server::{api_v1, state::AppState};
use gradatum_vault::Vault;
use gradatum_worker::dispatch::{Dispatcher, NoopAuditSink};
use serde_json::Value;
use sqlx::sqlite::SqlitePoolOptions;
use tempfile::TempDir;
const TEST_ACL_WRITE: &str = r#"
[[consumer]]
identity = "test-e2e-writer"
read_patterns = ["**"]
write_patterns = ["**"]
"#;
fn client() -> reqwest::Client {
reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.expect("construction client HTTP e2e")
}
#[tokio::test]
async fn write_curator_audit_re_read_chain() {
let data_dir = TempDir::new().expect("tempdir data e2e");
let audit_dir = TempDir::new().expect("tempdir audit e2e");
let vault_path = data_dir.path().join("vault");
let vault = Arc::new(
Vault::create(&vault_path, VaultId::new("main"))
.await
.expect("Vault::create e2e"),
);
let queue_path = data_dir.path().join("queue.db");
let queue = Arc::new(
SqliteQueue::new(&queue_path)
.await
.expect("SqliteQueue::new e2e"),
);
let audit_sink = Arc::new(JsonlFileSink::new(audit_dir.path().to_path_buf()));
let jwt = JwtService::new_ephemeral();
let bearer = jwt
.sign(
"test-e2e-writer",
&["read".to_string(), "write".to_string()],
TokenScope::Service,
"main",
)
.expect("bearer e2e");
let acl = AclEngine::from_preset_str(TEST_ACL_WRITE).expect("ACL e2e");
let jobs_pool = SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.expect("jobs pool in-memory — invariant test e2e_write");
run_migrations(&jobs_pool)
.await
.expect("migrations gradatum_jobs — invariant test e2e_write");
let job_store = Arc::new(SqliteQueueStore::new(jobs_pool.clone()));
let state = AppState::with_jwt_and_acl(jwt, acl)
.with_queue(Arc::clone(&queue) as Arc<dyn gradatum_queue::Queue>)
.with_job_store(job_store as Arc<dyn gradatum_core::QueueStore>, jobs_pool)
.with_vault_arc(Arc::clone(&vault) as Arc<dyn gradatum_vault::Registry>)
.with_audit(Arc::new(NoopAuditSink));
use axum::{middleware, Router};
let app = Router::new()
.nest("/api/v1", api_v1::router())
.layer(middleware::from_fn_with_state(
state.clone(),
auth_middleware,
))
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind e2e");
let addr = listener.local_addr().expect("addr e2e");
tokio::spawn(async move { axum::serve(listener, app).await.expect("serveur e2e") });
tokio::time::sleep(Duration::from_millis(50)).await;
let title = "[DEBUG] E2E test bug";
let resp = client()
.post(format!("http://{addr}/api/v1/vault_write"))
.bearer_auth(&bearer)
.json(&serde_json::json!({
"title": title,
"body": "Ce test valide la chaîne write→curator→audit dans T9 P2.0c.",
"tenant_id": "main"
}))
.send()
.await
.expect("POST vault_write e2e");
assert_eq!(resp.status(), 202, "vault_write doit retourner 202");
let body: Value = resp.json().await.expect("body 202 e2e");
let job_id = body["job_id"]
.as_str()
.expect("job_id doit être une string ULID Phase 1.2");
assert!(!job_id.is_empty(), "job_id ne doit pas être vide");
let depth_before = queue.depth().await.expect("depth avant dispatch");
assert_eq!(
depth_before, 0,
"queue legacy jobs_v2 doit rester vide après vault_write Phase 1.2 (job va dans gradatum_jobs)"
);
let curator = Arc::new(CuratorPipeline::heuristic());
let dispatcher = Dispatcher::new(Arc::clone(&queue))
.with_vault(Arc::clone(&vault))
.with_curator(curator)
.with_audit(Arc::clone(&audit_sink) as Arc<dyn gradatum_core::audit::http::AuditSink>);
let processed = dispatcher
.run_once()
.await
.expect("Dispatcher::run_once e2e");
assert!(
!processed,
"run_once doit retourner false (jobs_v2 vide — vault_write va dans gradatum_jobs Phase 1.2)"
);
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
let audit_path = audit_dir.path().join(format!("audit.{today}.jsonl"));
if audit_path.exists() {
let audit_content =
std::fs::read_to_string(&audit_path).expect("lecture fichier audit worker e2e");
assert!(
audit_content.is_empty(),
"fichier audit worker doit être vide Phase 1.2 (aucun job traité Dispatcher legacy)"
);
}
let depth_after = queue.depth().await.expect("depth après run_once vide");
assert_eq!(
depth_after, 0,
"queue legacy jobs_v2 doit rester vide après vault_write Phase 1.2"
);
}