Files
codex/codex-rs/codex-api/src/api_bridge_tests.rs
2026-05-20 22:39:19 -07:00

277 lines
8.5 KiB
Rust

use super::*;
use base64::Engine;
use pretty_assertions::assert_eq;
fn map_bad_request(body: String) -> CodexErr {
map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::BAD_REQUEST,
url: Some("http://example.com/v1/responses".to_string()),
headers: None,
body: Some(body),
}))
}
#[test]
fn map_api_error_maps_server_overloaded() {
let err = map_api_error(ApiError::ServerOverloaded);
assert!(matches!(err, CodexErr::ServerOverloaded));
}
#[test]
fn map_api_error_maps_server_overloaded_from_503_body() {
let body = serde_json::json!({
"error": {
"code": "server_is_overloaded"
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::SERVICE_UNAVAILABLE,
url: Some("http://example.com/v1/responses".to_string()),
headers: None,
body: Some(body),
}));
assert!(matches!(err, CodexErr::ServerOverloaded));
}
#[test]
fn map_api_error_maps_cyber_policy_from_400_body() {
let body = serde_json::json!({
"error": {
"message": "This request has been flagged for potentially high-risk cyber activity.",
"type": "invalid_request",
"param": null,
"code": "cyber_policy"
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::BAD_REQUEST,
url: Some("http://example.com/v1/responses".to_string()),
headers: None,
body: Some(body),
}));
let CodexErr::CyberPolicy { message } = err else {
panic!("expected CodexErr::CyberPolicy, got {err:?}");
};
assert_eq!(
message,
"This request has been flagged for potentially high-risk cyber activity."
);
}
#[test]
fn map_api_error_maps_wrapped_websocket_cyber_policy_from_400_body() {
let body = serde_json::json!({
"type": "error",
"status": 400,
"error": {
"message": "This websocket request was flagged.",
"type": "invalid_request",
"code": "cyber_policy"
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::BAD_REQUEST,
url: Some("ws://example.com/v1/responses".to_string()),
headers: None,
body: Some(body),
}));
let CodexErr::CyberPolicy { message } = err else {
panic!("expected CodexErr::CyberPolicy, got {err:?}");
};
assert_eq!(message, "This websocket request was flagged.");
}
#[test]
fn map_api_error_uses_cyber_policy_fallback_for_missing_message() {
let body = serde_json::json!({
"error": {
"code": "cyber_policy"
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::BAD_REQUEST,
url: Some("http://example.com/v1/responses".to_string()),
headers: None,
body: Some(body),
}));
let CodexErr::CyberPolicy { message } = err else {
panic!("expected CodexErr::CyberPolicy, got {err:?}");
};
assert_eq!(
message,
"This request has been flagged for possible cybersecurity risk."
);
}
#[test]
fn map_api_error_keeps_unknown_400_errors_generic() {
let body = serde_json::json!({
"error": {
"message": "Some other bad request.",
"code": "some_other_policy"
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::BAD_REQUEST,
url: Some("http://example.com/v1/responses".to_string()),
headers: None,
body: Some(body.clone()),
}));
let CodexErr::InvalidRequest(message) = err else {
panic!("expected CodexErr::InvalidRequest, got {err:?}");
};
assert_eq!(message, body);
}
#[test]
fn map_api_error_maps_image_400s_to_invalid_image_request() {
let cases = [
serde_json::json!({
"error": {
"message": "The image data you provided does not represent a valid image. Please check your input and try again.",
"type": "invalid_request_error",
"param": "input[0].content[1].image_url",
"code": "invalid_value"
}
}),
serde_json::json!({
"type": "error",
"status": 400,
"error": {
"type": "invalid_request_error",
"code": "invalid_value",
"message": "Error while downloading http://127.0.0.1:8787/image.png. Upstream status code: 407.",
"param": "url"
}
}),
serde_json::json!({
"error": {
"message": "Expected a base64-encoded data URL with an image MIME type, but got empty base64-encoded bytes.",
"code": "invalid_value"
}
}),
];
for body in cases {
assert!(matches!(
map_bad_request(body.to_string()),
CodexErr::InvalidImageRequest()
));
}
}
#[test]
fn map_api_error_maps_usage_limit_limit_name_header() {
let mut headers = HeaderMap::new();
headers.insert(
ACTIVE_LIMIT_HEADER,
http::HeaderValue::from_static("codex_other"),
);
headers.insert(
"x-codex-other-limit-name",
http::HeaderValue::from_static("codex_other"),
);
let body = serde_json::json!({
"error": {
"type": "usage_limit_reached",
"plan_type": "pro",
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::TOO_MANY_REQUESTS,
url: Some("http://example.com/v1/responses".to_string()),
headers: Some(headers),
body: Some(body),
}));
let CodexErr::UsageLimitReached(usage_limit) = err else {
panic!("expected CodexErr::UsageLimitReached, got {err:?}");
};
assert_eq!(
usage_limit
.rate_limits
.as_ref()
.and_then(|snapshot| snapshot.limit_name.as_deref()),
Some("codex_other")
);
}
#[test]
fn map_api_error_does_not_fallback_limit_name_to_limit_id() {
let mut headers = HeaderMap::new();
headers.insert(
ACTIVE_LIMIT_HEADER,
http::HeaderValue::from_static("codex_other"),
);
let body = serde_json::json!({
"error": {
"type": "usage_limit_reached",
"plan_type": "pro",
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::TOO_MANY_REQUESTS,
url: Some("http://example.com/v1/responses".to_string()),
headers: Some(headers),
body: Some(body),
}));
let CodexErr::UsageLimitReached(usage_limit) = err else {
panic!("expected CodexErr::UsageLimitReached, got {err:?}");
};
assert_eq!(
usage_limit
.rate_limits
.as_ref()
.and_then(|snapshot| snapshot.limit_name.as_deref()),
None
);
}
#[test]
fn map_api_error_extracts_identity_auth_details_from_headers() {
let mut headers = HeaderMap::new();
headers.insert(REQUEST_ID_HEADER, http::HeaderValue::from_static("req-401"));
headers.insert(CF_RAY_HEADER, http::HeaderValue::from_static("ray-401"));
headers.insert(
X_OPENAI_AUTHORIZATION_ERROR_HEADER,
http::HeaderValue::from_static("missing_authorization_header"),
);
let x_error_json =
base64::engine::general_purpose::STANDARD.encode(r#"{"error":{"code":"token_expired"}}"#);
headers.insert(
X_ERROR_JSON_HEADER,
http::HeaderValue::from_str(&x_error_json).expect("valid x-error-json header"),
);
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::UNAUTHORIZED,
url: Some("https://chatgpt.com/backend-api/codex/models".to_string()),
headers: Some(headers),
body: Some(r#"{"detail":"Unauthorized"}"#.to_string()),
}));
let CodexErr::UnexpectedStatus(err) = err else {
panic!("expected CodexErr::UnexpectedStatus, got {err:?}");
};
assert_eq!(err.request_id.as_deref(), Some("req-401"));
assert_eq!(err.cf_ray.as_deref(), Some("ray-401"));
assert_eq!(
err.identity_authorization_error.as_deref(),
Some("missing_authorization_header")
);
assert_eq!(err.identity_error_code.as_deref(), Some("token_expired"));
}