use cdk_http_client::{HttpClient, HttpClientBuilder, HttpError};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct TestPayload {
name: String,
value: i32,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct TestResponse {
success: bool,
data: String,
}
#[tokio::test]
async fn test_fetch_success() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/data")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"success": true, "data": "hello"}"#)
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/data", server.url());
let result: Result<TestResponse, _> = client.fetch(&url).await;
assert!(result.is_ok());
let response = result.expect("Fetch should succeed");
assert!(response.success);
assert_eq!(response.data, "hello");
mock.assert_async().await;
}
#[tokio::test]
async fn test_fetch_error_status() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/error")
.with_status(404)
.with_body("Not Found")
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/error", server.url());
let result: Result<TestResponse, _> = client.fetch(&url).await;
assert!(result.is_err());
if let Err(HttpError::Status { status, message }) = result {
assert_eq!(status, 404);
assert_eq!(message, "Not Found");
} else {
panic!("Expected HttpError::Status");
}
mock.assert_async().await;
}
#[tokio::test]
async fn test_fetch_server_error() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/server-error")
.with_status(500)
.with_body("Internal Server Error")
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/server-error", server.url());
let result: Result<TestResponse, _> = client.fetch(&url).await;
assert!(result.is_err());
if let Err(HttpError::Status { status, .. }) = result {
assert_eq!(status, 500);
} else {
panic!("Expected HttpError::Status");
}
mock.assert_async().await;
}
#[tokio::test]
async fn test_fetch_no_redirects_returns_redirect_status() {
let mut server = mockito::Server::new_async().await;
let location = format!("{}/api/target", server.url());
let redirect = server
.mock("GET", "/api/redirect")
.with_status(302)
.with_header("location", &location)
.with_body("Found")
.create_async()
.await;
let target = server
.mock("GET", "/api/target")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"success": true, "data": "followed"}"#)
.expect(0)
.create_async()
.await;
let client = HttpClientBuilder::default()
.no_redirects()
.build()
.expect("no-redirect client should build");
let url = format!("{}/api/redirect", server.url());
let result: Result<TestResponse, _> = client.fetch(&url).await;
assert!(result.is_err(), "expected redirect to be blocked");
redirect.assert_async().await;
target.assert_async().await;
}
#[cfg(feature = "reqwest")]
#[tokio::test]
async fn test_reqwest_proxy_matcher_applies_after_redirect() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind proxy listener");
let proxy_addr = listener.local_addr().expect("proxy listener addr");
let proxy_task = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.expect("accept proxy request");
let mut request = vec![0u8; 4096];
let bytes_read = socket.read(&mut request).await.expect("read proxy request");
let request = String::from_utf8_lossy(&request[..bytes_read]).to_string();
let response_body = r#"{"success": true, "data": "proxied"}"#;
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
response_body.len(),
response_body
);
socket
.write_all(response.as_bytes())
.await
.expect("write proxy response");
request
});
let mut server = mockito::Server::new_async().await;
let redirect = server
.mock("GET", "/api/redirect")
.with_status(302)
.with_header("location", "http://matched.example/proxied")
.create_async()
.await;
let proxy_url = url::Url::parse(&format!("http://{}", proxy_addr)).expect("valid proxy URL");
let client = HttpClientBuilder::default()
.proxy_with_matcher(proxy_url, r"^matched\.example$")
.expect("valid proxy matcher")
.build()
.expect("reqwest client with proxy should build");
let url = format!("{}/api/redirect", server.url());
let response: TestResponse = client.fetch(&url).await.expect("redirect should use proxy");
assert_eq!(response.data, "proxied");
redirect.assert_async().await;
let proxied_request = proxy_task.await.expect("proxy task should finish");
assert!(
proxied_request.starts_with("GET http://matched.example/proxied "),
"redirected request should be sent through HTTP proxy, got: {proxied_request}"
);
}
#[tokio::test]
async fn test_post_json_success() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/api/submit")
.match_header(
"content-type",
mockito::Matcher::Regex("application/json.*".to_string()),
)
.match_body(mockito::Matcher::Json(serde_json::json!({
"name": "test",
"value": 42
})))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"success": true, "data": "received"}"#)
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/submit", server.url());
let payload = TestPayload {
name: "test".to_string(),
value: 42,
};
let result: Result<TestResponse, _> = client.post_json(&url, &payload).await;
assert!(result.is_ok());
let response = result.expect("POST JSON should succeed");
assert!(response.success);
assert_eq!(response.data, "received");
mock.assert_async().await;
}
#[tokio::test]
async fn test_post_json_error_status() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/api/submit")
.with_status(400)
.with_body("Bad Request")
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/submit", server.url());
let payload = TestPayload {
name: "test".to_string(),
value: 42,
};
let result: Result<TestResponse, _> = client.post_json(&url, &payload).await;
assert!(result.is_err());
if let Err(HttpError::Status { status, message }) = result {
assert_eq!(status, 400);
assert_eq!(message, "Bad Request");
} else {
panic!("Expected HttpError::Status");
}
mock.assert_async().await;
}
#[tokio::test]
async fn test_get_raw_success() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/raw")
.with_status(200)
.with_body("raw content")
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/raw", server.url());
let result = client.get_raw(&url).await;
assert!(result.is_ok());
let response = result.expect("GET raw should succeed");
assert_eq!(response.status(), 200);
assert!(response.is_success());
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_is_success_with_200() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(200)
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
assert!(response.is_success());
assert!(!response.is_client_error());
assert!(!response.is_server_error());
assert_eq!(response.status(), 200);
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_is_success_with_201() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(201)
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
assert!(response.is_success());
assert_eq!(response.status(), 201);
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_is_success_with_299() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(299)
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
assert!(response.is_success());
assert_eq!(response.status(), 299);
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_is_not_success_with_100() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(199)
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
assert!(!response.is_success());
assert!(!(200..300).contains(&response.status()));
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_is_not_success_with_300() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(300)
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
assert!(!response.is_success());
assert_eq!(response.status(), 300);
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_is_client_error_with_400() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(400)
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
assert!(response.is_client_error());
assert!(!response.is_success());
assert!(!response.is_server_error());
assert_eq!(response.status(), 400);
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_is_client_error_with_499() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(499)
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
assert!(response.is_client_error());
assert_eq!(response.status(), 499);
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_is_not_client_error_with_399() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(399)
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
assert!(!response.is_client_error());
assert_eq!(response.status(), 399);
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_is_server_error_with_500() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(500)
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
assert!(response.is_server_error());
assert!(!response.is_success());
assert!(!response.is_client_error());
assert_eq!(response.status(), 500);
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_is_server_error_with_599() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(599)
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
assert!(response.is_server_error());
assert_eq!(response.status(), 599);
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_is_not_server_error_with_499() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(499)
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
assert!(!response.is_server_error());
assert_eq!(response.status(), 499);
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_text() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(200)
.with_body("Hello, World!")
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
let text = response
.text()
.await
.expect("Text extraction should succeed");
assert_eq!(text, "Hello, World!");
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_json() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"success": true, "data": "json_test"}"#)
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
let json: TestResponse = response.json().await.expect("JSON parsing should succeed");
assert!(json.success);
assert_eq!(json.data, "json_test");
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_bytes() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(200)
.with_body(vec![0x01, 0x02, 0x03, 0x04])
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
let bytes = response
.bytes()
.await
.expect("Bytes extraction should succeed");
assert_eq!(bytes, vec![0x01, 0x02, 0x03, 0x04]);
mock.assert_async().await;
}
#[tokio::test]
async fn test_request_builder_send() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/builder")
.with_status(200)
.with_body("builder response")
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/builder", server.url());
let response = client
.get(&url)
.send()
.await
.expect("Request should succeed");
assert_eq!(response.status(), 200);
assert_eq!(
response
.text()
.await
.expect("Text extraction should succeed"),
"builder response"
);
mock.assert_async().await;
}
#[tokio::test]
async fn test_request_builder_send_json() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/api/json")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"success": true, "data": "builder_json"}"#)
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/json", server.url());
let payload = TestPayload {
name: "builder".to_string(),
value: 100,
};
let result: TestResponse = client
.post(&url)
.json(&payload)
.send_json()
.await
.expect("Request should succeed");
assert!(result.success);
assert_eq!(result.data, "builder_json");
mock.assert_async().await;
}
#[tokio::test]
async fn test_request_builder_with_headers() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/headers")
.match_header("X-Custom-Header", "custom-value")
.match_header("Authorization", "Bearer token123")
.with_status(200)
.with_body("headers received")
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/headers", server.url());
let response = client
.get(&url)
.header("X-Custom-Header", "custom-value")
.header("Authorization", "Bearer token123")
.send()
.await
.expect("Request should succeed");
assert_eq!(response.status(), 200);
assert_eq!(
response
.text()
.await
.expect("Text extraction should succeed"),
"headers received"
);
mock.assert_async().await;
}
#[tokio::test]
async fn test_request_builder_post_with_form() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/api/form")
.match_header(
"content-type",
mockito::Matcher::Regex("application/x-www-form-urlencoded.*".to_string()),
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"success": true, "data": "form_received"}"#)
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/form", server.url());
let form_data = [("field1", "value1"), ("field2", "value2")];
let response: TestResponse = client
.post(&url)
.form(&form_data)
.send_json()
.await
.expect("Request should succeed");
assert!(response.success);
assert_eq!(response.data, "form_received");
mock.assert_async().await;
}
#[tokio::test]
async fn test_request_builder_patch() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("PATCH", "/api/resource")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"success": true, "data": "patched"}"#)
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/resource", server.url());
let payload = TestPayload {
name: "update".to_string(),
value: 99,
};
let result: TestResponse = client
.patch(&url)
.json(&payload)
.send_json()
.await
.expect("Request should succeed");
assert!(result.success);
assert_eq!(result.data, "patched");
mock.assert_async().await;
}
#[tokio::test]
async fn test_fetch_convenience_function() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/convenience")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"success": true, "data": "convenience"}"#)
.create_async()
.await;
let url = format!("{}/api/convenience", server.url());
let result: Result<TestResponse, _> = cdk_http_client::fetch(&url).await;
assert!(result.is_ok());
let response = result.expect("Fetch should succeed");
assert!(response.success);
assert_eq!(response.data, "convenience");
mock.assert_async().await;
}
#[tokio::test]
async fn test_json_deserialization_error() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/api/invalid-json")
.with_status(200)
.with_header("content-type", "application/json")
.with_body("not valid json")
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/invalid-json", server.url());
let result: Result<TestResponse, _> = client.fetch(&url).await;
assert!(result.is_err());
let err = result.expect_err("Should be a deserialization error");
assert!(
matches!(err, HttpError::Serialization(_)),
"Expected HttpError::Serialization, got: {}",
err
);
mock.assert_async().await;
}
#[tokio::test]
async fn test_request_builder_send_json_error_status() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/api/fail")
.with_status(400)
.with_body("Bad Request")
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/fail", server.url());
let payload = TestPayload {
name: "bad".to_string(),
value: 1,
};
let result: Result<TestResponse, _> = client.post(&url).json(&payload).send_json().await;
assert!(result.is_err());
match result.expect_err("Should be a status error") {
HttpError::Status { status, message } => {
assert_eq!(status, 400);
assert_eq!(message, "Bad Request");
}
err => panic!("Expected HttpError::Status, got: {}", err),
}
mock.assert_async().await;
}
#[tokio::test]
async fn test_request_builder_send_json_deserialization_error() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/api/invalid-json-builder")
.with_status(200)
.with_header("content-type", "application/json")
.with_body("not valid json")
.create_async()
.await;
let client = HttpClient::new();
let url = format!("{}/api/invalid-json-builder", server.url());
let payload = TestPayload {
name: "test".to_string(),
value: 42,
};
let result: Result<TestResponse, _> = client.post(&url).json(&payload).send_json().await;
assert!(result.is_err());
let err = result.expect_err("Should be a deserialization error");
assert!(
matches!(err, HttpError::Serialization(_)),
"Expected HttpError::Serialization, got: {}",
err
);
mock.assert_async().await;
}
#[tokio::test]
async fn test_raw_response_json_deserialization_error() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("GET", "/")
.with_status(200)
.with_body("invalid json")
.create_async()
.await;
let client = HttpClient::new();
let response = client
.get_raw(&server.url())
.await
.expect("Request should succeed");
let result: Result<TestResponse, _> = response.json().await;
assert!(result.is_err());
mock.assert_async().await;
}