Files
huike-back/htyts/tests/ts_e2e_http.rs
T
weli fa14a5ca8c perf: add GIN indexes on clazz JSONB columns and date-range indexes
- GIN indexes on students/teachers (jsonb_path_ops) for fast user lookups
- Composite index on clazz (is_repeat, start_from, end_by) for date range queries
- Indexes on clazz_repeat (clazz_id, repeat_start, repeat_end) for joins

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 09:39:36 +08:00

185 lines
5.8 KiB
Rust

//! 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,
current_org_id: None,
current_org_role_keys: None,
current_department_id: 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);
}