//! HTTP e2e against a real Postgres + Redis (see `huiwing/docker-compose.ts-e2e.yml`). //! Run: `huiwing/scripts/run-ts-e2e.sh` (starts compose, truncates data, runs this test). use std::sync::Arc; use std::time::Duration; use axum::serve; use chrono::Utc; use htycommons::db::pool; use htycommons::jwt::jwt_encode_token; use htycommons::web::HtyToken; use htyts::redis_store::RedisTaskStore; use htyts::state::TsState; use htyts::ts_router_with_state; use serde_json::{json, Value}; use tokio::net::TcpListener; fn ensure_e2e_env_defaults() { if std::env::var("JWT_KEY").is_err() { std::env::set_var("JWT_KEY", "e2e-jwt-secret-key-minimum-32-chars!!"); } if std::env::var("REDIS_HOST").is_err() { std::env::set_var("REDIS_HOST", "127.0.0.1"); } if std::env::var("REDIS_PORT").is_err() { std::env::set_var("REDIS_PORT", "6390"); } if std::env::var("POOL_SIZE").is_err() { std::env::set_var("POOL_SIZE", "8"); } // Avoid calling real HTYUC in e2e; matches optional UC verify when unset in dev. if std::env::var("TOKEN_VERIFY").is_err() { std::env::set_var("TOKEN_VERIFY", "false"); } } fn redis_url() -> String { let host = std::env::var("REDIS_HOST").expect("REDIS_HOST"); let port = std::env::var("REDIS_PORT").expect("REDIS_PORT"); format!("redis://{host}:{port}") } /// Mint sudoer JWT (`TOKEN_VERIFY=false` in e2e so `TS_SUDO_T_*` / UC are not required). fn mint_sudo_jwt() -> String { ensure_e2e_env_defaults(); let inner = HtyToken { token_id: "e2e_sudo".to_string(), hty_id: Some("hty-e2e-1".to_string()), app_id: None, ts: Utc::now().naive_utc(), roles: None, tags: None, }; jwt_encode_token(inner).expect("jwt_encode_token") } async fn spawn_ts_server() -> String { ensure_e2e_env_defaults(); let db_url = std::env::var("TS_DATABASE_URL").expect( "TS_DATABASE_URL must be set (run `huiwing/scripts/run-ts-e2e.sh` or docker-compose.ts-e2e)", ); let pg = pool(&db_url); let redis = RedisTaskStore::connect(&redis_url()) .await .expect("RedisTaskStore::connect"); let st = Arc::new(TsState { db: Arc::new(pg), redis, zombie_minutes: std::env::var("ZOMBIE_MIN") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(10), }); let app = ts_router_with_state(st); let listener = TcpListener::bind("127.0.0.1:0") .await .expect("bind ephemeral port"); let addr = listener.local_addr().expect("local_addr"); tokio::spawn(async move { if let Err(e) = serve(listener, app).await { eprintln!("htyts e2e server error: {e}"); } }); tokio::time::sleep(Duration::from_millis(150)).await; format!("http://127.0.0.1:{}", addr.port()) } #[tokio::test] async fn htyts_create_get_pending_update_delete_flow() { let sudo = mint_sudo_jwt(); let base = spawn_ts_server().await; let client = reqwest::Client::builder() .timeout(Duration::from_secs(60)) .build() .expect("reqwest client"); let hello = client .get(format!("{base}/api/v1/ts")) .send() .await .expect("hello"); assert_eq!(hello.status(), reqwest::StatusCode::OK); assert_eq!(hello.text().await.expect("body"), "-=Task Server=-"); let create_body = json!({ "task_type": "NOOP", "task_status": "PENDING", "payload": { "e2e": "note" } }); let create = client .post(format!("{base}/api/v1/ts/create_task")) .header("HtyHost", "e2e.test.local") .header("HtySudoerToken", &sudo) .json(&create_body) .send() .await .expect("create_task"); assert_eq!(create.status(), reqwest::StatusCode::CREATED); let created: Value = create.json().await.expect("create json"); assert_eq!(created["r"], true); let task_id = created["d"] .as_str() .expect("task id string") .to_string(); let get = client .get(format!("{base}/api/v1/ts/task/{task_id}")) .send() .await .expect("get task"); assert_eq!(get.status(), reqwest::StatusCode::OK); let got: Value = get.json().await.expect("get json"); assert_eq!(got["r"], true); let task = &got["d"]; assert_eq!(task["task_type"], "NOOP"); assert_eq!(task["task_status"], "PENDING"); assert!(task["payload"]["e2e"].is_string()); let st = client .get(format!("{base}/api/v1/ts/task_status/{task_id}")) .send() .await .expect("task_status"); assert_eq!(st.status(), reqwest::StatusCode::OK); let stj: Value = st.json().await.expect("status json"); assert_eq!(stj["d"], "PENDING"); let pend = client .get(format!("{base}/api/v1/ts/one_pending_task")) .send() .await .expect("one_pending"); assert_eq!(pend.status(), reqwest::StatusCode::OK); let pj: Value = pend.json().await.expect("pending json"); assert_eq!(pj["r"], true); assert_eq!(pj["d"]["task_id"], task_id); let update_body = json!({ "task_id": task_id, "task_type": "NOOP", "task_status": "DONE" }); let upd = client .post(format!("{base}/api/v1/ts/update_task")) .header("HtyHost", "e2e.test.local") .header("HtySudoerToken", &sudo) .json(&update_body) .send() .await .expect("update_task"); assert_eq!(upd.status(), reqwest::StatusCode::OK); let del = client .get(format!("{base}/api/v1/ts/del_task/{task_id}")) .send() .await .expect("del_task"); assert_eq!(del.status(), reqwest::StatusCode::OK); let dj: Value = del.json().await.expect("del json"); assert_eq!(dj["r"], true); }