From fcf058f4d8c25258b203f64d82fa2a40be8f422f Mon Sep 17 00:00:00 2001 From: zyycn Date: Sat, 27 Jun 2026 04:21:37 +0800 Subject: [PATCH] test: cover account connection tests Co-authored-by: Codex --- tests/admin/accounts/mod.rs | 1 + tests/admin/accounts/testing.rs | 182 ++++++++++++++++++++++++++++++++ tests/admin/settings/mod.rs | 50 +-------- 3 files changed, 188 insertions(+), 45 deletions(-) create mode 100644 tests/admin/accounts/testing.rs diff --git a/tests/admin/accounts/mod.rs b/tests/admin/accounts/mod.rs index 0fa12a72c..4e23700a9 100644 --- a/tests/admin/accounts/mod.rs +++ b/tests/admin/accounts/mod.rs @@ -36,6 +36,7 @@ mod import_export; mod lifecycle; mod list; mod quota; +mod testing; async fn seed_admin_session(pool: &SqlitePool, session_id: &str) { sqlx::query( diff --git a/tests/admin/accounts/testing.rs b/tests/admin/accounts/testing.rs new file mode 100644 index 000000000..00ed45163 --- /dev/null +++ b/tests/admin/accounts/testing.rs @@ -0,0 +1,182 @@ +use super::*; +use codex_proxy_rs::upstream::protocol::sse::{encode_sse_event, parse_sse_events}; +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; + +async fn seed_test_account(pool: &SqlitePool, secret_box: SecretBox) { + seed_encrypted_account( + pool, + secret_box, + NewAccount { + id: "acct_test".to_string(), + email: Some("test@example.com".to_string()), + account_id: Some("chatgpt-test".to_string()), + user_id: None, + label: None, + plan_type: Some("plus".to_string()), + access_token: SecretString::new("access-test".to_string().into()), + refresh_token: None, + access_token_expires_at: None, + status: AccountStatus::Active, + added_at: None, + }, + ) + .await; +} + +fn test_events(body: &str) -> Vec { + parse_sse_events(body) + .unwrap() + .into_iter() + .map(|event| serde_json::from_str::(&event.data).unwrap()) + .collect() +} + +#[tokio::test] +async fn account_test_models_should_return_upstream_models_only() { + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/backend-api/codex/models")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "models": [ + { + "slug": "gpt-5.5", + "display_name": "GPT 5.5" + }, + { + "id": "gpt-5.4", + "title": "GPT 5.4" + } + ] + }))) + .mount(&server) + .await; + let (app, _state, pool, _dir, secret_box) = admin_accounts_test_app_with_api_base_url( + "admin-account-test-models.sqlite", + 91, + format!("{}/backend-api", server.uri()), + ) + .await; + seed_test_account(&pool, secret_box).await; + + let response = app + .oneshot( + Request::builder() + .method("GET") + .uri("/api/admin/accounts/acct_test/test-models") + .header("cookie", "cpr_admin_session=session_1") + .header("x-request-id", "req_account_test_models") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + let requests = server.received_requests().await.unwrap(); + let upstream = requests + .iter() + .find(|request| request.url.path() == "/backend-api/codex/models") + .expect("models request should hit upstream"); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + upstream + .headers + .get("authorization") + .and_then(|value| value.to_str().ok()), + Some("Bearer access-test") + ); + assert_eq!( + upstream + .headers + .get("chatgpt-account-id") + .and_then(|value| value.to_str().ok()), + Some("chatgpt-test") + ); + let body = response_json(response).await; + assert_eq!(body["data"]["models"][0]["id"], "gpt-5.5"); + assert_eq!(body["data"]["models"][0]["label"], "GPT 5.5"); + assert_eq!(body["data"]["models"][1]["id"], "gpt-5.4"); + assert_eq!(body["data"]["models"][1]["label"], "GPT 5.4"); +} + +#[tokio::test] +async fn account_test_stream_should_translate_upstream_responses_sse() { + let server = MockServer::start().await; + let upstream_sse = [ + encode_sse_event( + "", + &json!({ + "type": "response.output_text.delta", + "delta": "ok" + }) + .to_string(), + ), + encode_sse_event("", &json!({ "type": "response.completed" }).to_string()), + ] + .join(""); + Mock::given(method("POST")) + .and(path("/backend-api/codex/responses")) + .respond_with( + ResponseTemplate::new(200) + .insert_header("content-type", "text/event-stream") + .set_body_string(upstream_sse), + ) + .mount(&server) + .await; + let (app, _state, pool, _dir, secret_box) = admin_accounts_test_app_with_api_base_url( + "admin-account-test-stream.sqlite", + 92, + format!("{}/backend-api", server.uri()), + ) + .await; + seed_test_account(&pool, secret_box).await; + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/admin/accounts/acct_test/test") + .header("content-type", "application/json") + .header("cookie", "cpr_admin_session=session_1") + .header("x-request-id", "req_account_test_stream") + .body(Body::from(json!({ "modelId": "gpt-5.5" }).to_string())) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response + .headers() + .get("content-type") + .and_then(|value| value.to_str().ok()), + Some("text/event-stream") + ); + + let body = String::from_utf8( + to_bytes(response.into_body(), usize::MAX) + .await + .unwrap() + .to_vec(), + ) + .unwrap(); + let events = test_events(&body); + let requests = server.received_requests().await.unwrap(); + let upstream = requests + .iter() + .find(|request| request.url.path() == "/backend-api/codex/responses") + .expect("test request should hit responses upstream"); + let upstream_body: Value = serde_json::from_slice(&upstream.body).unwrap(); + + assert_eq!(events[0]["type"], "test_start"); + assert_eq!(events[0]["model"], "gpt-5.5"); + assert_eq!(events[1]["type"], "content"); + assert_eq!(events[1]["text"], "ok"); + assert_eq!(events[2]["type"], "test_complete"); + assert_eq!(events[2]["success"], true); + assert_eq!(upstream_body["model"], "gpt-5.5"); + assert_eq!(upstream_body["stream"], true); + assert_eq!(upstream_body["store"], false); +} diff --git a/tests/admin/settings/mod.rs b/tests/admin/settings/mod.rs index 6da302737..bf1af306e 100644 --- a/tests/admin/settings/mod.rs +++ b/tests/admin/settings/mod.rs @@ -1,74 +1,34 @@ pub mod routes; -use std::collections::BTreeMap; - -use codex_proxy_rs::config::settings::{ - AdminQuotaWarningThresholds, AdminSettings, AdminSettingsPatch, SettingsService, -}; +use codex_proxy_rs::config::settings::{AdminSettings, AdminSettingsPatch, SettingsService}; #[test] fn settings_service_should_apply_retained_settings_patch() { let mut current = AdminSettings { default_model: "gpt-5.5".to_string(), default_reasoning_effort: Some("high".to_string()), - service_tier: Some("flex".to_string()), - model_aliases: BTreeMap::from([("codex-fast".to_string(), "gpt-5.5".to_string())]), - refresh_enabled: true, - refresh_margin_seconds: 240, - refresh_concurrency: 2, max_concurrent_per_account: 4, request_interval_ms: 50, rotation_strategy: "least_used".to_string(), - tier_priority: vec!["team".to_string(), "plus".to_string()], - quota_refresh_interval_minutes: 5, - quota_warning_thresholds: AdminQuotaWarningThresholds { - primary: vec![80, 90], - secondary: vec![70, 95], - }, - quota_skip_exhausted: true, - logs_enabled: true, - logs_capacity: 2_000, - logs_capture_body: false, - usage_history_retention_days: Some(30), + ..AdminSettings::default() }; SettingsService::apply_patch( &mut current, AdminSettingsPatch { default_model: Some(" gpt-6 ".to_string()), - default_reasoning_effort: Some("medium".to_string()), - service_tier: Some("priority".to_string()), - model_aliases: Some(BTreeMap::from([( - "fast".to_string(), - "gpt-6-fast".to_string(), - )])), - refresh_enabled: Some(false), - refresh_margin_seconds: Some(180), - refresh_concurrency: Some(3), max_concurrent_per_account: Some(5), request_interval_ms: Some(125), rotation_strategy: Some("round_robin".to_string()), - tier_priority: Some(vec!["pro".to_string(), "plus".to_string()]), - quota_refresh_interval_minutes: Some(15), - quota_warning_thresholds: Some(AdminQuotaWarningThresholds { - primary: vec![75, 90], - secondary: vec![65, 95], - }), - quota_skip_exhausted: Some(false), - logs_enabled: Some(false), - logs_capacity: Some(3_000), - logs_capture_body: Some(true), - usage_history_retention_days: Some(60), }, ) .expect("settings patch should be valid"); assert_eq!(current.default_model, "gpt-6"); assert_eq!(current.rotation_strategy, "round_robin"); - assert_eq!(current.quota_warning_thresholds.primary, vec![75, 90]); - assert_eq!(current.usage_history_retention_days, Some(60)); - assert!(!current.refresh_enabled); - assert!(current.logs_capture_body); + assert_eq!(current.max_concurrent_per_account, 5); + assert_eq!(current.request_interval_ms, 125); + assert_eq!(current.default_reasoning_effort.as_deref(), Some("high")); } #[test]