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>
225 lines
6.8 KiB
Rust
225 lines
6.8 KiB
Rust
use axum::body::Body;
|
|
use axum::http::{Request, StatusCode};
|
|
use axum::Router;
|
|
use dotenv::dotenv;
|
|
use htycommons::common::current_local_datetime;
|
|
use htycommons::jwt::jwt_encode_token;
|
|
use htycommons::redis_util::{get_token_expiration_days, save_token_with_exp_days};
|
|
use htycommons::web::HtyToken;
|
|
use http_body_util::BodyExt;
|
|
use serde_json::{json, Value};
|
|
use tower::util::ServiceExt;
|
|
|
|
fn setup() -> Router {
|
|
dotenv().ok();
|
|
std::env::set_var("POOL_SIZE", "1");
|
|
let db_url = std::env::var("KC_DB_URL")
|
|
.unwrap_or_else(|_| "postgres://htykc:htykc@localhost:5432/htykc_test".to_string());
|
|
htykc::clazz_router(&db_url)
|
|
}
|
|
|
|
fn build_test_token(with_org_context: bool) -> String {
|
|
std::env::set_var("JWT_KEY", "htykc-e2e-test-jwt-key");
|
|
let token = HtyToken {
|
|
token_id: "test-token-id".to_string(),
|
|
hty_id: Some("teacher-1".to_string()),
|
|
app_id: Some("app-1".to_string()),
|
|
ts: current_local_datetime(),
|
|
roles: None,
|
|
tags: None,
|
|
current_org_id: if with_org_context {
|
|
Some("org-1".to_string())
|
|
} else {
|
|
None
|
|
},
|
|
current_org_role_keys: Some(vec!["TEACHER".to_string()]),
|
|
current_department_id: None,
|
|
};
|
|
jwt_encode_token(token).expect("encode test token")
|
|
}
|
|
|
|
fn prepare_verified_token(with_org_context: bool) -> Option<String> {
|
|
let token = build_test_token(with_org_context);
|
|
let decoded = htycommons::jwt::jwt_decode_token(&token.clone()).ok()?;
|
|
let expiration_days = get_token_expiration_days().ok()?;
|
|
if save_token_with_exp_days(&decoded, expiration_days).is_err() {
|
|
return None;
|
|
}
|
|
Some(token)
|
|
}
|
|
|
|
async fn get_with_headers(router: &Router, uri: &str, headers: Vec<(&str, &str)>) -> (StatusCode, Value) {
|
|
let mut request = Request::builder().method("GET").uri(uri);
|
|
for (key, value) in headers {
|
|
request = request.header(key, value);
|
|
}
|
|
let response = router
|
|
.clone()
|
|
.oneshot(request.body(Body::empty()).expect("build request"))
|
|
.await
|
|
.expect("send request");
|
|
let status = response.status();
|
|
let body_bytes = response
|
|
.into_body()
|
|
.collect()
|
|
.await
|
|
.expect("collect body")
|
|
.to_bytes();
|
|
let json_body: Value = serde_json::from_slice(&body_bytes).unwrap_or(Value::Null);
|
|
(status, json_body)
|
|
}
|
|
|
|
async fn post_with_headers(
|
|
router: &Router,
|
|
uri: &str,
|
|
body: Value,
|
|
headers: Vec<(&str, &str)>,
|
|
) -> (StatusCode, Value) {
|
|
let mut request = Request::builder()
|
|
.method("POST")
|
|
.uri(uri)
|
|
.header("Content-Type", "application/json");
|
|
for (key, value) in headers {
|
|
request = request.header(key, value);
|
|
}
|
|
let response = router
|
|
.clone()
|
|
.oneshot(
|
|
request
|
|
.body(Body::from(body.to_string()))
|
|
.expect("build request"),
|
|
)
|
|
.await
|
|
.expect("send request");
|
|
let status = response.status();
|
|
let body_bytes = response
|
|
.into_body()
|
|
.collect()
|
|
.await
|
|
.expect("collect body")
|
|
.to_bytes();
|
|
let json_body: Value = serde_json::from_slice(&body_bytes).unwrap_or(Value::Null);
|
|
(status, json_body)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_teacher_hour_stats_requires_current_org_id() {
|
|
let router = match std::panic::catch_unwind(setup) {
|
|
Ok(ok) => ok,
|
|
Err(_) => {
|
|
println!("DB unavailable in current test env, skip stats test");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let token = match prepare_verified_token(false) {
|
|
Some(ok) => ok,
|
|
None => {
|
|
println!("Redis unavailable in current test env, skip stats test");
|
|
return;
|
|
}
|
|
};
|
|
let (status, response) = get_with_headers(
|
|
&router,
|
|
"/api/v1/clazz/stats/my-hours?start_date=2026-01-01%2000:00:00&end_date=2026-12-31%2023:59:59",
|
|
vec![
|
|
("Authorization", token.as_str()),
|
|
("HtySudoerToken", token.as_str()),
|
|
("HtyHost", "root"),
|
|
],
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(status, StatusCode::OK, "stats request should return business response envelope");
|
|
assert!(
|
|
!response["r"].as_bool().unwrap_or(true),
|
|
"stats should fail without current_org_id, body: {:?}",
|
|
response
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_create_clazz_leave_requires_current_org_id() {
|
|
let router = match std::panic::catch_unwind(setup) {
|
|
Ok(ok) => ok,
|
|
Err(_) => {
|
|
println!("DB unavailable in current test env, skip leave test");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let token = match prepare_verified_token(false) {
|
|
Some(ok) => ok,
|
|
None => {
|
|
println!("Redis unavailable in current test env, skip leave test");
|
|
return;
|
|
}
|
|
};
|
|
let (status, response) = post_with_headers(
|
|
&router,
|
|
"/api/v1/clazz/leave/create",
|
|
json!({
|
|
"clazz_id": "clazz-1",
|
|
"student_id": "student-1",
|
|
"teacher_id": "teacher-1",
|
|
"leave_type": "PERSONAL",
|
|
"reason": "test leave request"
|
|
}),
|
|
vec![
|
|
("Authorization", token.as_str()),
|
|
("HtySudoerToken", token.as_str()),
|
|
("HtyHost", "root"),
|
|
],
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(status, StatusCode::OK, "leave request should return business response envelope");
|
|
assert!(
|
|
!response["r"].as_bool().unwrap_or(true),
|
|
"create leave should fail without current_org_id, body: {:?}",
|
|
response
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_teacher_hour_stats_with_current_org_id_should_not_fail_on_org_context_check() {
|
|
let router = match std::panic::catch_unwind(setup) {
|
|
Ok(ok) => ok,
|
|
Err(_) => {
|
|
println!("DB unavailable in current test env, skip stats positive test");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let token = match prepare_verified_token(true) {
|
|
Some(ok) => ok,
|
|
None => {
|
|
println!("Redis unavailable in current test env, skip stats positive test");
|
|
return;
|
|
}
|
|
};
|
|
let (status, response) = get_with_headers(
|
|
&router,
|
|
"/api/v1/clazz/stats/my-hours?start_date=2026-01-01%2000:00:00&end_date=2026-12-31%2023:59:59",
|
|
vec![
|
|
("Authorization", token.as_str()),
|
|
("HtySudoerToken", token.as_str()),
|
|
("HtyHost", "root"),
|
|
],
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(status, StatusCode::OK, "stats request should return business response envelope");
|
|
|
|
let error_text = response
|
|
.get("e")
|
|
.and_then(|error_node| error_node.as_str())
|
|
.unwrap_or_default()
|
|
.to_string();
|
|
assert!(
|
|
!error_text.contains("current_org_id is required"),
|
|
"stats with org context should not hit org-context-missing error, body: {:?}",
|
|
response
|
|
);
|
|
}
|