mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
test: cover account connection tests
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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<Value> {
|
||||
parse_sse_events(body)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|event| serde_json::from_str::<Value>(&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);
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user