Files
khadhroony-solana-project/crates/ksp-offchain-transport-lib/unit_tests/http_client.rs

237 lines
12 KiB
Rust

// file: crates/ksp-offchain-transport-lib/unit_tests/http_client.rs
// version: 2
#[tokio::test]
async fn rest_client_accepts_bounded_json_and_never_exposes_request_debug() -> ksp_core_lib::Result<()> {
let server_result =
spawn_single_response("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 13\r\nConnection: close\r\n\r\n{\"price\":123}").await;
let server = match server_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut request = match crate::HttpGetRequest::new_test_http(server.url.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
request.append_query_pair("symbol", "SOL/USD");
let secret = "sensitive-canary-value";
if let std::result::Result::Err(error) = request.insert_sensitive_header("x-api-key", secret) {
return std::result::Result::Err(error);
}
let debug = format!("{request:?}");
assert!(!debug.contains(secret));
assert!(!debug.contains(server.url.as_str()));
let client = match crate::HttpRestClient::new(crate::HttpClientSettings::default()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let document = match client.get_json("test-provider", "sol_usd", request).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(document.as_bytes(), br#"{"price":123}"#);
if let std::result::Result::Err(error) = finish_server(server.join).await {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
#[test]
fn https_request_builder_accepts_official_style_url_without_exposing_it() -> ksp_core_lib::Result<()> {
let mut request = match crate::HttpGetRequest::new_https("https://example.com/price") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
request.append_query_pair("symbol", "SOL/USD");
if let std::result::Result::Err(error) = request.insert_sensitive_header("x-api-key", "redaction-canary") {
return std::result::Result::Err(error);
}
let debug = format!("{request:?}");
assert!(!debug.contains("example.com"));
assert!(!debug.contains("redaction-canary"));
return std::result::Result::Ok(());
}
#[tokio::test]
async fn rest_client_rejects_redirects_instead_of_following_them() -> ksp_core_lib::Result<()> {
let server_result =
spawn_single_response("HTTP/1.1 302 Found\r\nLocation: https://example.invalid/secret\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").await;
let server = match server_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let request = match crate::HttpGetRequest::new_test_http(server.url.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let client = match crate::HttpRestClient::new(crate::HttpClientSettings::default()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let error = match client.get_json("test-provider", "redirect", request).await {
std::result::Result::Ok(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_REQUEST_FAILED, "test expected redirect rejection"));
},
std::result::Result::Err(error) => error,
};
assert_eq!(error.code(), crate::ERROR_CODE_HTTP_REQUEST_FAILED);
if let std::result::Result::Err(error) = finish_server(server.join).await {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
#[tokio::test]
async fn rest_client_bounds_chunked_body_before_json_deserialization() -> ksp_core_lib::Result<()> {
let server_result = spawn_single_response(concat!(
"HTTP/1.1 200 OK\r\n",
"Content-Type: application/json\r\n",
"Transfer-Encoding: chunked\r\n",
"Connection: close\r\n\r\n",
"8\r\n{\"aaaa\":\r\n",
"8\r\n\"bbbbbb\"\r\n",
"1\r\n}\r\n0\r\n\r\n",
))
.await;
let server = match server_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let settings = match crate::HttpClientSettings::new(std::time::Duration::from_secs(1), std::time::Duration::from_secs(2), 8) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let request = match crate::HttpGetRequest::new_test_http(server.url.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let client = match crate::HttpRestClient::new(settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let error = match client.get_json("test-provider", "bounded", request).await {
std::result::Result::Ok(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_RESPONSE_TOO_LARGE, "test expected body bound"));
},
std::result::Result::Err(error) => error,
};
assert_eq!(error.code(), crate::ERROR_CODE_HTTP_RESPONSE_TOO_LARGE);
if let std::result::Result::Err(error) = finish_server(server.join).await {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
#[tokio::test]
async fn rest_client_classifies_429_retry_after_and_never_copies_remote_body() -> ksp_core_lib::Result<()> {
let remote_canary = "REMOTE_SECRET_CANARY";
let response =
format!("HTTP/1.1 429 Too Many Requests\r\nRetry-After: 7\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", remote_canary.len(), remote_canary);
let server_result = spawn_single_response(response.as_str()).await;
let server = match server_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let request = match crate::HttpGetRequest::new_test_http(server.url.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let client = match crate::HttpRestClient::new(crate::HttpClientSettings::default()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let error = match client.get_json("test-provider", "rate_limited", request).await {
std::result::Result::Ok(_) => return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_RATE_LIMITED, "test expected 429")),
std::result::Result::Err(error) => error,
};
assert_eq!(error.code(), crate::ERROR_CODE_HTTP_RATE_LIMITED);
assert!(!format!("{error:?}").contains(remote_canary));
assert!(!error.to_string().contains(remote_canary));
if let std::result::Result::Err(error) = finish_server(server.join).await {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
#[tokio::test]
async fn rest_client_rejects_invalid_json_after_success_status() -> ksp_core_lib::Result<()> {
let server_result = spawn_single_response("HTTP/1.1 200 OK\r\nContent-Length: 8\r\nConnection: close\r\n\r\nnot-json").await;
let server = match server_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let request = match crate::HttpGetRequest::new_test_http(server.url.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let client = match crate::HttpRestClient::new(crate::HttpClientSettings::default()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let error = match client.get_json("test-provider", "invalid_json", request).await {
std::result::Result::Ok(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_INVALID_JSON, "test expected invalid JSON"));
},
std::result::Result::Err(error) => error,
};
assert_eq!(error.code(), crate::ERROR_CODE_HTTP_INVALID_JSON);
if let std::result::Result::Err(error) = finish_server(server.join).await {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
struct TestServer {
join: tokio::task::JoinHandle<std::io::Result<()>>,
url: std::string::String,
}
async fn spawn_single_response(response: &str) -> ksp_core_lib::Result<TestServer> {
let listener_result = tokio::net::TcpListener::bind("127.0.0.1:0").await;
let listener = match listener_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(io_error(error)),
};
let address = match listener.local_addr() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(io_error(error)),
};
let response = response.as_bytes().to_vec();
let join = tokio::spawn(async move {
let accept_result = listener.accept().await;
let (mut socket, _) = match accept_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut request = [0_u8; 4096];
if let std::result::Result::Err(error) = tokio::io::AsyncReadExt::read(&mut socket, &mut request).await {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = tokio::io::AsyncWriteExt::write_all(&mut socket, response.as_slice()).await {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = tokio::io::AsyncWriteExt::shutdown(&mut socket).await {
return std::result::Result::Err(error);
}
return std::io::Result::Ok(());
});
return std::result::Result::Ok(TestServer { join, url: format!("http://{address}/price") });
}
async fn finish_server(join: tokio::task::JoinHandle<std::io::Result<()>>) -> ksp_core_lib::Result<()> {
let joined = join.await;
return match joined {
std::result::Result::Ok(std::result::Result::Ok(())) => std::result::Result::Ok(()),
std::result::Result::Ok(std::result::Result::Err(error)) => std::result::Result::Err(io_error(error)),
std::result::Result::Err(error) => std::result::Result::Err(join_error(error)),
};
}
fn io_error(error: std::io::Error) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_REQUEST_FAILED, "test HTTP server failed").with_source(error);
}
fn join_error(error: tokio::task::JoinError) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_REQUEST_FAILED, "test HTTP server task failed").with_source(error);
}