cc-switch-tui 0.1.4

All-in-One Assistant for Claude Code, Codex, Gemini, OpenCode, OpenClaw & Hermes
Documentation
use axum::{body::to_bytes, http::StatusCode};
use bytes::Bytes;
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use serde_json::json;

use super::*;

async fn buffered_body(response: Response) -> Bytes {
    to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("read buffered response body")
}

#[tokio::test]
async fn non_success_parse_failures_fall_back_to_upstream_response() {
    let mut headers = HeaderMap::new();
    headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));

    let prepared = build_buffered_json_response(
        reqwest::StatusCode::BAD_REQUEST,
        &headers,
        Bytes::from_static(br#"{not-json"#),
        |_| Ok(json!({"type": "error"})),
    )
    .expect("fallback to raw upstream response");

    assert_eq!(prepared.response.status(), StatusCode::BAD_REQUEST);
    assert_eq!(
        prepared
            .response
            .headers()
            .get(CONTENT_TYPE)
            .and_then(|value| value.to_str().ok()),
        Some("application/json")
    );
    assert_eq!(
        buffered_body(prepared.response).await,
        Bytes::from_static(br#"{not-json"#)
    );
}

#[tokio::test]
async fn non_success_transform_failures_fall_back_to_upstream_response() {
    let mut headers = HeaderMap::new();
    headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));

    let prepared = build_buffered_json_response(
        reqwest::StatusCode::BAD_REQUEST,
        &headers,
        Bytes::from_static(br#"{"message":"upstream rejected the request"}"#),
        |_| {
            Err(ProxyError::TransformError(
                "missing error envelope".to_string(),
            ))
        },
    )
    .expect("fallback to raw upstream response");

    assert_eq!(prepared.response.status(), StatusCode::BAD_REQUEST);
    assert_eq!(
        buffered_body(prepared.response).await,
        Bytes::from_static(br#"{"message":"upstream rejected the request"}"#)
    );
}

#[test]
fn non_success_non_transform_failures_preserve_original_proxy_error() {
    let headers = HeaderMap::new();
    let result = build_buffered_json_response(
        reqwest::StatusCode::BAD_REQUEST,
        &headers,
        Bytes::from_static(br#"{"message":"upstream rejected the request"}"#),
        |_| {
            Err(ProxyError::Timeout(
                "proxy transform pipeline broke".to_string(),
            ))
        },
    );

    match result {
        Ok(_) => panic!("non-transform errors must not fall back to upstream passthrough"),
        Err(ProxyError::Timeout(message)) => {
            assert_eq!(message, "proxy transform pipeline broke");
        }
        Err(other) => panic!("expected original proxy error, got {other:?}"),
    }
}

#[test]
fn success_parse_failures_use_proxy_request_failed_errors() {
    let headers = HeaderMap::new();
    let result = build_buffered_json_response(
        reqwest::StatusCode::OK,
        &headers,
        Bytes::from_static(br#"{not-json"#),
        |_| Ok(json!({"type": "message"})),
    );

    match result {
        Ok(_) => panic!("success responses should still fail on malformed upstream json"),
        Err(ProxyError::RequestFailed(message)) => {
            assert!(message.contains("parse upstream json failed"));
        }
        Err(other) => panic!("expected request failed error, got {other:?}"),
    }
}

#[test]
fn success_transform_failures_use_proxy_request_failed_errors() {
    let headers = HeaderMap::new();
    let result = build_buffered_json_response(
        reqwest::StatusCode::OK,
        &headers,
        Bytes::from_static(br#"{"message":"upstream accepted the request"}"#),
        |_| {
            Err(ProxyError::TransformError(
                "missing success envelope".to_string(),
            ))
        },
    );

    match result {
        Ok(_) => panic!("success responses must surface transform failures as proxy errors"),
        Err(ProxyError::RequestFailed(message)) => {
            assert!(message.contains("transform upstream json failed"));
            assert!(message.contains("missing success envelope"));
        }
        Err(other) => panic!("expected request failed error, got {other:?}"),
    }
}

#[tokio::test]
async fn non_success_standard_json_errors_can_still_transform() {
    let mut headers = HeaderMap::new();
    headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));

    let prepared = build_buffered_json_response(
        reqwest::StatusCode::BAD_REQUEST,
        &headers,
        Bytes::from_static(
            br#"{"error":{"message":"upstream rejected the request","type":"invalid_request_error"}}"#,
        ),
        |body| {
            assert_eq!(
                body,
                json!({
                    "error": {
                        "message": "upstream rejected the request",
                        "type": "invalid_request_error"
                    }
                })
            );
            Ok(json!({
                "type": "error",
                "error": {
                    "type": "invalid_request_error",
                    "message": "upstream rejected the request"
                }
            }))
        },
    )
    .expect("standard upstream json errors should still transform");

    assert_eq!(prepared.response.status(), StatusCode::BAD_REQUEST);
    assert_eq!(
        buffered_body(prepared.response).await,
        Bytes::from_static(
            br#"{"error":{"message":"upstream rejected the request","type":"invalid_request_error"},"type":"error"}"#,
        )
    );
}