use super::*; use base64::Engine; use pretty_assertions::assert_eq; #[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_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")); }