mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat(admin): 完善代理节点与 OAuth 授权管理
This commit is contained in:
@@ -430,10 +430,13 @@ async fn gateway_handles_admin_api_keys_create_locally_with_trusted_admin_princi
|
||||
admin_request(reqwest::Client::new().post(format!("{gateway_url}/api/admin/api-keys")))
|
||||
.json(&json!({
|
||||
"name": "standalone-key",
|
||||
"rate_limit": 180,
|
||||
"rate_limit": null,
|
||||
"allowed_providers": ["openai"],
|
||||
"allowed_api_formats": ["openai:chat"],
|
||||
"allowed_models": ["gpt-4.1"],
|
||||
"initial_balance_usd": 12.5,
|
||||
"expires_at": "2030-01-02",
|
||||
"auto_delete_on_expiry": true,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
@@ -443,11 +446,18 @@ async fn gateway_handles_admin_api_keys_create_locally_with_trusted_admin_princi
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["name"], json!("standalone-key"));
|
||||
assert_eq!(payload["is_standalone"], json!(true));
|
||||
assert_eq!(payload["rate_limit"], json!(180));
|
||||
assert_eq!(payload["rate_limit"], serde_json::Value::Null);
|
||||
assert_eq!(payload["allowed_providers"], json!(["openai"]));
|
||||
assert_eq!(payload["allowed_api_formats"], json!(["openai:chat"]));
|
||||
assert_eq!(payload["allowed_models"], json!(["gpt-4.1"]));
|
||||
assert_eq!(payload["wallet"], serde_json::Value::Null);
|
||||
assert_eq!(payload["auto_delete_on_expiry"], json!(true));
|
||||
assert_eq!(payload["wallet"]["balance"], json!(12.5));
|
||||
assert_eq!(payload["wallet"]["limit_mode"], json!("finite"));
|
||||
assert_eq!(payload["wallet"]["unlimited"], json!(false));
|
||||
assert!(payload["expires_at"]
|
||||
.as_str()
|
||||
.expect("expires_at should exist")
|
||||
.starts_with("2030-01-02"));
|
||||
let plaintext = payload["key"]
|
||||
.as_str()
|
||||
.expect("plaintext key should exist")
|
||||
@@ -489,6 +499,7 @@ async fn gateway_handles_admin_api_keys_update_locally_with_trusted_admin_princi
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_auth_wallets_for_tests([sample_standalone_wallet("key-123")])
|
||||
.with_data_state_for_tests(
|
||||
crate::data::GatewayDataState::with_auth_api_key_repository_for_tests(Arc::clone(
|
||||
&repository,
|
||||
@@ -503,10 +514,13 @@ async fn gateway_handles_admin_api_keys_update_locally_with_trusted_admin_princi
|
||||
)
|
||||
.json(&json!({
|
||||
"name": "renamed-key",
|
||||
"rate_limit": 240,
|
||||
"rate_limit": null,
|
||||
"allowed_providers": ["gemini"],
|
||||
"allowed_api_formats": ["gemini:chat"],
|
||||
"allowed_models": ["gemini-2.5-pro"],
|
||||
"expires_at": "2030-03-04",
|
||||
"auto_delete_on_expiry": true,
|
||||
"unlimited_balance": true,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
@@ -516,10 +530,17 @@ async fn gateway_handles_admin_api_keys_update_locally_with_trusted_admin_princi
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["id"], json!("key-123"));
|
||||
assert_eq!(payload["name"], json!("renamed-key"));
|
||||
assert_eq!(payload["rate_limit"], json!(240));
|
||||
assert_eq!(payload["rate_limit"], serde_json::Value::Null);
|
||||
assert_eq!(payload["allowed_providers"], json!(["gemini"]));
|
||||
assert_eq!(payload["allowed_api_formats"], json!(["gemini:chat"]));
|
||||
assert_eq!(payload["allowed_models"], json!(["gemini-2.5-pro"]));
|
||||
assert_eq!(payload["auto_delete_on_expiry"], json!(true));
|
||||
assert_eq!(payload["wallet"]["limit_mode"], json!("unlimited"));
|
||||
assert_eq!(payload["wallet"]["unlimited"], json!(true));
|
||||
assert!(payload["expires_at"]
|
||||
.as_str()
|
||||
.expect("expires_at should exist")
|
||||
.starts_with("2030-03-04"));
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
|
||||
@@ -1932,6 +1932,118 @@ async fn gateway_imports_admin_provider_oauth_refresh_token_via_execution_runtim
|
||||
execution_runtime_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_imports_admin_provider_oauth_refresh_token_via_execution_runtime_system_proxy() {
|
||||
let execution_plans = Arc::new(Mutex::new(Vec::<ExecutionPlan>::new()));
|
||||
let execution_plans_clone = Arc::clone(&execution_plans);
|
||||
let execution_runtime = Router::new().route(
|
||||
"/v1/execute/sync",
|
||||
any(move |Json(plan): Json<ExecutionPlan>| {
|
||||
let execution_plans_inner = Arc::clone(&execution_plans_clone);
|
||||
async move {
|
||||
execution_plans_inner
|
||||
.lock()
|
||||
.expect("mutex should lock")
|
||||
.push(plan.clone());
|
||||
let proxy = plan.proxy.as_ref().expect("proxy snapshot should exist");
|
||||
assert_eq!(proxy.node_id.as_deref(), Some("proxy-node-codex-system"));
|
||||
Json(json!({
|
||||
"request_id": plan.request_id,
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"json_body": {
|
||||
"access_token": "imported-codex-access-token",
|
||||
"refresh_token": "imported-codex-refresh-token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 1800,
|
||||
"scope": "openid email profile offline_access",
|
||||
"email": "alice@example.com",
|
||||
"account_id": "acct-codex-123",
|
||||
"plan_type": "plus"
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let mut provider = sample_provider("provider-codex", "codex", 10);
|
||||
provider.provider_type = "codex".to_string();
|
||||
let endpoint = sample_endpoint(
|
||||
"endpoint-codex-chat",
|
||||
"provider-codex",
|
||||
"openai:chat",
|
||||
"https://chatgpt.com/backend-api/codex",
|
||||
);
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
vec![endpoint],
|
||||
vec![],
|
||||
));
|
||||
let mut manual_node = sample_proxy_node("proxy-node-codex-system");
|
||||
manual_node.status = "online".to_string();
|
||||
manual_node.is_manual = true;
|
||||
manual_node.tunnel_mode = false;
|
||||
manual_node.tunnel_connected = false;
|
||||
manual_node.proxy_url = Some("http://proxy.example:8080".to_string());
|
||||
let proxy_node_repository = Arc::new(InMemoryProxyNodeRepository::seed(vec![manual_node]));
|
||||
|
||||
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
|
||||
let gateway = build_router_with_state(
|
||||
build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
.with_data_state_for_tests(
|
||||
GatewayDataState::with_provider_catalog_repository_for_tests(
|
||||
provider_catalog_repository.clone(),
|
||||
)
|
||||
.attach_proxy_node_repository_for_tests(proxy_node_repository)
|
||||
.with_system_config_values_for_tests(vec![(
|
||||
"system_proxy_node_id".to_string(),
|
||||
json!("proxy-node-codex-system"),
|
||||
)])
|
||||
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
)
|
||||
.with_provider_oauth_token_url_for_tests("codex", "https://oauth.example/oauth/token"),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"{gateway_url}/api/admin/provider-oauth/providers/provider-codex/import-refresh-token"
|
||||
))
|
||||
.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!({
|
||||
"refresh_token": "provider-import-refresh-token",
|
||||
"name": "codex-import"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
let status = response.status();
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(status, StatusCode::OK, "payload={payload}");
|
||||
assert_eq!(payload["provider_type"], "codex");
|
||||
|
||||
let plans = execution_plans.lock().expect("mutex should lock");
|
||||
assert_eq!(plans.len(), 1);
|
||||
assert_eq!(
|
||||
plans[0]
|
||||
.proxy
|
||||
.as_ref()
|
||||
.and_then(|proxy| proxy.node_id.as_deref()),
|
||||
Some("proxy-node-codex-system")
|
||||
);
|
||||
|
||||
gateway_handle.abort();
|
||||
execution_runtime_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_batch_imports_admin_provider_oauth_kiro_locally_with_trusted_admin_principal() {
|
||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||
@@ -2541,6 +2653,168 @@ async fn gateway_refreshes_admin_provider_oauth_key_locally_with_trusted_admin_p
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_refreshes_admin_provider_oauth_key_locally_via_execution_runtime_key_proxy_before_system_proxy(
|
||||
) {
|
||||
let execution_plans = Arc::new(Mutex::new(Vec::<ExecutionPlan>::new()));
|
||||
let execution_plans_clone = Arc::clone(&execution_plans);
|
||||
let execution_runtime = Router::new().route(
|
||||
"/v1/execute/sync",
|
||||
any(move |Json(plan): Json<ExecutionPlan>| {
|
||||
let execution_plans_inner = Arc::clone(&execution_plans_clone);
|
||||
async move {
|
||||
execution_plans_inner
|
||||
.lock()
|
||||
.expect("mutex should lock")
|
||||
.push(plan.clone());
|
||||
if plan.request_id == "provider-oauth:local-refresh-token" {
|
||||
Json(json!({
|
||||
"request_id": plan.request_id,
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"json_body": {
|
||||
"access_token": "refreshed-codex-access-token",
|
||||
"refresh_token": "refreshed-codex-refresh-token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 1800,
|
||||
"scope": "openid email profile offline_access",
|
||||
"email": "alice@example.com",
|
||||
"account_id": "acct-codex-123",
|
||||
"plan_type": "plus"
|
||||
}
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
Json(json!({
|
||||
"request_id": plan.request_id,
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"json_body": {}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let mut provider = sample_provider("provider-codex", "codex", 10);
|
||||
provider.provider_type = "codex".to_string();
|
||||
provider.proxy = Some(json!({"node_id":"proxy-node-provider","enabled":true}));
|
||||
let endpoint = sample_endpoint(
|
||||
"endpoint-codex-cli",
|
||||
"provider-codex",
|
||||
"openai:cli",
|
||||
"https://chatgpt.com/backend-api/codex",
|
||||
);
|
||||
|
||||
let mut key = sample_key(
|
||||
"key-codex-oauth-refresh",
|
||||
"provider-codex",
|
||||
"openai:cli",
|
||||
"stale-codex-access-token",
|
||||
);
|
||||
key.auth_type = "oauth".to_string();
|
||||
key.proxy = Some(json!({"node_id":"proxy-node-key","enabled":true}));
|
||||
key.encrypted_auth_config = Some(
|
||||
encrypt_python_fernet_plaintext(
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
r#"{"provider_type":"codex","refresh_token":"old-codex-refresh-token","email":"alice@example.com","account_id":"acct-codex-123","plan_type":"plus","expires_at":1}"#,
|
||||
)
|
||||
.expect("auth config ciphertext should build"),
|
||||
);
|
||||
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
vec![endpoint],
|
||||
vec![key],
|
||||
));
|
||||
let mut key_node = sample_proxy_node("proxy-node-key");
|
||||
key_node.status = "online".to_string();
|
||||
key_node.is_manual = true;
|
||||
key_node.tunnel_mode = false;
|
||||
key_node.tunnel_connected = false;
|
||||
key_node.proxy_url = Some("http://proxy-key.example:8080".to_string());
|
||||
let mut provider_node = sample_proxy_node("proxy-node-provider");
|
||||
provider_node.status = "online".to_string();
|
||||
provider_node.is_manual = true;
|
||||
provider_node.tunnel_mode = false;
|
||||
provider_node.tunnel_connected = false;
|
||||
provider_node.proxy_url = Some("http://proxy-provider.example:8080".to_string());
|
||||
let mut system_node = sample_proxy_node("proxy-node-system");
|
||||
system_node.status = "online".to_string();
|
||||
system_node.is_manual = true;
|
||||
system_node.tunnel_mode = false;
|
||||
system_node.tunnel_connected = false;
|
||||
system_node.proxy_url = Some("http://proxy-system.example:8080".to_string());
|
||||
let proxy_node_repository = Arc::new(InMemoryProxyNodeRepository::seed(vec![
|
||||
key_node,
|
||||
provider_node,
|
||||
system_node,
|
||||
]));
|
||||
|
||||
let oauth_refresh =
|
||||
crate::provider_transport::LocalOAuthRefreshCoordinator::with_adapters_for_tests(vec![
|
||||
Arc::new(
|
||||
crate::provider_transport::oauth_refresh::GenericOAuthRefreshAdapter::default()
|
||||
.with_token_url_for_tests("codex", "https://oauth.example/oauth/token"),
|
||||
),
|
||||
]);
|
||||
|
||||
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
|
||||
let gateway = build_router_with_state(
|
||||
build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
.with_data_state_for_tests(
|
||||
GatewayDataState::with_provider_catalog_repository_for_tests(
|
||||
provider_catalog_repository.clone(),
|
||||
)
|
||||
.attach_proxy_node_repository_for_tests(proxy_node_repository)
|
||||
.with_system_config_values_for_tests(vec![(
|
||||
"system_proxy_node_id".to_string(),
|
||||
json!("proxy-node-system"),
|
||||
)])
|
||||
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
)
|
||||
.with_oauth_refresh_coordinator_for_tests(oauth_refresh),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"{gateway_url}/api/admin/provider-oauth/keys/key-codex-oauth-refresh/refresh"
|
||||
))
|
||||
.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 plans = execution_plans.lock().expect("mutex should lock");
|
||||
let refresh_plan = plans
|
||||
.iter()
|
||||
.find(|plan| plan.request_id == "provider-oauth:local-refresh-token")
|
||||
.expect("local refresh plan should exist");
|
||||
assert_eq!(
|
||||
refresh_plan
|
||||
.proxy
|
||||
.as_ref()
|
||||
.and_then(|proxy| proxy.node_id.as_deref()),
|
||||
Some("proxy-node-key")
|
||||
);
|
||||
|
||||
gateway_handle.abort();
|
||||
execution_runtime_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_admin_provider_oauth_unavailable_routes_locally_with_trusted_admin_principal(
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use aether_data::repository::management_tokens::InMemoryManagementTokenRepository;
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
use aether_data::repository::proxy_nodes::{
|
||||
InMemoryProxyNodeRepository, ProxyNodeHeartbeatMutation, StoredProxyNodeEvent,
|
||||
};
|
||||
@@ -9,10 +10,11 @@ use axum::routing::any;
|
||||
use axum::{extract::Request, Router};
|
||||
use http::StatusCode;
|
||||
use serde_json::json;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use super::super::{
|
||||
build_router_with_state, hash_management_token, sample_management_token, sample_proxy_node,
|
||||
start_server, AppState,
|
||||
build_router_with_state, hash_management_token, sample_endpoint, sample_key,
|
||||
sample_management_token, sample_provider, sample_proxy_node, start_server, AppState,
|
||||
};
|
||||
use crate::constants::{
|
||||
GATEWAY_HEADER, TRUSTED_ADMIN_SESSION_ID_HEADER, TRUSTED_ADMIN_USER_ID_HEADER,
|
||||
@@ -715,6 +717,251 @@ async fn gateway_registers_and_unregisters_proxy_nodes_locally_with_management_t
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_creates_updates_and_tests_manual_proxy_nodes_locally() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("listener should bind");
|
||||
let proxy_port = listener
|
||||
.local_addr()
|
||||
.expect("listener addr should resolve")
|
||||
.port();
|
||||
let accept_handle =
|
||||
tokio::spawn(async move { while let Ok((_stream, _addr)) = listener.accept().await {} });
|
||||
|
||||
let proxy_node_repository = Arc::new(InMemoryProxyNodeRepository::default());
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(GatewayDataState::with_proxy_node_repository_for_tests(
|
||||
proxy_node_repository,
|
||||
)),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
let client = reqwest::Client::new();
|
||||
let proxy_url = format!("http://127.0.0.1:{proxy_port}");
|
||||
|
||||
let create_response = client
|
||||
.post(format!("{gateway_url}/api/admin/proxy-nodes/manual"))
|
||||
.header(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!({
|
||||
"name": "manual-node",
|
||||
"proxy_url": proxy_url,
|
||||
"region": "US-West"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("create request should succeed");
|
||||
let create_status = create_response.status();
|
||||
let create_body = create_response
|
||||
.text()
|
||||
.await
|
||||
.expect("body should read as text");
|
||||
assert_eq!(create_status, StatusCode::OK, "create body: {create_body}");
|
||||
let create_payload: serde_json::Value =
|
||||
serde_json::from_str(&create_body).expect("json body should parse");
|
||||
let node_id = create_payload["node_id"]
|
||||
.as_str()
|
||||
.expect("node id should exist")
|
||||
.to_string();
|
||||
assert_eq!(create_payload["node"]["is_manual"], true);
|
||||
assert_eq!(create_payload["node"]["status"], "online");
|
||||
assert_eq!(create_payload["node"]["proxy_url"], proxy_url);
|
||||
|
||||
let test_url_response = client
|
||||
.post(format!("{gateway_url}/api/admin/proxy-nodes/test-url"))
|
||||
.header(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!({
|
||||
"proxy_url": proxy_url
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("test-url request should succeed");
|
||||
assert_eq!(test_url_response.status(), StatusCode::OK);
|
||||
let test_url_payload: serde_json::Value = test_url_response
|
||||
.json()
|
||||
.await
|
||||
.expect("json body should parse");
|
||||
assert_eq!(test_url_payload["success"], true);
|
||||
assert!(test_url_payload["latency_ms"].is_u64());
|
||||
|
||||
let test_node_response = client
|
||||
.post(format!(
|
||||
"{gateway_url}/api/admin/proxy-nodes/{node_id}/test"
|
||||
))
|
||||
.header(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("test-node request should succeed");
|
||||
assert_eq!(test_node_response.status(), StatusCode::OK);
|
||||
let test_node_payload: serde_json::Value = test_node_response
|
||||
.json()
|
||||
.await
|
||||
.expect("json body should parse");
|
||||
assert_eq!(test_node_payload["success"], true);
|
||||
|
||||
let update_response = client
|
||||
.patch(format!("{gateway_url}/api/admin/proxy-nodes/{node_id}"))
|
||||
.header(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!({
|
||||
"name": "manual-node-updated",
|
||||
"region": "US-East"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("update request should succeed");
|
||||
assert_eq!(update_response.status(), StatusCode::OK);
|
||||
let update_payload: serde_json::Value = update_response
|
||||
.json()
|
||||
.await
|
||||
.expect("json body should parse");
|
||||
assert_eq!(update_payload["node"]["name"], "manual-node-updated");
|
||||
assert_eq!(update_payload["node"]["region"], "US-East");
|
||||
assert_eq!(update_payload["node"]["proxy_url"], proxy_url);
|
||||
|
||||
gateway_handle.abort();
|
||||
accept_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_tests_disconnected_tunnel_proxy_nodes_locally() {
|
||||
let proxy_node_repository =
|
||||
Arc::new(InMemoryProxyNodeRepository::seed(vec![sample_proxy_node(
|
||||
"node-offline",
|
||||
)]));
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(GatewayDataState::with_proxy_node_repository_for_tests(
|
||||
proxy_node_repository,
|
||||
)),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"{gateway_url}/api/admin/proxy-nodes/node-offline/test"
|
||||
))
|
||||
.header(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["success"], false);
|
||||
assert_eq!(payload["error"], "tunnel 未连接");
|
||||
|
||||
gateway_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_deletes_proxy_nodes_and_clears_proxy_refs_locally() {
|
||||
let mut manual_node = sample_proxy_node("manual-node-1");
|
||||
manual_node.name = "manual-node-1".to_string();
|
||||
manual_node.status = "online".to_string();
|
||||
manual_node.is_manual = true;
|
||||
manual_node.tunnel_mode = false;
|
||||
manual_node.tunnel_connected = false;
|
||||
manual_node.proxy_url = Some("http://127.0.0.1:8899".to_string());
|
||||
manual_node.last_heartbeat_at_unix_secs = None;
|
||||
manual_node.tunnel_connected_at_unix_secs = None;
|
||||
|
||||
let proxy_node_repository = Arc::new(InMemoryProxyNodeRepository::seed(vec![manual_node]));
|
||||
let mut provider = sample_provider("provider-1", "OpenAI", 10);
|
||||
provider.proxy = Some(json!({ "node_id": "manual-node-1", "enabled": true }));
|
||||
let mut endpoint = sample_endpoint(
|
||||
"endpoint-1",
|
||||
"provider-1",
|
||||
"openai:chat",
|
||||
"https://example.com/v1",
|
||||
);
|
||||
endpoint.proxy = Some(json!({ "node_id": "manual-node-1", "enabled": true }));
|
||||
let mut key = sample_key("key-1", "provider-1", "openai:chat", "sk-test");
|
||||
key.proxy = Some(json!({ "node_id": "manual-node-1", "enabled": true }));
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
vec![endpoint],
|
||||
vec![key],
|
||||
));
|
||||
|
||||
let data_state =
|
||||
GatewayDataState::with_proxy_node_repository_for_tests(Arc::clone(&proxy_node_repository))
|
||||
.attach_provider_catalog_repository_for_tests(Arc::clone(&provider_catalog_repository))
|
||||
.with_system_config_values_for_tests(vec![(
|
||||
"system_proxy_node_id".to_string(),
|
||||
json!("manual-node-1"),
|
||||
)]);
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state.clone()),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.delete(format!("{gateway_url}/api/admin/proxy-nodes/manual-node-1"))
|
||||
.header(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["cleared_system_proxy"], true);
|
||||
assert_eq!(payload["cleared_providers"], 1);
|
||||
assert_eq!(payload["cleared_endpoints"], 1);
|
||||
assert_eq!(payload["cleared_keys"], 1);
|
||||
|
||||
assert!(data_state
|
||||
.find_proxy_node("manual-node-1")
|
||||
.await
|
||||
.expect("node lookup should succeed")
|
||||
.is_none());
|
||||
assert_eq!(
|
||||
data_state
|
||||
.find_system_config_value("system_proxy_node_id")
|
||||
.await
|
||||
.expect("system config lookup should succeed"),
|
||||
Some(serde_json::Value::Null)
|
||||
);
|
||||
|
||||
let provider_ids = vec!["provider-1".to_string()];
|
||||
let providers = data_state
|
||||
.list_provider_catalog_providers(false)
|
||||
.await
|
||||
.expect("provider list should succeed");
|
||||
assert!(providers.iter().all(|provider| provider.proxy.is_none()));
|
||||
let endpoints = data_state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(&provider_ids)
|
||||
.await
|
||||
.expect("endpoint list should succeed");
|
||||
assert!(endpoints.iter().all(|endpoint| endpoint.proxy.is_none()));
|
||||
let keys = data_state
|
||||
.list_provider_catalog_keys_by_provider_ids(&provider_ids)
|
||||
.await
|
||||
.expect("key list should succeed");
|
||||
assert!(keys.iter().all(|key| key.proxy.is_none()));
|
||||
|
||||
gateway_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_admin_proxy_node_events_locally_with_trusted_admin_principal() {
|
||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||
|
||||
Reference in New Issue
Block a user