fa14a5ca8c
- 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>
185 lines
5.8 KiB
Rust
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);
|
|
}
|