chore add core rust project files and diesel migrations
Track required workspace crates, scripts, and historical diesel migrations so the repository contains the complete runnable backend baseline. Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
//! 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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user