mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-14 08:06:40 +00:00
218 lines
7.4 KiB
Rust
218 lines
7.4 KiB
Rust
use super::*;
|
|
|
|
#[test]
|
|
fn endpoints_should_join_backend_paths() {
|
|
assert_eq!(
|
|
codex_proxy_rs::codex::transport::endpoint_url(
|
|
"https://api.example.com/",
|
|
"/codex/responses"
|
|
),
|
|
"https://api.example.com/codex/responses"
|
|
);
|
|
assert_eq!(
|
|
codex_proxy_rs::codex::transport::endpoint_request_path(
|
|
"https://api.example.com/backend-api",
|
|
"/codex/usage"
|
|
),
|
|
"/backend-api/codex/usage"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn custom_ca_should_report_environment_cache_key_consistently() {
|
|
const CASE_ENV: &str = "CODEX_PROXY_TEST_CUSTOM_CA_CACHE_KEY_CASE";
|
|
const SSL_CERT_PATH: &str = "/tmp/codex-proxy-ssl-cert-file.pem";
|
|
const CODEX_CA_PATH: &str = "/tmp/codex-proxy-codex-ca-certificate.pem";
|
|
|
|
if let Ok(case) = std::env::var(CASE_ENV) {
|
|
let expected = match case.as_str() {
|
|
"unset" => None,
|
|
"ssl_cert_file" => Some(format!(
|
|
"{}={SSL_CERT_PATH}",
|
|
codex_proxy_rs::codex::transport::SSL_CERT_FILE_ENV
|
|
)),
|
|
"codex_ca_priority" => Some(format!(
|
|
"{}={CODEX_CA_PATH}",
|
|
codex_proxy_rs::codex::transport::CODEX_CA_CERT_ENV
|
|
)),
|
|
_ => panic!("unknown custom CA cache key test case: {case}"),
|
|
};
|
|
|
|
assert_eq!(
|
|
codex_proxy_rs::codex::transport::custom_ca_env_cache_key(),
|
|
expected
|
|
);
|
|
return;
|
|
}
|
|
|
|
let current_exe = std::env::current_exe().expect("current test binary path");
|
|
let cases = [
|
|
("unset", None, None),
|
|
(
|
|
"ssl_cert_file",
|
|
None,
|
|
Some((
|
|
codex_proxy_rs::codex::transport::SSL_CERT_FILE_ENV,
|
|
SSL_CERT_PATH,
|
|
)),
|
|
),
|
|
(
|
|
"codex_ca_priority",
|
|
Some((
|
|
codex_proxy_rs::codex::transport::CODEX_CA_CERT_ENV,
|
|
CODEX_CA_PATH,
|
|
)),
|
|
Some((
|
|
codex_proxy_rs::codex::transport::SSL_CERT_FILE_ENV,
|
|
SSL_CERT_PATH,
|
|
)),
|
|
),
|
|
];
|
|
|
|
for (case, codex_ca, ssl_cert_file) in cases {
|
|
let mut command = Command::new(¤t_exe);
|
|
command
|
|
.arg("--exact")
|
|
.arg("custom_ca_should_report_environment_cache_key_consistently")
|
|
.arg("--nocapture")
|
|
.env(CASE_ENV, case)
|
|
.env_remove(codex_proxy_rs::codex::transport::CODEX_CA_CERT_ENV)
|
|
.env_remove(codex_proxy_rs::codex::transport::SSL_CERT_FILE_ENV);
|
|
if let Some((key, value)) = codex_ca {
|
|
command.env(key, value);
|
|
}
|
|
if let Some((key, value)) = ssl_cert_file {
|
|
command.env(key, value);
|
|
}
|
|
|
|
let output = command.output().expect("run isolated custom CA case");
|
|
assert!(
|
|
output.status.success(),
|
|
"isolated custom CA case {case} failed\nstdout:\n{}\nstderr:\n{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn codex_backend_client_probe_should_return_models_endpoint_status() {
|
|
let server = wiremock::MockServer::start().await;
|
|
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
|
.and(wiremock::matchers::path("/codex/models"))
|
|
.respond_with(wiremock::ResponseTemplate::new(204))
|
|
.mount(&server)
|
|
.await;
|
|
let client = CodexBackendClient::new(
|
|
reqwest::Client::builder().no_proxy().build().unwrap(),
|
|
server.uri(),
|
|
codex_proxy_rs::codex::fingerprint::Fingerprint::default_for_tests(),
|
|
);
|
|
|
|
let probe = client
|
|
.probe_connectivity(CodexRequestContext {
|
|
access_token: "access-token",
|
|
account_id: Some("chatgpt-account"),
|
|
request_id: "req_probe",
|
|
turn_state: None,
|
|
turn_metadata: None,
|
|
beta_features: None,
|
|
include_timing_metrics: None,
|
|
version: None,
|
|
codex_window_id: None,
|
|
parent_thread_id: None,
|
|
cookie_header: None,
|
|
installation_id: None,
|
|
session_id: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(probe.status, reqwest::StatusCode::NO_CONTENT);
|
|
assert!(probe
|
|
.endpoint
|
|
.ends_with("/codex/models?client_version=26.519.81530"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn codex_backend_client_should_cap_non_success_error_body_at_one_mib() {
|
|
let server = wiremock::MockServer::start().await;
|
|
let large_error_body = "x".repeat(1024 * 1024 + 17);
|
|
wiremock::Mock::given(wiremock::matchers::method("POST"))
|
|
.and(wiremock::matchers::path("/codex/responses"))
|
|
.respond_with(wiremock::ResponseTemplate::new(500).set_body_string(large_error_body))
|
|
.mount(&server)
|
|
.await;
|
|
let client = CodexBackendClient::new(
|
|
reqwest::Client::builder().no_proxy().build().unwrap(),
|
|
server.uri(),
|
|
codex_proxy_rs::codex::fingerprint::Fingerprint::default_for_tests(),
|
|
);
|
|
let mut request =
|
|
codex_proxy_rs::codex::protocol::responses::CodexResponsesRequest::new_http_sse(
|
|
"gpt-5.5",
|
|
"",
|
|
Vec::new(),
|
|
);
|
|
request.force_http_sse = true;
|
|
|
|
let result = client
|
|
.create_response(
|
|
&request,
|
|
CodexRequestContext {
|
|
access_token: "access-token",
|
|
account_id: Some("chatgpt-account"),
|
|
request_id: "req_large_error",
|
|
turn_state: None,
|
|
turn_metadata: None,
|
|
beta_features: None,
|
|
include_timing_metrics: None,
|
|
version: None,
|
|
codex_window_id: None,
|
|
parent_thread_id: None,
|
|
cookie_header: None,
|
|
installation_id: None,
|
|
session_id: None,
|
|
},
|
|
)
|
|
.await;
|
|
|
|
let Err(CodexClientError::Upstream { status, body, .. }) = result else {
|
|
panic!("expected upstream error");
|
|
};
|
|
assert_eq!(status, reqwest::StatusCode::INTERNAL_SERVER_ERROR);
|
|
assert_eq!(body.len(), 1024 * 1024);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn build_reqwest_client_should_reuse_cached_connection_pool() {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let server = tokio::spawn(async move {
|
|
let (mut first_stream, _) = listener.accept().await.unwrap();
|
|
read_http_request(&mut first_stream).await;
|
|
write_empty_http_response(&mut first_stream).await;
|
|
|
|
tokio::select! {
|
|
request = read_http_request(&mut first_stream) => {
|
|
write_empty_http_response(&mut first_stream).await;
|
|
!request.is_empty()
|
|
}
|
|
accepted = listener.accept() => {
|
|
let (mut second_stream, _) = accepted.unwrap();
|
|
read_http_request(&mut second_stream).await;
|
|
write_empty_http_response(&mut second_stream).await;
|
|
false
|
|
}
|
|
_ = tokio::time::sleep(Duration::from_millis(500)) => false,
|
|
}
|
|
});
|
|
|
|
let url = format!("http://{addr}/reuse");
|
|
let client = reqwest::Client::builder().no_proxy().build().unwrap();
|
|
client.get(&url).send().await.unwrap().text().await.unwrap();
|
|
client.get(&url).send().await.unwrap().text().await.unwrap();
|
|
|
|
assert!(server.await.unwrap());
|
|
}
|