Files
Aether/apps/aether-gateway/src/tests/control/admin/system.rs
T

2228 lines
88 KiB
Rust
Raw Normal View History

use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
use aether_data::repository::auth::{
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeyExportRecord,
};
use aether_data::repository::auth_modules::{
InMemoryAuthModuleReadRepository, StoredOAuthProviderModuleConfig,
};
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
use aether_data::repository::global_models::InMemoryGlobalModelReadRepository;
use aether_data::repository::oauth_providers::InMemoryOAuthProviderRepository;
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
use aether_data::repository::proxy_nodes::InMemoryProxyNodeRepository;
2026-05-10 17:28:23 +08:00
use aether_data::repository::users::{
InMemoryUserReadRepository, StoredUserAuthRecord, UpsertUserGroupRecord, UserReadRepository,
};
use aether_data::repository::wallet::{InMemoryWalletRepository, StoredWalletSnapshot};
use aether_data_contracts::repository::global_models::StoredPublicGlobalModel;
use axum::body::Body;
use axum::routing::{any, delete, get, post, put};
use axum::{extract::Request, Router};
use http::StatusCode;
use serde_json::json;
use super::super::{
build_router_with_state, issue_test_admin_access_token, sample_admin_global_model,
sample_admin_provider_model, sample_endpoint, sample_key, sample_ldap_module_config,
sample_oauth_provider_config, sample_provider, sample_proxy_node,
sample_recent_key_rpm_candidate, start_server, AppState,
};
use crate::constants::{
GATEWAY_HEADER, TRUSTED_ADMIN_SESSION_ID_HEADER, TRUSTED_ADMIN_USER_ID_HEADER,
TRUSTED_ADMIN_USER_ROLE_HEADER,
};
use crate::data::GatewayDataState;
2026-05-22 15:54:51 +08:00
static SYSTEM_UPDATE_TEST_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
2026-05-20 22:02:47 +08:00
#[tokio::test]
async fn gateway_handles_admin_system_version_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/version",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/version"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert!(payload["version"]
.as_str()
.is_some_and(|value| !value.is_empty()));
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_check_update_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/check-update",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/check-update"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert!(payload["current_version"]
.as_str()
.is_some_and(|value| !value.is_empty()));
assert_eq!(payload["latest_version"], serde_json::Value::Null);
assert_eq!(payload["has_update"], json!(false));
2026-05-13 10:27:45 +08:00
assert_eq!(payload["error"], "测试环境未请求 GitHub Releases");
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_check_update_locally_with_bearer_admin_session() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/check-update",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let state = AppState::new().expect("gateway should build");
let access_token = issue_test_admin_access_token(&state, "device-admin-system").await;
let gateway = build_router_with_state(state);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/check-update"))
.header("authorization", format!("Bearer {access_token}"))
.header("x-client-device-id", "device-admin-system")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["has_update"], json!(false));
2026-05-13 10:27:45 +08:00
assert_eq!(payload["error"], "测试环境未请求 GitHub Releases");
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
2026-05-20 22:02:47 +08:00
#[tokio::test]
async fn gateway_handles_admin_system_update_capability_locally() {
2026-05-22 15:54:51 +08:00
let _lock = SYSTEM_UPDATE_TEST_MUTEX.lock().await;
2026-05-20 22:02:47 +08:00
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/update-capability",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/update-capability"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert!(payload["supported"].is_boolean());
assert!(payload["build_type"].is_string());
assert!(payload["task_status"].is_string());
2026-05-20 22:02:47 +08:00
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_prepares_admin_system_update_locally() {
2026-05-22 15:54:51 +08:00
let _lock = SYSTEM_UPDATE_TEST_MUTEX.lock().await;
2026-05-20 22:02:47 +08:00
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/prepare-update",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.post(format!("{gateway_url}/api/admin/system/prepare-update"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
2026-05-20 22:02:47 +08:00
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert!(payload["detail"].is_string());
2026-05-20 22:02:47 +08:00
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_rejects_admin_system_apply_update_without_prepared_version() {
2026-05-22 15:54:51 +08:00
let _lock = SYSTEM_UPDATE_TEST_MUTEX.lock().await;
2026-05-20 22:02:47 +08:00
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/apply-update",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.post(format!("{gateway_url}/api/admin/system/apply-update"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
2026-05-20 22:02:47 +08:00
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert!(payload["detail"].is_string());
2026-05-20 22:02:47 +08:00
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_rejects_admin_system_rollback_without_previous_release() {
2026-05-22 15:54:51 +08:00
let _lock = SYSTEM_UPDATE_TEST_MUTEX.lock().await;
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/rollback",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
2026-05-20 22:02:47 +08:00
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
2026-05-20 22:02:47 +08:00
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.post(format!("{gateway_url}/api/admin/system/rollback"))
2026-05-20 22:02:47 +08:00
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::PRECONDITION_REQUIRED);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert!(payload["detail"].is_string());
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_releases_locally() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/releases",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/releases"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert!(payload["current_version"]
2026-05-20 22:02:47 +08:00
.as_str()
.is_some_and(|value| !value.is_empty()));
assert!(payload["releases"].is_array());
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_rejects_admin_system_apply_update_with_nonexistent_version() {
2026-05-22 15:54:51 +08:00
let _lock = SYSTEM_UPDATE_TEST_MUTEX.lock().await;
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/apply-update",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.post(format!("{gateway_url}/api/admin/system/apply-update"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.header("content-type", "application/json")
.body(r#"{"version":"v99.99.99"}"#)
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert!(payload["detail"].is_string());
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_update_status_locally() {
2026-05-22 15:54:51 +08:00
let _lock = SYSTEM_UPDATE_TEST_MUTEX.lock().await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/update-status"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert!(payload["phase"].is_string());
2026-05-20 22:02:47 +08:00
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_aws_regions_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/aws-regions",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/aws-regions"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
let regions = payload["regions"]
.as_array()
.expect("regions should be array");
assert!(regions.len() > 10);
assert!(regions.iter().any(|value| value == "us-east-1"));
assert!(regions.iter().any(|value| value == "eu-west-1"));
assert!(regions.iter().any(|value| value == "ap-southeast-1"));
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_aws_regions_locally_with_bearer_admin_session() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/aws-regions",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let state = AppState::new().expect("gateway should build");
let access_token = issue_test_admin_access_token(&state, "device-admin-aws-regions").await;
let gateway = build_router_with_state(state);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/aws-regions"))
.header("authorization", format!("Bearer {access_token}"))
.header("x-client-device-id", "device-admin-aws-regions")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
let regions = payload["regions"]
.as_array()
.expect("regions should be array");
assert!(regions.iter().any(|value| value == "us-east-1"));
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_stats_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/stats",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![
sample_provider("provider-openai", "openai", 10),
sample_provider("provider-anthropic", "anthropic", 20)
.with_transport_fields(false, false, false, None, None, None, None, None, None),
],
vec![],
vec![],
));
let data_state =
GatewayDataState::with_provider_catalog_reader_for_tests(provider_catalog_repository);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(data_state),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/stats"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["users"]["total"], json!(0));
assert_eq!(payload["users"]["active"], json!(0));
assert_eq!(payload["providers"]["total"], json!(2));
assert_eq!(payload["providers"]["active"], json!(1));
assert_eq!(payload["api_keys"], json!(0));
assert_eq!(payload["requests"], json!(0));
2026-05-19 00:30:24 +08:00
assert_eq!(payload["usage_counter"]["status"], json!("idle"));
assert_eq!(payload["usage_counter"]["outbox_pending_rows"], json!(0));
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_settings_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/settings",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![sample_provider("provider-openai", "openai", 10)],
vec![],
vec![],
));
let data_state =
GatewayDataState::with_provider_catalog_reader_for_tests(provider_catalog_repository)
.with_system_config_values_for_tests(vec![(
"default_model".to_string(),
json!("gpt-4o-mini"),
)]);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(data_state),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/settings"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["default_provider"], "openai");
assert_eq!(payload["default_model"], "gpt-4o-mini");
assert_eq!(payload["enable_usage_tracking"], json!(true));
assert_eq!(payload["password_policy_level"], "weak");
let response = reqwest::Client::new()
.put(format!("{gateway_url}/api/admin/system/settings"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.json(&json!({
"default_provider": "openai",
"default_model": "gpt-5",
"enable_usage_tracking": false,
"password_policy_level": "strong",
}))
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["message"], "系统设置更新成功");
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/settings"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["default_provider"], "openai");
assert_eq!(payload["default_model"], "gpt-5");
assert_eq!(payload["enable_usage_tracking"], json!(false));
assert_eq!(payload["password_policy_level"], "strong");
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_config_export_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/config/export",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let provider_id = "provider-openai".to_string();
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![
sample_provider(&provider_id, "openai", 10).with_transport_fields(
true,
false,
true,
Some(8),
Some(3),
Some(json!({"node_id": "node-1"})),
Some(30.0),
Some(12.5),
Some(json!({
"provider_ops": {
"connector": {
"credentials": {
"refresh_token": encrypt_python_fernet_plaintext(
DEVELOPMENT_ENCRYPTION_KEY,
"provider-refresh-token",
)
.expect("provider credential should encrypt")
}
}
}
})),
),
],
vec![
sample_endpoint(
"endpoint-chat",
&provider_id,
"openai",
"https://api.openai.example",
),
sample_endpoint(
"endpoint-cli",
&provider_id,
2026-04-26 21:12:57 +08:00
"openai:responses",
"https://api.openai.example",
),
],
vec![{
let mut key = sample_key("key-openai", &provider_id, "openai:chat", "live-api-key");
key.name = "primary".to_string();
key.allowed_models = Some(json!(["gpt-5"]));
key.encrypted_auth_config = Some(
encrypt_python_fernet_plaintext(
DEVELOPMENT_ENCRYPTION_KEY,
r#"{"refresh_token":"oauth-refresh"}"#,
)
.expect("auth config should encrypt"),
);
key
}],
));
let global_model_repository = Arc::new(
InMemoryGlobalModelReadRepository::seed(Vec::<StoredPublicGlobalModel>::new())
.with_admin_global_models(vec![{
let mut model = sample_admin_global_model("global-gpt-5", "gpt-5", "GPT 5");
model.usage_count = 7;
model
}])
.with_admin_provider_models(vec![sample_admin_provider_model(
"model-gpt-5",
&provider_id,
"global-gpt-5",
"gpt-5",
)]),
);
let auth_module_repository = Arc::new(InMemoryAuthModuleReadRepository::seed(
Vec::<StoredOAuthProviderModuleConfig>::new(),
Some(sample_ldap_module_config()),
));
let oauth_provider_repository = Arc::new(InMemoryOAuthProviderRepository::seed(vec![
sample_oauth_provider_config("linuxdo"),
]));
let proxy_node_repository =
Arc::new(InMemoryProxyNodeRepository::seed(vec![sample_proxy_node(
"node-1",
)
.with_manual_proxy_fields(
Some("http://proxy.local:8080".to_string()),
Some("proxy-user".to_string()),
Some("proxy-pass".to_string()),
)]));
let data_state = GatewayDataState::disabled()
.attach_provider_catalog_repository_for_tests(provider_catalog_repository)
.with_global_model_repository_for_tests(global_model_repository)
.attach_auth_module_reader_for_tests(auth_module_repository)
.attach_oauth_provider_repository_for_tests(oauth_provider_repository)
.attach_proxy_node_repository_for_tests(proxy_node_repository)
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY)
.with_system_config_values_for_tests(vec![
(
"smtp_password".to_string(),
json!(
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "smtp-secret",)
.expect("smtp secret should encrypt")
),
),
("site_name".to_string(), json!("Aether Test")),
]);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(data_state),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/config/export"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["version"], "2.3");
assert!(payload["exported_at"].as_str().is_some());
assert_eq!(payload["global_models"][0]["name"], "gpt-5");
assert_eq!(payload["global_models"][0]["usage_count"], json!(7));
assert_eq!(payload["providers"][0]["name"], "openai");
assert_eq!(
payload["providers"][0]["config"]["provider_ops"]["connector"]["credentials"]
["refresh_token"],
"provider-refresh-token"
);
assert_eq!(
payload["providers"][0]["api_keys"][0]["api_key"],
"live-api-key"
);
assert_eq!(
payload["providers"][0]["api_keys"][0]["auth_config"],
r#"{"refresh_token":"oauth-refresh"}"#
);
assert_eq!(
payload["providers"][0]["api_keys"][0]["supported_endpoints"],
json!(["openai:chat"])
);
assert_eq!(
payload["providers"][0]["models"][0]["global_model_name"],
"gpt-5"
);
assert_eq!(payload["ldap_config"]["bind_password"], "");
assert_eq!(
payload["oauth_providers"][0]["client_secret"],
"secret-value"
);
assert_eq!(
payload["proxy_nodes"][0]["proxy_url"],
"http://proxy.local:8080"
);
let smtp_password = payload["system_configs"]
.as_array()
.expect("system configs should be array")
.iter()
.find(|entry| entry["key"] == "smtp_password")
.cloned()
.expect("smtp_password should exist");
assert_eq!(smtp_password["value"], "smtp-secret");
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_users_export_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/users/export",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
2026-05-10 17:28:23 +08:00
let user_repository = Arc::new(InMemoryUserReadRepository::seed_auth_users(vec![
StoredUserAuthRecord::new(
"user-1".to_string(),
Some("[email protected]".to_string()),
true,
"alice".to_string(),
Some("argon2-hash".to_string()),
"user".to_string(),
"local".to_string(),
Some(json!(["openai"])),
Some(json!(["openai:chat"])),
Some(json!(["gpt-5"])),
true,
2026-05-10 17:28:23 +08:00
false,
Some(chrono::Utc::now()),
None,
)
2026-05-10 17:28:23 +08:00
.expect("user export row should build")
.with_policy_modes(
"specific".to_string(),
"specific".to_string(),
"specific".to_string(),
)
.expect("user policy modes should build"),
]));
2026-05-10 17:28:23 +08:00
let user_group = user_repository
.create_user_group(UpsertUserGroupRecord {
name: "Restricted GPT".to_string(),
description: Some("GPT-only users".to_string()),
priority: 10,
allowed_providers: Some(vec!["openai".to_string()]),
allowed_providers_mode: "specific".to_string(),
allowed_api_formats: Some(vec!["openai:chat".to_string()]),
allowed_api_formats_mode: "specific".to_string(),
allowed_models: Some(vec!["gpt-5".to_string()]),
allowed_models_mode: "specific".to_string(),
rate_limit: Some(60),
rate_limit_mode: "custom".to_string(),
})
.await
.expect("user group should create")
.expect("user group should exist");
user_repository
.replace_user_groups_for_user("user-1", std::slice::from_ref(&user_group.id))
.await
.expect("user group membership should create");
let auth_repository = Arc::new(
InMemoryAuthApiKeySnapshotRepository::default().with_export_records(vec![
StoredAuthApiKeyExportRecord::new(
"user-1".to_string(),
"key-user-1".to_string(),
"hash-user-1".to_string(),
Some(
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "ak-user-live-1")
.expect("user api key should encrypt"),
),
Some("User Key".to_string()),
Some(json!(["openai"])),
Some(json!(["openai:chat"])),
Some(json!(["gpt-5"])),
Some(60),
Some(2),
Some(json!({"cache": true})),
true,
Some(1_900_000_000),
false,
42,
420,
12.34,
false,
)
.expect("user api key export record should build"),
StoredAuthApiKeyExportRecord::new(
"admin-owner".to_string(),
"key-standalone-1".to_string(),
"hash-standalone-1".to_string(),
Some(
encrypt_python_fernet_plaintext(
DEVELOPMENT_ENCRYPTION_KEY,
"ak-standalone-live-1",
)
.expect("standalone api key should encrypt"),
),
Some("Standalone Key".to_string()),
Some(json!(["openai"])),
Some(json!(["openai:chat"])),
Some(json!(["gpt-5"])),
None,
Some(5),
None,
true,
None,
false,
7,
84,
3.21,
true,
)
.expect("standalone api key export record should build"),
]),
);
let wallet_repository = Arc::new(InMemoryWalletRepository::seed(vec![
StoredWalletSnapshot::new(
"wallet-user-1".to_string(),
Some("user-1".to_string()),
None,
10.0,
2.5,
"finite".to_string(),
"USD".to_string(),
"active".to_string(),
100.0,
20.0,
5.0,
1.0,
1_800_000_000,
)
.expect("user wallet should build"),
StoredWalletSnapshot::new(
"wallet-standalone-1".to_string(),
None,
Some("key-standalone-1".to_string()),
30.0,
0.0,
"unlimited".to_string(),
"USD".to_string(),
"active".to_string(),
80.0,
10.0,
0.0,
0.0,
1_800_000_100,
)
.expect("standalone wallet should build"),
]));
let data_state =
GatewayDataState::with_auth_and_wallet_for_tests(auth_repository, wallet_repository)
.with_user_reader(user_repository)
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(data_state),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/users/export"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["version"], "1.5");
assert!(payload["exported_at"].as_str().is_some());
2026-05-10 17:28:23 +08:00
assert_eq!(payload["user_groups"][0]["name"], "Restricted GPT");
assert!(payload["user_groups"][0].get("priority").is_none());
assert_eq!(
payload["user_groups"][0]["allowed_models"],
json!(["gpt-5"])
);
assert_eq!(payload["users"][0]["email"], "[email protected]");
2026-05-10 17:28:23 +08:00
assert_eq!(
payload["users"][0]["allowed_models_mode"],
json!("specific")
);
assert_eq!(payload["users"][0]["rate_limit_mode"], json!("system"));
assert_eq!(
payload["users"][0]["group_names"],
json!(["Restricted GPT"])
);
2026-05-24 21:40:48 +08:00
assert_eq!(payload["users"][0]["id"], json!("user-1"));
assert_eq!(payload["users"][0]["request_count"], json!(0));
assert_eq!(payload["users"][0]["total_tokens"], json!(0));
assert_eq!(payload["users"][0]["wallet"]["balance"], json!(12.5));
assert_eq!(
payload["users"][0]["wallet"]["recharge_balance"],
json!(10.0)
);
assert_eq!(payload["users"][0]["wallet"]["gift_balance"], json!(2.5));
assert_eq!(
payload["users"][0]["wallet"]["refundable_balance"],
json!(10.0)
);
assert_eq!(payload["users"][0]["unlimited"], json!(false));
assert_eq!(payload["users"][0]["api_keys"][0]["key"], "ak-user-live-1");
assert_eq!(
payload["users"][0]["api_keys"][0]["key_hash"],
"hash-user-1"
);
assert_eq!(
payload["users"][0]["api_keys"][0]["is_standalone"],
json!(false)
);
2026-05-24 21:40:48 +08:00
assert_eq!(
payload["users"][0]["api_keys"][0]["api_key_id"],
json!("key-user-1")
);
assert_eq!(
payload["users"][0]["api_keys"][0]["total_tokens"],
json!(420)
);
assert_eq!(
payload["standalone_keys"][0]["key"],
json!("ak-standalone-live-1")
);
2026-05-24 21:40:48 +08:00
assert_eq!(
payload["standalone_keys"][0]["api_key_id"],
json!("key-standalone-1")
);
assert_eq!(payload["standalone_keys"][0]["total_tokens"], json!(84));
assert_eq!(
payload["standalone_keys"][0]["wallet"]["unlimited"],
json!(true)
);
assert_eq!(payload["standalone_keys"][0].get("is_standalone"), None,);
2026-05-24 21:40:48 +08:00
assert_eq!(payload["usage_aggregates"]["stats_daily"], json!([]));
assert_eq!(payload["usage_aggregates"]["stats_user_daily"], json!([]));
assert_eq!(
payload["usage_aggregates"]["stats_daily_api_key"],
json!([])
);
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_unavailable_write_routes_locally_with_trusted_admin_principal(
) {
const DETAIL: &str = "Admin system data unavailable";
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().fallback(any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}));
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let client = reqwest::Client::new();
let unavailable_paths = [
"/api/admin/system/config/import",
"/api/admin/system/users/import",
"/api/admin/system/data/import",
];
let local_paths = [
"/api/admin/system/cleanup",
"/api/admin/system/purge/config",
"/api/admin/system/purge/users",
"/api/admin/system/purge/usage",
"/api/admin/system/purge/audit-logs",
"/api/admin/system/purge/request-bodies",
"/api/admin/system/purge/stats",
];
for path in unavailable_paths {
let response = client
.post(format!("{gateway_url}{path}"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.json(&json!({}))
.send()
.await
.expect("request should succeed");
assert_eq!(
response.status(),
StatusCode::SERVICE_UNAVAILABLE,
"path={path}"
);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["detail"], DETAIL, "path={path}");
}
for path in local_paths {
let response = client
.post(format!("{gateway_url}{path}"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.json(&json!({}))
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK, "path={path}");
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert!(
payload["message"]
.as_str()
.is_some_and(|value| !value.is_empty()),
"path={path}"
);
}
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_smtp_test_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().fallback(any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}));
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let client = reqwest::Client::new();
let response = client
.post(format!("{gateway_url}/api/admin/system/smtp/test"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.json(&json!({}))
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["success"], json!(false));
assert_eq!(
payload["message"],
json!("SMTP 配置不完整,请检查 smtp_host, smtp_user, smtp_password, smtp_from_email")
);
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_email_templates_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new()
.route(
"/api/admin/system/email/templates",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
)
.route(
"/api/admin/system/email/templates/verification",
any(|| async { (StatusCode::OK, Body::from("unexpected upstream hit")) }),
)
.route(
"/api/admin/system/email/templates/verification/preview",
any(|| async { (StatusCode::OK, Body::from("unexpected upstream hit")) }),
)
.route(
"/api/admin/system/email/templates/verification/reset",
any(|| async { (StatusCode::OK, Body::from("unexpected upstream hit")) }),
);
let data_state = GatewayDataState::disabled().with_system_config_values_for_tests(vec![
(
"email_template_verification_subject".to_string(),
json!("自定义验证码"),
),
(
"email_template_verification_html".to_string(),
json!("<div>{{ app_name }} - {{ code }}</div>"),
),
("smtp_from_name".to_string(), json!("Aether Mail")),
]);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(data_state),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let client = reqwest::Client::new();
let list_response = client
.get(format!("{gateway_url}/api/admin/system/email/templates"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(list_response.status(), StatusCode::OK);
let list_payload: serde_json::Value = list_response.json().await.expect("json should parse");
let templates = list_payload["templates"]
.as_array()
.expect("templates should be an array");
let verification = templates
.iter()
.find(|item| item["type"] == "verification")
.expect("verification template should exist");
assert_eq!(verification["subject"], "自定义验证码");
assert_eq!(verification["is_custom"], json!(true));
let detail_response = client
.get(format!(
"{gateway_url}/api/admin/system/email/templates/verification"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(detail_response.status(), StatusCode::OK);
let detail_payload: serde_json::Value =
detail_response.json().await.expect("json should parse");
assert_eq!(detail_payload["default_subject"], "验证码");
assert_eq!(detail_payload["is_custom"], json!(true));
let update_response = client
.put(format!(
"{gateway_url}/api/admin/system/email/templates/verification"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.json(&json!({
"subject": "新验证码",
"html": "<div>{{ app_name }}: {{ code }}</div>",
}))
.send()
.await
.expect("request should succeed");
assert_eq!(update_response.status(), StatusCode::OK);
let update_payload: serde_json::Value =
update_response.json().await.expect("json should parse");
assert_eq!(update_payload["message"], "模板保存成功");
let preview_response = client
.post(format!(
"{gateway_url}/api/admin/system/email/templates/verification/preview"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.json(&json!({
"code": "654321",
}))
.send()
.await
.expect("request should succeed");
assert_eq!(preview_response.status(), StatusCode::OK);
let preview_payload: serde_json::Value =
preview_response.json().await.expect("json should parse");
assert_eq!(preview_payload["variables"]["code"], "654321");
assert!(preview_payload["html"]
.as_str()
.is_some_and(|html| html.contains("Aether Mail: 654321")));
let reset_response = client
.post(format!(
"{gateway_url}/api/admin/system/email/templates/verification/reset"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(reset_response.status(), StatusCode::OK);
let reset_payload: serde_json::Value = reset_response.json().await.expect("json should parse");
assert_eq!(reset_payload["message"], "模板已重置为默认值");
assert_eq!(reset_payload["template"]["type"], "verification");
let final_detail_response = client
.get(format!(
"{gateway_url}/api/admin/system/email/templates/verification"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(final_detail_response.status(), StatusCode::OK);
let final_detail_payload: serde_json::Value = final_detail_response
.json()
.await
.expect("json should parse");
assert_eq!(final_detail_payload["subject"], "验证码");
assert_eq!(final_detail_payload["is_custom"], json!(false));
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_api_formats_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/api-formats",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/api-formats"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
let formats = payload["formats"]
.as_array()
.expect("formats should be an array");
assert_eq!(formats[0]["value"], "openai:chat");
assert_eq!(formats[0]["default_path"], "/v1/chat/completions");
let gemini_embedding = formats
.iter()
.find(|item| item["value"] == "gemini:embedding")
.expect("gemini embedding format should exist");
assert_eq!(
gemini_embedding["default_path"],
"/v1beta/models/{model}:{action}"
);
2026-05-03 17:32:41 +08:00
assert!(formats
.iter()
.any(|item| item["value"] == "openai:embedding"));
assert!(formats.iter().any(|item| item["value"] == "openai:rerank"));
assert!(formats.iter().any(|item| item["value"] == "jina:embedding"));
assert!(formats.iter().any(|item| item["value"] == "jina:rerank"));
assert!(formats.iter().any(|item| item["value"] == "gemini:video"));
let aliyun_embedding = formats
.iter()
.find(|item| item["value"] == "aliyun:multimodal_embedding")
.expect("aliyun multimodal embedding format should exist");
assert_eq!(
aliyun_embedding["default_path"],
"/api/v1/services/embeddings/multimodal-embedding/multimodal-embedding"
);
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_configs_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/configs",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let data_state = GatewayDataState::disabled().with_system_config_values_for_tests(vec![
("request_log_level".to_string(), json!("headers")),
("smtp_password".to_string(), json!("encrypted-secret")),
2026-05-15 01:54:20 +08:00
(
"turnstile_secret_key".to_string(),
json!("encrypted-turnstile-secret"),
),
("site_name".to_string(), json!("Aether Test")),
]);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(data_state),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!("{gateway_url}/api/admin/system/configs"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
let items = payload.as_array().expect("payload should be an array");
assert!(items
.iter()
.any(|item| item["key"] == "request_record_level"));
assert!(!items.iter().any(|item| item["key"] == "request_log_level"));
let smtp_password = items
.iter()
.find(|item| item["key"] == "smtp_password")
.expect("smtp_password should exist");
assert_eq!(smtp_password["value"], serde_json::Value::Null);
assert_eq!(smtp_password["is_set"], json!(true));
2026-05-15 01:54:20 +08:00
let turnstile_secret_key = items
.iter()
.find(|item| item["key"] == "turnstile_secret_key")
.expect("turnstile_secret_key should exist");
assert_eq!(turnstile_secret_key["value"], serde_json::Value::Null);
assert_eq!(turnstile_secret_key["is_set"], json!(true));
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_config_detail_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/configs/password_policy_level",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!(
"{gateway_url}/api/admin/system/configs/password_policy_level"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["key"], "password_policy_level");
assert_eq!(payload["value"], "weak");
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_format_conversion_default_as_disabled() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/configs/enable_format_conversion",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!(
"{gateway_url}/api/admin/system/configs/enable_format_conversion"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["key"], "enable_format_conversion");
assert_eq!(payload["value"], json!(false));
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
2026-05-03 14:48:25 +08:00
async fn gateway_handles_admin_system_model_directives_default_as_disabled() {
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!(
"{gateway_url}/api/admin/system/configs/enable_model_directives"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["key"], "enable_model_directives");
assert_eq!(payload["value"], json!(false));
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_validates_chat_pii_redaction_system_config_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/configs/module.chat_pii_redaction.cache_ttl_seconds",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let data_state =
GatewayDataState::disabled()
.with_system_config_values_for_tests(Vec::<(String, serde_json::Value)>::new());
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(data_state),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let client = reqwest::Client::new();
let get_config = |key: &'static str| {
let client = client.clone();
let gateway_url = gateway_url.clone();
async move {
let response = client
.get(format!("{gateway_url}/api/admin/system/configs/{key}"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK, "key={key}");
response
.json::<serde_json::Value>()
.await
.expect("json body should parse")
}
};
let put_config = |key: &'static str, value: serde_json::Value| {
let client = client.clone();
let gateway_url = gateway_url.clone();
async move {
client
.put(format!("{gateway_url}/api/admin/system/configs/{key}"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.json(&json!({ "value": value }))
.send()
.await
.expect("request should succeed")
}
};
assert_eq!(
get_config("module.chat_pii_redaction.enabled").await["value"],
json!(false)
);
assert_eq!(
get_config("module.chat_pii_redaction.cache_ttl_seconds").await["value"],
json!(300)
);
assert_eq!(
2026-05-14 11:14:20 +08:00
get_config("module.chat_pii_redaction.placeholder_prefix").await["value"],
json!("AETHER")
);
let default_rules_payload = get_config("module.chat_pii_redaction.rules").await;
let default_rules = default_rules_payload["value"]
.as_array()
.expect("default rules should be an array");
assert!(
default_rules.iter().any(|rule| {
rule["name"] == json!("手机号") && rule["features"]["validator"] == json!("cn_phone")
}),
"default rules should include 手机号"
);
let enabled_response = put_config("module.chat_pii_redaction.enabled", json!(true)).await;
assert_eq!(enabled_response.status(), StatusCode::OK);
let enabled_payload: serde_json::Value = enabled_response
.json()
.await
.expect("json body should parse");
assert_eq!(enabled_payload["value"], json!(true));
2026-05-14 11:14:20 +08:00
let rules_response = put_config(
"module.chat_pii_redaction.rules",
json!([
{
"id": "email",
"name": "邮箱",
"pattern": r"(?i)[A-Z0-9._%+-]{1,64}@[A-Z0-9.-]{1,253}\.[A-Z]{2,63}",
"enabled": true,
"features": {"validator": "email"},
"system": true
},
{
"id": "custom_code",
"name": "自定义规则",
"pattern": r"CODE-\d{6}",
"enabled": false,
"features": {"validator": "custom_code", "experimental": true},
"system": false
}
]),
)
.await;
2026-05-14 11:14:20 +08:00
assert_eq!(rules_response.status(), StatusCode::OK);
let rules_payload: serde_json::Value =
rules_response.json().await.expect("json body should parse");
assert_eq!(
2026-05-14 11:14:20 +08:00
rules_payload["value"][1]["features"]["experimental"],
json!(true)
);
let ttl_response = put_config("module.chat_pii_redaction.cache_ttl_seconds", json!(3600)).await;
assert_eq!(ttl_response.status(), StatusCode::OK);
let ttl_payload: serde_json::Value = ttl_response.json().await.expect("json body should parse");
assert_eq!(ttl_payload["value"], json!(3600));
2026-05-14 11:14:20 +08:00
let prefix_response = put_config(
"module.chat_pii_redaction.placeholder_prefix",
json!("vendor_safe"),
)
.await;
2026-05-14 11:14:20 +08:00
assert_eq!(prefix_response.status(), StatusCode::OK);
let prefix_payload: serde_json::Value = prefix_response
.json()
.await
.expect("json body should parse");
2026-05-14 11:14:20 +08:00
assert_eq!(prefix_payload["value"], json!("VENDOR_SAFE"));
2026-05-14 11:14:20 +08:00
let invalid_prefix_response = put_config(
"module.chat_pii_redaction.placeholder_prefix",
json!("bad-prefix"),
)
.await;
2026-05-14 11:14:20 +08:00
assert_eq!(invalid_prefix_response.status(), StatusCode::BAD_REQUEST);
2026-05-14 11:14:20 +08:00
let invalid_rules_response = put_config(
"module.chat_pii_redaction.rules",
json!([
{
"id": "broken",
"name": "坏规则",
"pattern": "[",
"enabled": true,
"features": {"validator": "broken"},
"system": false
}
]),
)
.await;
2026-05-14 11:14:20 +08:00
assert_eq!(invalid_rules_response.status(), StatusCode::BAD_REQUEST);
let enabled_default_response =
put_config("module.chat_pii_redaction.enabled", serde_json::Value::Null).await;
assert_eq!(enabled_default_response.status(), StatusCode::OK);
let enabled_default_payload: serde_json::Value = enabled_default_response
.json()
.await
.expect("json body should parse");
assert_eq!(enabled_default_payload["value"], json!(false));
2026-05-14 11:14:20 +08:00
let rules_default_response =
put_config("module.chat_pii_redaction.rules", serde_json::Value::Null).await;
assert_eq!(rules_default_response.status(), StatusCode::OK);
let rules_default_payload: serde_json::Value = rules_default_response
.json()
.await
.expect("json body should parse");
2026-05-14 11:14:20 +08:00
assert!(!rules_default_payload["value"]
.as_array()
.expect("default rules should be an array")
.is_empty());
2026-05-14 11:14:20 +08:00
let invalid_ttl_response =
put_config("module.chat_pii_redaction.cache_ttl_seconds", json!(600)).await;
assert_eq!(invalid_ttl_response.status(), StatusCode::BAD_REQUEST);
let ttl_default_response = put_config(
"module.chat_pii_redaction.cache_ttl_seconds",
serde_json::Value::Null,
)
.await;
assert_eq!(ttl_default_response.status(), StatusCode::OK);
let ttl_default_payload: serde_json::Value = ttl_default_response
.json()
.await
.expect("json body should parse");
assert_eq!(ttl_default_payload["value"], json!(300));
2026-05-14 11:14:20 +08:00
let prefix_default_response = put_config(
"module.chat_pii_redaction.placeholder_prefix",
serde_json::Value::Null,
)
.await;
2026-05-14 11:14:20 +08:00
assert_eq!(prefix_default_response.status(), StatusCode::OK);
let prefix_default_payload: serde_json::Value = prefix_default_response
.json()
.await
.expect("json body should parse");
2026-05-14 11:14:20 +08:00
assert_eq!(prefix_default_payload["value"], json!("AETHER"));
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_system_provider_priority_mode_locally_with_bearer_admin_session() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/configs/provider_priority_mode",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let state = AppState::new().expect("gateway should build");
let access_token = issue_test_admin_access_token(&state, "device-admin-config").await;
let gateway = build_router_with_state(state);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!(
"{gateway_url}/api/admin/system/configs/provider_priority_mode"
))
.header("authorization", format!("Bearer {access_token}"))
.header("x-client-device-id", "device-admin-config")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["key"], "provider_priority_mode");
assert_eq!(payload["value"], "provider");
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_sets_admin_system_config_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/configs/smtp_password",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let data_state =
GatewayDataState::disabled().with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(data_state),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let client = reqwest::Client::new();
let put_response = client
.put(format!(
"{gateway_url}/api/admin/system/configs/smtp_password"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.json(&json!({
"value": "smtp-secret-123",
"description": "SMTP password",
}))
.send()
.await
.expect("request should succeed");
assert_eq!(put_response.status(), StatusCode::OK);
let put_payload: serde_json::Value = put_response.json().await.expect("json body should parse");
assert_eq!(put_payload["key"], "smtp_password");
assert_eq!(put_payload["value"], "********");
assert!(put_payload["updated_at"].as_str().is_some());
let get_response = client
.get(format!(
"{gateway_url}/api/admin/system/configs/smtp_password"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(get_response.status(), StatusCode::OK);
let get_payload: serde_json::Value = get_response.json().await.expect("json body should parse");
assert_eq!(get_payload["value"], serde_json::Value::Null);
assert_eq!(get_payload["is_set"], json!(true));
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_deletes_admin_system_config_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/system/configs/custom_flag",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let data_state = GatewayDataState::disabled()
.with_system_config_values_for_tests(vec![("custom_flag".to_string(), json!(true))]);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(data_state),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let client = reqwest::Client::new();
let delete_response = client
.delete(format!(
"{gateway_url}/api/admin/system/configs/custom_flag"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(delete_response.status(), StatusCode::OK);
let delete_payload: serde_json::Value = delete_response
.json()
.await
.expect("json body should parse");
assert_eq!(delete_payload["message"], "配置项 'custom_flag' 已删除");
let get_response = client
.get(format!(
"{gateway_url}/api/admin/system/configs/custom_flag"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(get_response.status(), StatusCode::NOT_FOUND);
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_key_rpm_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/endpoints/rpm/key/key-openai",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![sample_provider("provider-openai", "openai", 10)],
vec![sample_endpoint(
"endpoint-openai",
"provider-openai",
"openai:chat",
"https://api.openai.example",
)],
vec![
sample_key("key-openai", "provider-openai", "openai:chat", "sk-test")
.with_rate_limit_fields(Some(60), None, None, None, None, None, None, None, None),
],
));
let now_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("current time should be after epoch")
.as_secs() as i64;
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::seed(vec![
sample_recent_key_rpm_candidate(
"cand-openai-rpm-1",
"req-openai-rpm-1",
"endpoint-openai",
"key-openai",
now_unix_secs,
2,
),
sample_recent_key_rpm_candidate(
"cand-openai-rpm-2",
"req-openai-rpm-2",
"endpoint-openai",
"key-openai",
now_unix_secs,
1,
),
]));
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(
GatewayDataState::with_provider_catalog_and_request_candidate_reader_for_tests(
provider_catalog_repository,
request_candidate_repository,
),
),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.get(format!(
"{gateway_url}/api/admin/endpoints/rpm/key/key-openai"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["key_id"], "key-openai");
assert_eq!(payload["current_rpm"], 2);
assert_eq!(payload["rpm_limit"], 60);
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_resets_admin_key_rpm_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
"/api/admin/endpoints/rpm/key/key-openai",
any(move |_request: Request| {
let upstream_hits_inner = Arc::clone(&upstream_hits_clone);
async move {
*upstream_hits_inner.lock().expect("mutex should lock") += 1;
(StatusCode::OK, Body::from("unexpected upstream hit"))
}
}),
);
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![sample_provider("provider-openai", "openai", 10)],
vec![sample_endpoint(
"endpoint-openai",
"provider-openai",
"openai:chat",
"https://api.openai.example",
)],
vec![
sample_key("key-openai", "provider-openai", "openai:chat", "sk-test")
.with_rate_limit_fields(Some(60), None, None, None, None, None, None, None, None),
],
));
let now_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("current time should be after epoch")
.as_secs() as i64;
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::seed(vec![
sample_recent_key_rpm_candidate(
"cand-openai-rpm-1",
"req-openai-rpm-1",
"endpoint-openai",
"key-openai",
now_unix_secs,
2,
),
sample_recent_key_rpm_candidate(
"cand-openai-rpm-2",
"req-openai-rpm-2",
"endpoint-openai",
"key-openai",
now_unix_secs,
1,
),
]));
let (upstream_url, upstream_handle) = start_server(upstream).await;
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(
GatewayDataState::with_provider_catalog_and_request_candidate_reader_for_tests(
provider_catalog_repository,
request_candidate_repository,
),
),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let client = reqwest::Client::new();
let reset_response = client
.delete(format!(
"{gateway_url}/api/admin/endpoints/rpm/key/key-openai"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(reset_response.status(), StatusCode::OK);
let reset_payload: serde_json::Value =
reset_response.json().await.expect("json body should parse");
assert_eq!(reset_payload["message"], "RPM 计数已重置");
let rpm_response = client
.get(format!(
"{gateway_url}/api/admin/endpoints/rpm/key/key-openai"
))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.send()
.await
.expect("request should succeed");
assert_eq!(rpm_response.status(), StatusCode::OK);
let rpm_payload: serde_json::Value = rpm_response.json().await.expect("json body should parse");
assert_eq!(rpm_payload["key_id"], "key-openai");
assert_eq!(rpm_payload["current_rpm"], 0);
assert_eq!(rpm_payload["rpm_limit"], 60);
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
upstream_handle.abort();
}