use std::io::Read;
use std::io::Write;
use std::sync::Arc;
use std::time::Duration;
use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn client_preserves_supplied_http_client_factory_policy() {
let client = Client::new(
"https://example.test",
HttpClientFactory::new(OutboundProxyPolicy::RespectSystemProxy),
);
assert_eq!(
client.http.outbound_proxy_policy(),
OutboundProxyPolicy::RespectSystemProxy
);
}
#[test]
fn list_tasks_url_omits_empty_query_and_encodes_all_parameters() {
let client = Client::new(
"https://example.test",
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
);
assert_eq!(
client
.list_tasks_url(
None, None, None,
None,
)
.unwrap(),
"https://example.test/api/codex/tasks/list"
);
assert_eq!(
client
.list_tasks_url(
Some(10),
Some("mine / shared"),
Some("env&one"),
Some("next=page"),
)
.unwrap(),
"https://example.test/api/codex/tasks/list?limit=10&task_filter=mine+%2F+shared&cursor=next%3Dpage&environment_id=env%26one"
);
}
#[tokio::test]
async fn migrated_requests_preserve_query_auth_and_json_body() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("HTTP listener should bind");
let address = listener
.local_addr()
.expect("HTTP listener should have an address");
let server = std::thread::spawn(move || {
let mut requests = Vec::new();
for body in [r#"{"items":[]}"#, r#"{"task":{"id":"task-created"}}"#] {
let (mut stream, _) = listener.accept().expect("HTTP listener should accept");
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.expect("HTTP stream should get a read timeout");
let mut request = Vec::new();
let mut buffer = [0_u8; 4096];
loop {
let size = stream.read(&mut buffer).expect("HTTP request should read");
if size == 0 {
break;
}
request.extend_from_slice(&buffer[..size]);
let Some(headers_end) = request.windows(4).position(|part| part == b"\r\n\r\n")
else {
continue;
};
let headers = String::from_utf8_lossy(&request[..headers_end]);
let content_length = headers
.lines()
.find_map(|line| {
let (name, value) = line.split_once(':')?;
name.eq_ignore_ascii_case("content-length")
.then(|| value.trim().parse::<usize>().ok())
.flatten()
})
.unwrap_or(0);
if request.len() >= headers_end + 4 + content_length {
break;
}
}
requests.push(String::from_utf8(request).expect("request should be UTF-8"));
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
)
.expect("HTTP response should write");
}
requests
});
let client = Client::new(
format!("http://{address}"),
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
)
.with_auth_provider(Arc::new(codex_model_provider::BearerAuthProvider::new(
"request-token".to_string(),
)));
let tasks = client
.list_tasks(
Some(10),
Some("mine / shared"),
Some("env&one"),
Some("next=page"),
)
.await
.expect("list request should succeed");
let task_id = client
.create_task(serde_json::json!({ "prompt": "hello" }))
.await
.expect("create request should succeed");
let requests = server.join().expect("HTTP server should finish");
assert_eq!(tasks, PaginatedListTaskListItem::new(Vec::new()));
assert_eq!(task_id, "task-created");
assert_eq!(requests.len(), 2);
assert!(requests[0].starts_with(
"GET /api/codex/tasks/list?limit=10&task_filter=mine+%2F+shared&cursor=next%3Dpage&environment_id=env%26one HTTP/1.1\r\n"
));
assert!(
requests[0]
.to_ascii_lowercase()
.contains("authorization: bearer request-token\r\n")
);
assert!(requests[1].starts_with("POST /api/codex/tasks HTTP/1.1\r\n"));
assert!(
requests[1]
.to_ascii_lowercase()
.contains("authorization: bearer request-token\r\n")
);
assert!(requests[1].ends_with(r#"{"prompt":"hello"}"#));
}