//! HTYTS + AuthCore HTYUC 联调:需已启动 `htyuc`(`HTYUC_URL`),且 `JWT_KEY` 与 UC 一致。 //! UC `verify_jwt` 要求 JWT 已在 UC Redis 中(与 `token_id` 一致),故通过 `login_with_password` 取令牌(数据见 AuthCore `htyuc/tests/fixtures/init_test_data.sql`)。 //! 在 CI 中由 `.github/workflows/htyts-authcore-weekly.yml` 构建并启动 UC 后执行;不在默认 PR workflow 中跑。 use std::sync::Arc; use std::time::Duration; use axum::serve; use htycommons::db::pool; use htyts::redis_store::RedisTaskStore; use htyts::state::TsState; use htyts::ts_router_with_state; use serde_json::{json, Value}; use tokio::net::TcpListener; /// 与 AuthCore `rust.yml` e2e 及 UC 进程保持一致,便于 `verify_jwt_token` 校验同一密钥。 const JWT_KEY_AUTHCORE_E2E: &str = "test_jwt_key_for_testing_only_1234567890"; /// 与 `init_test_data.sql` 中 `hty_apps.domain` 一致,登录与后续请求的 `HtyHost` 须相同。 const UC_E2E_HTY_HOST: &str = "root"; /// 具备 `SYS_CAN_SUDO` 的测试用户(fixture)。 const UC_E2E_SUDO_USER: &str = "sudouser"; const UC_E2E_SUDO_PASS: &str = "sudopass"; fn ensure_authcore_joint_env() { std::env::var("HTYUC_URL").expect( "HTYUC_URL must point to running HTYUC (e.g. http://127.0.0.1:18080). \ See huiwing/scripts/ci-authcore-weekly.sh or the weekly GitHub workflow.", ); std::env::set_var("JWT_KEY", JWT_KEY_AUTHCORE_E2E); std::env::set_var("TOKEN_VERIFY", "true"); std::env::set_var("AUTH_CHECKING", "true"); 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", "6379"); } if std::env::var("POOL_SIZE").is_err() { std::env::set_var("POOL_SIZE", "8"); } if std::env::var("TS_DOMAIN").is_err() { std::env::set_var("TS_DOMAIN", "e2e.test.local"); } } 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}") } async fn fetch_sudoer_jwt_via_uc_login() -> String { ensure_authcore_joint_env(); let uc_base = std::env::var("HTYUC_URL").expect("HTYUC_URL"); let client = reqwest::Client::builder() .timeout(Duration::from_secs(120)) .build() .expect("reqwest client"); let login_body = json!({ "username": UC_E2E_SUDO_USER, "password": UC_E2E_SUDO_PASS, }); let resp = client .post(format!( "{}/api/v1/uc/login_with_password", uc_base.trim_end_matches('/') )) .header("HtyHost", UC_E2E_HTY_HOST) .json(&login_body) .send() .await .expect("login_with_password"); let status = resp.status(); let text = resp.text().await.unwrap_or_default(); assert!( status.is_success(), "login_with_password http={status} body={text}" ); let v: Value = serde_json::from_str(&text).expect("login json"); assert!( v["r"].as_bool().unwrap_or(false), "login r=false body={text}" ); v["d"] .as_str() .expect("login token in d") .to_string() } async fn spawn_ts_server() -> String { ensure_authcore_joint_env(); let db_url = std::env::var("TS_DATABASE_URL").expect("TS_DATABASE_URL"); 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 authcore e2e server error: {e}"); } }); tokio::time::sleep(Duration::from_millis(200)).await; format!("http://127.0.0.1:{}", addr.port()) } /// 首次 `create_task` 在缓存未命中时应走 UC `POST /api/v1/uc/verify_jwt_token`(与 `check_auth` 一致)。 /// 默认 `cargo test` 跳过;周更 CI 与本地联调使用 `cargo test -p htyts --test ts_e2e_authcore_http -- --ignored`。 #[tokio::test] #[ignore = "needs HTYUC_URL + running HTYUC (AuthCore); see scripts/ci-authcore-weekly.sh"] async fn create_task_with_uc_verify_jwt_token() { let base = spawn_ts_server().await; let sudo = fetch_sudoer_jwt_via_uc_login().await; let client = reqwest::Client::builder() .timeout(Duration::from_secs(120)) .build() .expect("reqwest client"); let create_body = json!({ "task_type": "NOOP", "task_status": "PENDING", "payload": { "e2e_authcore": "note" } }); let create = client .post(format!("{base}/api/v1/ts/create_task")) .header("HtyHost", UC_E2E_HTY_HOST) .header("HtySudoerToken", &sudo) .json(&create_body) .send() .await .expect("create_task"); let status = create.status(); let body_text = create.text().await.unwrap_or_default(); assert_eq!( status, reqwest::StatusCode::CREATED, "create_task (cache miss → UC verify) body={}", body_text ); let created: Value = serde_json::from_str(&body_text).expect("create json"); assert_eq!(created["r"], true); let create2 = client .post(format!("{base}/api/v1/ts/create_task")) .header("HtyHost", UC_E2E_HTY_HOST) .header("HtySudoerToken", &sudo) .json(&json!({ "task_type": "NOOP", "task_status": "PENDING", "payload": { "e2e_authcore": "second_same_sudo_cache" } })) .send() .await .expect("create_task 2"); let status2 = create2.status(); let body2 = create2.text().await.unwrap_or_default(); assert_eq!( status2, reqwest::StatusCode::CREATED, "second create (Redis sudo cache hit) body={}", body2 ); }