use std::io::Write;
use hotdata::models::QueryRequest;
use hotdata::{Client, Configuration, UploadOptions};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, Request, ResponseTemplate};
fn config_for(base_url: &str) -> Configuration {
Configuration {
base_path: base_url.to_owned(),
user_agent: Some("hotdata-rust-test".to_owned()),
bearer_access_token: Some("test-token".to_owned()),
..Configuration::default()
}
}
fn client_for(base_url: &str) -> Client {
Client::builder()
.api_token("test-token")
.workspace_id("ws_test")
.base_url(base_url)
.build()
.expect("building the test client should succeed")
}
fn accept_encoding_for(requests: &[Request], p: &str) -> Option<String> {
requests
.iter()
.find(|r| r.url.path() == p)
.expect("a request to that path should have been recorded")
.headers
.get("accept-encoding")
.map(|v| v.to_str().expect("header should be ASCII").to_owned())
}
fn assert_negotiates_gzip(encoding: Option<String>, site: &str) {
let encoding = encoding
.unwrap_or_else(|| panic!("{site} must send an Accept-Encoding header; none was present"));
assert!(
encoding.contains("gzip"),
"{site} must offer gzip; got Accept-Encoding: {encoding}"
);
}
fn sql(text: &str) -> QueryRequest {
QueryRequest::new(text.to_owned())
}
#[tokio::test]
async fn generated_op_negotiates_gzip() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/workspaces"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"workspaces": [],
})))
.mount(&server)
.await;
let config = config_for(&server.uri());
let _ = hotdata::apis::workspaces_api::list_workspaces(&config, None).await;
let requests = server.received_requests().await.expect("requests recorded");
assert_negotiates_gzip(
accept_encoding_for(&requests, "/v1/workspaces"),
"a generated op via Configuration::default",
);
}
#[tokio::test]
async fn send_query_negotiates_gzip() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/query"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"columns": [],
"rows": [],
})))
.mount(&server)
.await;
let client = client_for(&server.uri());
let _ = client.query(sql("SELECT 1")).await;
let requests = server.received_requests().await.expect("requests recorded");
assert_negotiates_gzip(
accept_encoding_for(&requests, "/v1/query"),
"query::send_query",
);
}
#[tokio::test]
async fn submit_query_negotiates_gzip() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/query"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"columns": [],
"rows": [],
})))
.mount(&server)
.await;
let client = client_for(&server.uri());
let _ = client.submit_query(sql("SELECT 1"), None).await;
let requests = server.received_requests().await.expect("requests recorded");
assert_negotiates_gzip(
accept_encoding_for(&requests, "/v1/query"),
"Client::submit_query",
);
}
#[cfg(feature = "arrow")]
#[tokio::test]
async fn arrow_fetch_negotiates_gzip() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/results/res_1"))
.respond_with(ResponseTemplate::new(200).set_body_string("not-arrow-ipc"))
.mount(&server)
.await;
let client = client_for(&server.uri());
let _ = client.get_result_arrow("res_1", "dbid_1", None, None).await;
let requests = server.received_requests().await.expect("requests recorded");
assert_negotiates_gzip(
accept_encoding_for(&requests, "/v1/results/res_1"),
"the Arrow fetch",
);
}
#[cfg(feature = "arrow")]
#[tokio::test]
async fn arrow_fetch_keeps_its_own_accept_header() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/results/res_1"))
.respond_with(ResponseTemplate::new(200).set_body_string("not-arrow-ipc"))
.mount(&server)
.await;
let client = client_for(&server.uri());
let _ = client.get_result_arrow("res_1", "dbid_1", None, None).await;
let requests = server.received_requests().await.expect("requests recorded");
let accept = requests
.iter()
.find(|r| r.url.path() == "/v1/results/res_1")
.expect("the Arrow request should have been recorded")
.headers
.get("accept")
.map(|v| v.to_str().expect("header should be ASCII").to_owned())
.expect("the Arrow fetch must send an Accept header");
assert_eq!(
accept, "application/vnd.apache.arrow.stream",
"the Arrow Accept header must be exactly the Arrow media type, not a \
list that a default header joined"
);
}
#[tokio::test]
async fn gzip_encoded_response_is_transparently_decoded() {
let server = MockServer::start().await;
let body = serde_json::json!({ "ok": true, "workspaces": [] }).to_string();
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
encoder
.write_all(body.as_bytes())
.expect("gzip encoding should succeed");
let compressed = encoder.finish().expect("gzip encoding should finish");
assert_ne!(
compressed.as_slice(),
body.as_bytes(),
"the fixture must actually be compressed, or this test proves nothing"
);
Mock::given(method("GET"))
.and(path("/v1/workspaces"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(compressed)
.insert_header("content-type", "application/json")
.insert_header("content-encoding", "gzip"),
)
.mount(&server)
.await;
let config = config_for(&server.uri());
hotdata::apis::workspaces_api::list_workspaces(&config, None)
.await
.expect("a gzip-encoded response must decode and parse");
}
#[tokio::test]
async fn presigned_storage_put_stays_header_bare() {
let server = MockServer::start().await;
let storage_url = format!("{}/storage/single", server.uri());
let contents = b"hello compression";
Mock::given(method("POST"))
.and(path("/v1/uploads"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"finalize_token": "ftok_single",
"headers": {},
"mode": "single",
"upload_id": "upl_single",
"url": storage_url,
})))
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path("/storage/single"))
.respond_with(ResponseTemplate::new(200).insert_header("ETag", "\"single-etag\""))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/v1/uploads/upl_single/finalize"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"created_at": "2026-06-25T00:00:00Z",
"size_bytes": contents.len(),
"status": "ready",
"upload_id": "upl_single",
})))
.mount(&server)
.await;
let client = client_for(&server.uri());
let file = std::env::temp_dir().join(format!(
"hotdata-compression-{}",
uuid::Uuid::new_v4().simple()
));
std::fs::write(&file, contents).expect("writing the temp upload file should succeed");
let result = client.upload_file(&file, UploadOptions::default()).await;
let _ = std::fs::remove_file(&file);
result.expect("single upload should succeed");
let requests = server.received_requests().await.expect("requests recorded");
assert_eq!(
accept_encoding_for(&requests, "/storage/single"),
None,
"the presigned storage PUT must stay header-bare; an SDK-added \
Accept-Encoding can fall outside the signed header set"
);
assert_negotiates_gzip(
accept_encoding_for(&requests, "/v1/uploads"),
"the upload create-session leg",
);
}