#[path = "support/api.rs"]
mod api_support;
use api_support::*;
use http_body_util::BodyExt;
async fn send(router: &axum::Router, method: &str, uri: &str) -> (StatusCode, bytes::Bytes) {
let request = Request::builder()
.method(method)
.uri(uri)
.body(Body::empty())
.unwrap();
let request = arcbox_docker::api::strip_api_version_prefix(request);
let response = router.clone().oneshot(request).await.unwrap();
let status = response.status();
let body = response.into_body().collect().await.unwrap().to_bytes();
(status, body)
}
#[tokio::test]
async fn query_string_reaches_the_guest_verbatim() {
let (router, _runtime, guest, _tmp) = router_with_mock_guest(vec![MockRoute {
method: "DELETE",
path: "/containers/web",
status: 204,
body: "",
}])
.await;
let (status, _) = send(&router, "DELETE", "/containers/web?force=true&v=true").await;
assert_eq!(status, StatusCode::NO_CONTENT);
let seen = guest.last_request_uri().await.expect("guest saw a request");
assert_eq!(
seen, "/containers/web?force=true&v=true",
"the request target must reach dockerd byte-for-byte"
);
}
#[tokio::test]
async fn json_filters_query_is_not_re_encoded() {
let (router, _runtime, guest, _tmp) = router_with_mock_guest(vec![MockRoute {
method: "GET",
path: "/containers/json",
status: 200,
body: "[]",
}])
.await;
let filters = "%7B%22label%22%3A%7B%22a%3Db%22%3Atrue%7D%7D";
let (status, _) = send(
&router,
"GET",
&format!("/containers/json?filters={filters}"),
)
.await;
assert_eq!(status, StatusCode::OK);
let seen = guest.last_request_uri().await.expect("guest saw a request");
assert_eq!(
seen,
format!("/containers/json?filters={filters}"),
"the percent-encoded filters value must not be rewritten, extended, or joined by \
extra query material"
);
}
#[tokio::test]
async fn versioned_request_is_forwarded_with_its_prefix() {
let (router, _runtime, guest, _tmp) = router_with_mock_guest(vec![MockRoute {
method: "GET",
path: "/containers/json",
status: 200,
body: "[]",
}])
.await;
let (status, _) = send(&router, "GET", "/v1.43/containers/json?all=true&limit=5").await;
assert_eq!(status, StatusCode::OK);
let seen = guest.last_request_uri().await.expect("guest saw a request");
assert_eq!(
seen, "/v1.43/containers/json?all=true&limit=5",
"the client's original target reaches dockerd unchanged, prefix included"
);
}
#[tokio::test]
async fn absent_query_stays_absent() {
let (router, _runtime, guest, _tmp) = router_with_mock_guest(vec![MockRoute {
method: "GET",
path: "/containers/json",
status: 200,
body: "[]",
}])
.await;
let (status, _) = send(&router, "GET", "/containers/json").await;
assert_eq!(status, StatusCode::OK);
let seen = guest.last_request_uri().await.expect("guest saw a request");
assert_eq!(
seen, "/containers/json",
"a query-less request must not gain an empty query"
);
}
#[tokio::test]
async fn proxy_failure_returns_a_docker_shaped_error() {
let (router, _runtime, guest, _tmp) = router_with_mock_guest(vec![]).await;
guest.cancel.cancel();
let _ = std::fs::remove_file(&guest.socket_path);
let (status, body) = send(&router, "GET", "/containers/json").await;
assert!(
status.is_client_error() || status.is_server_error(),
"an unreachable backend must not report success (got {status})"
);
let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap_or_else(|e| {
panic!(
"error body is not JSON ({e}): {:?}",
String::from_utf8_lossy(&body)
)
});
let message = parsed.get("message").and_then(serde_json::Value::as_str);
assert!(
message.is_some_and(|m| !m.is_empty()),
"error body must carry a non-empty `message` field, got {parsed}"
);
}