mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 00:17:45 +08:00
Merge remote-tracking branch 'origin/pr/498'
This commit is contained in:
@@ -347,6 +347,78 @@ async fn gateway_counts_keys_with_null_api_formats_for_each_fixed_provider_endpo
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_counts_inherited_windsurf_key_formats_for_admin_provider_endpoints() {
|
||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||
let upstream_hits_clone = Arc::clone(&upstream_hits);
|
||||
let upstream = Router::new().route(
|
||||
"/api/admin/endpoints/providers/provider-windsurf/endpoints",
|
||||
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 mut key = sample_key(
|
||||
"key-windsurf-a",
|
||||
"provider-windsurf",
|
||||
"openai:chat",
|
||||
"oauth-secret",
|
||||
);
|
||||
key.auth_type = "oauth".to_string();
|
||||
key.api_formats = None;
|
||||
|
||||
let mut provider = sample_provider("provider-windsurf", "windsurf", 10);
|
||||
provider.provider_type = "windsurf".to_string();
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
vec![sample_endpoint(
|
||||
"endpoint-windsurf-chat",
|
||||
"provider-windsurf",
|
||||
"openai:chat",
|
||||
"https://server.codeium.com",
|
||||
)],
|
||||
vec![key],
|
||||
));
|
||||
|
||||
let (_, 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_reader_for_tests(
|
||||
provider_catalog_repository,
|
||||
)),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.get(format!(
|
||||
"{gateway_url}/api/admin/endpoints/providers/provider-windsurf/endpoints?skip=0&limit=50"
|
||||
))
|
||||
.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_eq!(items.len(), 1);
|
||||
assert_eq!(items[0]["id"], "endpoint-windsurf-chat");
|
||||
assert_eq!(items[0]["total_keys"], 1);
|
||||
assert_eq!(items[0]["active_keys"], 1);
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_returns_service_unavailable_for_admin_provider_endpoint_create_when_catalog_writer_unavailable(
|
||||
) {
|
||||
|
||||
@@ -216,6 +216,23 @@ fn codex_quota_execution_result(request_id: &str) -> serde_json::Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn windsurf_register_user_execution_result(request_id: &str) -> serde_json::Value {
|
||||
json!({
|
||||
"request_id": request_id,
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"json_body": {
|
||||
"apiKey": "devin-session-token$registered",
|
||||
"name": "Windsurf User",
|
||||
"apiServerUrl": "https://server.codeium.com"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_single_provider_oauth_refresh_token_plan<'a>(
|
||||
plans: &'a [ExecutionPlan],
|
||||
) -> &'a ExecutionPlan {
|
||||
@@ -281,18 +298,404 @@ async fn gateway_handles_admin_provider_oauth_supported_types_locally_with_trust
|
||||
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("items should be array");
|
||||
assert_eq!(items.len(), 5);
|
||||
assert_eq!(items.len(), 6);
|
||||
assert_eq!(items[0]["provider_type"], "claude_code");
|
||||
assert_eq!(items[1]["provider_type"], "codex");
|
||||
assert_eq!(items[2]["provider_type"], "chatgpt_web");
|
||||
assert_eq!(items[3]["provider_type"], "gemini_cli");
|
||||
assert_eq!(items[4]["provider_type"], "antigravity");
|
||||
assert_eq!(items[5]["provider_type"], "windsurf");
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_admin_provider_oauth_device_authorize_for_windsurf_browser() {
|
||||
let mut provider = sample_provider("provider-windsurf", "windsurf", 10);
|
||||
provider.provider_type = "windsurf".to_string();
|
||||
let endpoint = sample_endpoint(
|
||||
"endpoint-windsurf-chat",
|
||||
"provider-windsurf",
|
||||
"openai:chat",
|
||||
"https://server.codeium.com",
|
||||
);
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
vec![endpoint],
|
||||
vec![],
|
||||
));
|
||||
let state = AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(GatewayDataState::with_provider_catalog_reader_for_tests(
|
||||
provider_catalog_repository,
|
||||
));
|
||||
|
||||
let response = local_admin_provider_oauth_response(
|
||||
&state,
|
||||
http::Method::POST,
|
||||
"/api/admin/provider-oauth/providers/provider-windsurf/device-authorize",
|
||||
Some(json!({
|
||||
"auth_type": "browser",
|
||||
"login_option": "github",
|
||||
"proxy_node_id": "proxy-node-windsurf"
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json body should parse");
|
||||
let session_id = payload["session_id"]
|
||||
.as_str()
|
||||
.expect("session_id should exist");
|
||||
assert_eq!(payload["auth_type"], "browser");
|
||||
assert_eq!(payload["login_option"], "github");
|
||||
assert_eq!(payload["redirect_uri"], "show-auth-token");
|
||||
assert_eq!(payload["callback_required"], true);
|
||||
|
||||
let authorization_url = payload["verification_uri_complete"]
|
||||
.as_str()
|
||||
.expect("authorization url should exist");
|
||||
let parsed = url::Url::parse(authorization_url).expect("authorization url should parse");
|
||||
let params = parsed
|
||||
.query_pairs()
|
||||
.into_owned()
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
assert_eq!(
|
||||
parsed.as_str().split('?').next(),
|
||||
Some("https://windsurf.com/windsurf/signin")
|
||||
);
|
||||
assert_eq!(
|
||||
params.get("response_type").map(String::as_str),
|
||||
Some("token")
|
||||
);
|
||||
assert_eq!(params.get("state").map(String::as_str), Some(session_id));
|
||||
assert_eq!(
|
||||
params.get("redirect_uri").map(String::as_str),
|
||||
Some("show-auth-token")
|
||||
);
|
||||
assert_eq!(
|
||||
params.get("redirect_parameters_type").map(String::as_str),
|
||||
Some("query")
|
||||
);
|
||||
|
||||
let stored = state
|
||||
.load_provider_oauth_device_session_for_tests(&format!("device_auth_session:{session_id}"))
|
||||
.expect("device session should be stored");
|
||||
let stored: serde_json::Value =
|
||||
serde_json::from_str(&stored).expect("device session json should parse");
|
||||
assert_eq!(stored["provider_id"], "provider-windsurf");
|
||||
assert_eq!(stored["auth_type"], "browser");
|
||||
assert_eq!(stored["social_provider"], "github");
|
||||
assert_eq!(stored["redirect_uri"], "show-auth-token");
|
||||
assert_eq!(stored["proxy_node_id"], "proxy-node-windsurf");
|
||||
assert_eq!(stored["status"], "pending");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_rejects_generic_oauth_start_for_windsurf_provider() {
|
||||
let mut provider = sample_provider("provider-windsurf", "windsurf", 10);
|
||||
provider.provider_type = "windsurf".to_string();
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
vec![],
|
||||
vec![],
|
||||
));
|
||||
let state = AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(GatewayDataState::with_provider_catalog_reader_for_tests(
|
||||
provider_catalog_repository,
|
||||
));
|
||||
|
||||
let response = local_admin_provider_oauth_response(
|
||||
&state,
|
||||
http::Method::POST,
|
||||
"/api/admin/provider-oauth/providers/provider-windsurf/start",
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json body should parse");
|
||||
assert!(
|
||||
payload["detail"]
|
||||
.as_str()
|
||||
.is_some_and(|detail| detail.contains("浏览器登录")),
|
||||
"payload={payload}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_admin_provider_oauth_device_poll_for_windsurf_callback_token() {
|
||||
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:windsurf-register:new" {
|
||||
return Json(windsurf_register_user_execution_result(&plan.request_id));
|
||||
}
|
||||
Json(json!({
|
||||
"request_id": plan.request_id,
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"json_body": {}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let mut provider = sample_provider("provider-windsurf", "windsurf", 10);
|
||||
provider.provider_type = "windsurf".to_string();
|
||||
let endpoint = sample_endpoint(
|
||||
"endpoint-windsurf-chat",
|
||||
"provider-windsurf",
|
||||
"openai:chat",
|
||||
"https://server.codeium.com",
|
||||
);
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
vec![endpoint],
|
||||
vec![],
|
||||
));
|
||||
let mut proxy_node = sample_proxy_node("proxy-node-windsurf");
|
||||
proxy_node.status = "online".to_string();
|
||||
proxy_node.is_manual = true;
|
||||
proxy_node.tunnel_mode = false;
|
||||
proxy_node.tunnel_connected = false;
|
||||
proxy_node.proxy_url = Some("http://proxy.example:8080".to_string());
|
||||
let proxy_node_repository = Arc::new(InMemoryProxyNodeRepository::seed(vec![proxy_node]));
|
||||
|
||||
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
|
||||
let 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(),
|
||||
)
|
||||
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY)
|
||||
.attach_proxy_node_repository_for_tests(proxy_node_repository),
|
||||
)
|
||||
.with_provider_oauth_device_session_entry_for_tests(
|
||||
"session-windsurf",
|
||||
json!({
|
||||
"provider_id": "provider-windsurf",
|
||||
"region": "",
|
||||
"client_id": "",
|
||||
"client_secret": "",
|
||||
"device_code": "",
|
||||
"auth_type": "browser",
|
||||
"social_provider": "google",
|
||||
"code_verifier": null,
|
||||
"redirect_uri": "show-auth-token",
|
||||
"machine_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"interval": 5,
|
||||
"expires_at_unix_secs": 4_102_444_800u64,
|
||||
"status": "pending",
|
||||
"proxy_node_id": "proxy-node-windsurf",
|
||||
"created_at_unix_ms": 1_711_000_000u64,
|
||||
"key_id": null,
|
||||
"email": null,
|
||||
"replaced": false,
|
||||
"error_msg": null,
|
||||
}),
|
||||
);
|
||||
|
||||
let response = local_admin_provider_oauth_response(
|
||||
&state,
|
||||
http::Method::POST,
|
||||
"/api/admin/provider-oauth/providers/provider-windsurf/device-poll",
|
||||
Some(json!({
|
||||
"session_id": "session-windsurf",
|
||||
"callback_url": "https://windsurf.com/show-auth-token?token=firebase-id-token&state=session-windsurf&provider=google"
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
let status = response.status();
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json body should parse");
|
||||
assert_eq!(status, StatusCode::OK, "payload={payload}");
|
||||
assert_eq!(payload["status"], "authorized");
|
||||
assert_eq!(payload["replaced"], false);
|
||||
|
||||
let stored = state
|
||||
.load_provider_oauth_device_session_for_tests("device_auth_session:session-windsurf")
|
||||
.expect("device session should persist");
|
||||
let stored: serde_json::Value =
|
||||
serde_json::from_str(&stored).expect("device session json should parse");
|
||||
assert_eq!(stored["status"], "authorized");
|
||||
let key_id = stored["key_id"]
|
||||
.as_str()
|
||||
.expect("key_id should be stored")
|
||||
.to_string();
|
||||
assert_eq!(payload["key_id"], key_id);
|
||||
|
||||
let persisted = provider_catalog_repository
|
||||
.list_keys_by_ids(std::slice::from_ref(&key_id))
|
||||
.await
|
||||
.expect("keys should load")
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("persisted key should exist");
|
||||
assert_eq!(persisted.auth_type, "oauth");
|
||||
assert_eq!(
|
||||
persisted.proxy,
|
||||
Some(json!({"node_id": "proxy-node-windsurf", "enabled": true}))
|
||||
);
|
||||
let decrypted_api_key = decrypt_python_fernet_ciphertext(
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
persisted
|
||||
.encrypted_api_key
|
||||
.as_deref()
|
||||
.expect("api key should be present"),
|
||||
)
|
||||
.expect("api key should decrypt");
|
||||
assert_eq!(decrypted_api_key, "devin-session-token$registered");
|
||||
let decrypted_auth_config = decrypt_python_fernet_ciphertext(
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
persisted
|
||||
.encrypted_auth_config
|
||||
.as_deref()
|
||||
.expect("auth config should exist"),
|
||||
)
|
||||
.expect("auth config should decrypt");
|
||||
let auth_config: serde_json::Value =
|
||||
serde_json::from_str(&decrypted_auth_config).expect("auth config should parse");
|
||||
assert_eq!(auth_config["provider_type"], "windsurf");
|
||||
assert_eq!(auth_config["auth_method"], "browser");
|
||||
assert_eq!(auth_config["register_source"], "new");
|
||||
assert_eq!(auth_config["social_provider"], "google");
|
||||
|
||||
{
|
||||
let plans = execution_plans.lock().expect("mutex should lock");
|
||||
let register_plan = plans
|
||||
.iter()
|
||||
.find(|plan| plan.request_id == "provider-oauth:windsurf-register:new")
|
||||
.expect("register plan should execute");
|
||||
assert_eq!(register_plan.method, "POST");
|
||||
assert_eq!(
|
||||
register_plan
|
||||
.body
|
||||
.json_body
|
||||
.as_ref()
|
||||
.and_then(|body| body.get("firebase_id_token"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("firebase-id-token")
|
||||
);
|
||||
assert_eq!(
|
||||
register_plan
|
||||
.proxy
|
||||
.as_ref()
|
||||
.and_then(|proxy| proxy.node_id.as_deref()),
|
||||
Some("proxy-node-windsurf")
|
||||
);
|
||||
}
|
||||
|
||||
execution_runtime_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_rejects_windsurf_callback_state_mismatch_and_missing_token() {
|
||||
let mut provider = sample_provider("provider-windsurf", "windsurf", 10);
|
||||
provider.provider_type = "windsurf".to_string();
|
||||
let endpoint = sample_endpoint(
|
||||
"endpoint-windsurf-chat",
|
||||
"provider-windsurf",
|
||||
"openai:chat",
|
||||
"https://server.codeium.com",
|
||||
);
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
vec![endpoint],
|
||||
vec![],
|
||||
));
|
||||
let state = AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(GatewayDataState::with_provider_catalog_reader_for_tests(
|
||||
provider_catalog_repository,
|
||||
))
|
||||
.with_provider_oauth_device_session_entry_for_tests(
|
||||
"session-windsurf",
|
||||
json!({
|
||||
"provider_id": "provider-windsurf",
|
||||
"region": "",
|
||||
"client_id": "",
|
||||
"client_secret": "",
|
||||
"device_code": "",
|
||||
"auth_type": "browser",
|
||||
"social_provider": "google",
|
||||
"code_verifier": null,
|
||||
"redirect_uri": "show-auth-token",
|
||||
"machine_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"interval": 5,
|
||||
"expires_at_unix_secs": 4_102_444_800u64,
|
||||
"status": "pending",
|
||||
"proxy_node_id": null,
|
||||
"created_at_unix_ms": 1_711_000_000u64,
|
||||
"key_id": null,
|
||||
"email": null,
|
||||
"replaced": false,
|
||||
"error_msg": null,
|
||||
}),
|
||||
);
|
||||
|
||||
let response = local_admin_provider_oauth_response(
|
||||
&state,
|
||||
http::Method::POST,
|
||||
"/api/admin/provider-oauth/providers/provider-windsurf/device-poll",
|
||||
Some(json!({
|
||||
"session_id": "session-windsurf",
|
||||
"callback_url": "https://windsurf.com/show-auth-token?token=firebase-id-token&state=wrong-state"
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json body should parse");
|
||||
assert_eq!(payload["status"], "error");
|
||||
assert!(payload["error"]
|
||||
.as_str()
|
||||
.is_some_and(|error| error.contains("state")));
|
||||
|
||||
let response = local_admin_provider_oauth_response(
|
||||
&state,
|
||||
http::Method::POST,
|
||||
"/api/admin/provider-oauth/providers/provider-windsurf/device-poll",
|
||||
Some(json!({
|
||||
"session_id": "session-windsurf",
|
||||
"callback_url": "https://windsurf.com/show-auth-token?state=session-windsurf"
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json body should parse");
|
||||
assert_eq!(payload["status"], "error");
|
||||
assert!(payload["error"]
|
||||
.as_str()
|
||||
.is_some_and(|error| error.contains("token")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_admin_provider_oauth_device_authorize_locally_with_trusted_admin_principal(
|
||||
) {
|
||||
@@ -2477,6 +2880,7 @@ async fn gateway_completes_admin_provider_oauth_key_locally_with_trusted_admin_p
|
||||
.expect("account_state_recheck_error should be string when recheck is attempted");
|
||||
assert!(
|
||||
account_state_recheck_error == "wham/usage API 返回状态码 401"
|
||||
|| account_state_recheck_error == "wham/usage API 返回状态码 403"
|
||||
|| account_state_recheck_error.starts_with("wham/usage 请求执行失败:"),
|
||||
"unexpected account_state_recheck_error: {account_state_recheck_error}"
|
||||
);
|
||||
@@ -4948,6 +5352,7 @@ async fn gateway_refreshes_admin_provider_oauth_key_locally_with_trusted_admin_p
|
||||
.expect("account_state_recheck_error should be string when attempted");
|
||||
assert!(
|
||||
account_state_recheck_error == "wham/usage API 返回状态码 401"
|
||||
|| account_state_recheck_error == "wham/usage API 返回状态码 403"
|
||||
|| account_state_recheck_error.starts_with("wham/usage 请求执行失败:"),
|
||||
"unexpected account_state_recheck_error: {account_state_recheck_error}"
|
||||
);
|
||||
@@ -4997,6 +5402,14 @@ async fn gateway_refreshes_admin_provider_oauth_key_locally_with_trusted_admin_p
|
||||
stored_key.oauth_invalid_reason.as_deref(),
|
||||
Some("[OAUTH_EXPIRED] Codex Token 无效或已过期 (401)")
|
||||
);
|
||||
} else if account_state_recheck_attempted
|
||||
&& payload["account_state_recheck_error"] == "wham/usage API 返回状态码 403"
|
||||
{
|
||||
assert!(stored_key.oauth_invalid_at_unix_secs.is_some());
|
||||
assert!(stored_key
|
||||
.oauth_invalid_reason
|
||||
.as_deref()
|
||||
.is_some_and(|reason| reason.contains("(403)")));
|
||||
} else {
|
||||
assert_eq!(stored_key.oauth_invalid_at_unix_secs, None);
|
||||
assert_eq!(stored_key.oauth_invalid_reason, None);
|
||||
|
||||
@@ -242,6 +242,144 @@ async fn gateway_handles_admin_provider_query_models_fetches_upstream_for_select
|
||||
execution_runtime_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_admin_provider_query_models_fetches_windsurf_model_configs() {
|
||||
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
|
||||
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
|
||||
let execution_runtime = Router::new().route(
|
||||
"/v1/execute/sync",
|
||||
any(move |Json(plan): Json<ExecutionPlan>| {
|
||||
let execution_runtime_hits_inner = Arc::clone(&execution_runtime_hits_clone);
|
||||
async move {
|
||||
*execution_runtime_hits_inner
|
||||
.lock()
|
||||
.expect("mutex should lock") += 1;
|
||||
assert_eq!(plan.method, "POST");
|
||||
assert_eq!(
|
||||
plan.url,
|
||||
"https://server.codeium.com/exa.api_server_pb.ApiServerService/GetCascadeModelConfigs"
|
||||
);
|
||||
assert_eq!(plan.client_api_format, "openai:chat");
|
||||
assert_eq!(plan.provider_api_format, "windsurf:model_configs");
|
||||
assert_eq!(plan.model_name.as_deref(), Some("GetCascadeModelConfigs"));
|
||||
assert_eq!(
|
||||
plan.headers.get("connect-protocol-version").map(String::as_str),
|
||||
Some("1")
|
||||
);
|
||||
assert_eq!(
|
||||
plan.body
|
||||
.json_body
|
||||
.as_ref()
|
||||
.and_then(|body| body.get("metadata"))
|
||||
.and_then(|metadata| metadata.get("apiKey")),
|
||||
Some(&json!("devin-session-token$abc"))
|
||||
);
|
||||
Json(json!({
|
||||
"request_id": "req-provider-query-windsurf",
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"json_body": {
|
||||
"clientModelConfigs": [{
|
||||
"modelUid": "claude-sonnet-4-6",
|
||||
"label": "Claude Sonnet 4.6",
|
||||
"provider": "anthropic",
|
||||
"supportsImages": true,
|
||||
"creditMultiplier": 4
|
||||
}],
|
||||
"defaultOverrideModelConfig": {
|
||||
"modelUid": "claude-sonnet-4-6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
|
||||
let mut provider = sample_provider("provider-windsurf", "Windsurf", 10);
|
||||
provider.provider_type = "windsurf".to_string();
|
||||
let mut windsurf_key = sample_key(
|
||||
"key-windsurf-selected",
|
||||
"provider-windsurf",
|
||||
"openai:chat",
|
||||
"devin-session-token$abc",
|
||||
);
|
||||
windsurf_key.auth_type = "oauth".to_string();
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
vec![StoredProviderCatalogEndpoint::new(
|
||||
"endpoint-windsurf-chat".to_string(),
|
||||
"provider-windsurf".to_string(),
|
||||
"openai:chat".to_string(),
|
||||
Some("chat".to_string()),
|
||||
Some("primary".to_string()),
|
||||
true,
|
||||
)
|
||||
.expect("endpoint should build")
|
||||
.with_transport_fields(
|
||||
"https://server.codeium.com".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("endpoint transport should build")],
|
||||
vec![windsurf_key],
|
||||
));
|
||||
|
||||
let gateway = build_router_with_state(
|
||||
build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
.with_data_state_for_tests(GatewayDataState::with_provider_transport_reader_for_tests(
|
||||
provider_catalog_repository,
|
||||
DEVELOPMENT_ENCRYPTION_KEY.to_string(),
|
||||
)),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!("{gateway_url}/api/admin/provider-query/models"))
|
||||
.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!({
|
||||
"provider_id": "provider-windsurf",
|
||||
"api_key_id": "key-windsurf-selected"
|
||||
}))
|
||||
.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!(true));
|
||||
assert_eq!(payload["data"]["error"], serde_json::Value::Null);
|
||||
assert_eq!(payload["data"]["from_cache"], json!(false));
|
||||
let models = payload["data"]["models"]
|
||||
.as_array()
|
||||
.expect("models should be an array");
|
||||
assert_eq!(models.len(), 1);
|
||||
assert_eq!(models[0]["id"], json!("claude-sonnet-4-6"));
|
||||
assert_eq!(
|
||||
models[0]["api_formats"],
|
||||
json!(["openai:chat", "openai:responses", "claude:messages"])
|
||||
);
|
||||
assert_eq!(
|
||||
*execution_runtime_hits.lock().expect("mutex should lock"),
|
||||
1
|
||||
);
|
||||
|
||||
gateway_handle.abort();
|
||||
execution_runtime_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_admin_provider_query_models_with_openai_responses_endpoint() {
|
||||
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
|
||||
@@ -2245,6 +2383,122 @@ async fn gateway_routes_grok_responses_admin_pool_model_test_through_grok_runtim
|
||||
execution_runtime_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_streams_windsurf_connect_upstream_for_admin_model_test() {
|
||||
let execution_runtime = Router::new().route(
|
||||
"/v1/execute/sync",
|
||||
any(move |Json(plan): Json<ExecutionPlan>| async move {
|
||||
assert_eq!(plan.provider_id, "provider-windsurf");
|
||||
assert_eq!(plan.endpoint_id, "endpoint-windsurf-chat");
|
||||
assert_eq!(plan.key_id, "key-windsurf-primary");
|
||||
assert_eq!(plan.provider_api_format, "openai:chat");
|
||||
assert_eq!(plan.content_type.as_deref(), Some("application/connect+json"));
|
||||
assert!(plan.stream, "Windsurf Connect model test must stream upstream");
|
||||
assert_eq!(
|
||||
plan.body
|
||||
.json_body
|
||||
.as_ref()
|
||||
.and_then(|body| body.get("stream")),
|
||||
Some(&json!(true))
|
||||
);
|
||||
let windsurf_payload = serde_json::to_vec(&json!({
|
||||
"chatMessage": {
|
||||
"text": "ok"
|
||||
}
|
||||
}))
|
||||
.expect("windsurf payload should encode");
|
||||
let mut windsurf_frame = vec![0u8];
|
||||
windsurf_frame.extend_from_slice(&(windsurf_payload.len() as u32).to_be_bytes());
|
||||
windsurf_frame.extend_from_slice(&windsurf_payload);
|
||||
Json(json!({
|
||||
"request_id": plan.request_id,
|
||||
"candidate_id": plan.candidate_id,
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"content-type": "application/connect+json"
|
||||
},
|
||||
"body": {
|
||||
"body_bytes_b64": base64::engine::general_purpose::STANDARD.encode(windsurf_frame)
|
||||
},
|
||||
"telemetry": {
|
||||
"elapsed_ms": 24
|
||||
}
|
||||
}))
|
||||
}),
|
||||
);
|
||||
|
||||
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
|
||||
let mut provider = sample_provider("provider-windsurf", "Windsurf", 10);
|
||||
provider.provider_type = "windsurf".to_string();
|
||||
let mut key = sample_key(
|
||||
"key-windsurf-primary",
|
||||
"provider-windsurf",
|
||||
"openai:chat",
|
||||
"devin-session-token$abc",
|
||||
);
|
||||
key.auth_type = "oauth".to_string();
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
vec![sample_endpoint(
|
||||
"endpoint-windsurf-chat",
|
||||
"provider-windsurf",
|
||||
"openai:chat",
|
||||
"https://server.codeium.com",
|
||||
)],
|
||||
vec![key],
|
||||
));
|
||||
|
||||
let gateway = build_router_with_state(
|
||||
build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
.with_data_state_for_tests(GatewayDataState::with_provider_transport_reader_for_tests(
|
||||
provider_catalog_repository,
|
||||
DEVELOPMENT_ENCRYPTION_KEY.to_string(),
|
||||
)),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!("{gateway_url}/api/admin/provider-query/test-model"))
|
||||
.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!({
|
||||
"provider_id": "provider-windsurf",
|
||||
"model": "claude-opus-4-7-medium",
|
||||
"api_format": "openai:chat",
|
||||
"endpoint_id": "endpoint-windsurf-chat",
|
||||
"request_body": {
|
||||
"model": "claude-opus-4-7-medium",
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": "Hello! This is a test message."
|
||||
}],
|
||||
"max_tokens": 30,
|
||||
"temperature": 0.7,
|
||||
"stream": true
|
||||
}
|
||||
}))
|
||||
.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!(true));
|
||||
assert_eq!(
|
||||
payload["attempts"][0]["request_body"]["stream"],
|
||||
json!(true)
|
||||
);
|
||||
assert_eq!(
|
||||
payload["attempts"][0]["response_body"]["choices"][0]["message"]["content"],
|
||||
json!("ok")
|
||||
);
|
||||
|
||||
gateway_handle.abort();
|
||||
execution_runtime_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_uses_pool_scheduler_order_for_admin_pool_model_test() {
|
||||
let execution_runtime = Router::new().route(
|
||||
|
||||
Reference in New Issue
Block a user