headroom/crates/headroom-proxy/tests/integration_http.rs
chopratejas bcc2ad810a test(rust): integration tests + wiremock harness (phase-1)
15 integration tests across five suites that spin up the proxy on an
ephemeral port pointed at a per-test mock upstream:

- integration_http: all 7 methods round-trip with body, status passthrough
  for 404/500/502, query strings preserved, 1MB POST streams through.
- integration_sse: a 10-event in-process hyper SSE upstream emits at 50ms
  cadence; chunks reach the client with max gap < 500ms (loose CI bound)
  and a client disconnect propagates to the upstream within 2s.
- integration_ws: 5 text + 5 binary messages echo through a tungstenite
  upstream byte-equal; client-initiated close propagates.
- integration_headers: hop-by-hop strip both directions, X-Forwarded-*
  injection, X-Forwarded-For appends to existing value, multi-valued
  response headers preserved.
- integration_body: 5MB POST round-trips byte-equal; streaming response
  yields first byte before the upstream finishes sending.
- integration_health: own /healthz always 200; /healthz/upstream is 200
  when upstream healthy and 503 when down.

The Sec-WebSocket-Protocol forwarding is exercised implicitly by the WS
tests via tungstenite handshake. The harness lives at tests/common/mod.rs
and is shared by every integration suite.
2026-04-24 15:52:31 -07:00

135 lines
4.3 KiB
Rust

//! HTTP method round-trip + status passthrough.
mod common;
use common::start_proxy;
use wiremock::matchers::{any, method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn all_methods_round_trip_with_body() {
let upstream = MockServer::start().await;
for m in ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] {
Mock::given(method(m))
.and(path("/echo"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("x-from-upstream", "yes")
.set_body_string(format!("ok-{m}")),
)
.mount(&upstream)
.await;
}
Mock::given(method("HEAD"))
.and(path("/echo"))
.respond_with(ResponseTemplate::new(200).insert_header("x-from-upstream", "yes"))
.mount(&upstream)
.await;
let proxy = start_proxy(&upstream.uri()).await;
let client = reqwest::Client::new();
for m in ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] {
let url = format!("{}/echo", proxy.url());
let resp = client
.request(reqwest::Method::from_bytes(m.as_bytes()).unwrap(), &url)
.body(format!("payload-{m}"))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200, "method {m}");
assert_eq!(resp.headers().get("x-from-upstream").unwrap(), "yes");
let body = resp.text().await.unwrap();
assert_eq!(body, format!("ok-{m}"));
}
// HEAD has no body but should round-trip headers + status.
let head_resp = client
.head(format!("{}/echo", proxy.url()))
.send()
.await
.unwrap();
assert_eq!(head_resp.status(), 200);
assert_eq!(head_resp.headers().get("x-from-upstream").unwrap(), "yes");
proxy.shutdown().await;
}
#[tokio::test]
async fn upstream_error_codes_passthrough() {
let upstream = MockServer::start().await;
for status in [404u16, 500, 502] {
Mock::given(method("GET"))
.and(path(format!("/code/{status}")))
.respond_with(ResponseTemplate::new(status).set_body_string(format!("err-{status}")))
.mount(&upstream)
.await;
}
let proxy = start_proxy(&upstream.uri()).await;
let client = reqwest::Client::new();
for status in [404u16, 500, 502] {
let resp = client
.get(format!("{}/code/{status}", proxy.url()))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), status);
assert_eq!(resp.text().await.unwrap(), format!("err-{status}"));
}
proxy.shutdown().await;
}
#[tokio::test]
async fn query_string_preserved() {
let upstream = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/q"))
.and(query_param("a", "1"))
.and(query_param("b", "two"))
.respond_with(ResponseTemplate::new(200).set_body_string("matched"))
.mount(&upstream)
.await;
Mock::given(any())
.respond_with(ResponseTemplate::new(418))
.mount(&upstream)
.await;
let proxy = start_proxy(&upstream.uri()).await;
let resp = reqwest::get(format!("{}/q?a=1&b=two", proxy.url()))
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.text().await.unwrap(), "matched");
proxy.shutdown().await;
}
#[tokio::test]
async fn one_mb_post_streams_through() {
let upstream = MockServer::start().await;
let payload = vec![0xABu8; 1024 * 1024];
let payload_clone = payload.clone();
Mock::given(method("POST"))
.and(path("/upload"))
.respond_with(move |req: &wiremock::Request| {
assert_eq!(
req.body.len(),
payload_clone.len(),
"upstream got full body"
);
assert_eq!(&req.body[..], &payload_clone[..]);
ResponseTemplate::new(200).set_body_string("uploaded")
})
.mount(&upstream)
.await;
let proxy = start_proxy(&upstream.uri()).await;
let resp = reqwest::Client::new()
.post(format!("{}/upload", proxy.url()))
.body(payload)
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.text().await.unwrap(), "uploaded");
proxy.shutdown().await;
}