use super::*;
use actix_web::{http::StatusCode, test};
use std::io::Read;
fn cfg() -> ServerConfig {
ServerConfig::default()
}
#[actix_web::test]
async fn zstd_endpoint_returns_zstd_encoded_json() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/zstd").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-encoding")
.unwrap()
.to_str()
.unwrap(),
"zstd"
);
let body = test::read_body(resp).await;
let decoded = zstd::decode_all(&body[..]).expect("body should be valid zstd");
let v: serde_json::Value =
serde_json::from_slice(&decoded).expect("decoded body should be JSON");
assert_eq!(v.get("zstd").and_then(|x| x.as_bool()), Some(true));
assert_eq!(v.get("method").and_then(|x| x.as_str()), Some("GET"));
assert!(v.get("url").is_some());
}
#[actix_web::test]
async fn query_method_anything_echoes_method_and_body() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::default()
.method(query_method())
.uri("/anything")
.insert_header(("content-type", "application/json"))
.set_payload(r#"{"hello":"world"}"#)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v.get("method").and_then(|x| x.as_str()), Some("QUERY"));
assert_eq!(v["json"]["hello"].as_str(), Some("world"));
assert!(v.get("url").is_some());
}
#[actix_web::test]
async fn query_method_anything_with_param_records_path() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::default()
.method(query_method())
.uri("/anything/foo/bar")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v.get("method").and_then(|x| x.as_str()), Some("QUERY"));
assert_eq!(v["args"]["anything"].as_str(), Some("foo/bar"));
}
#[actix_web::test]
async fn query_method_echo_mirrors_body() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::default()
.method(query_method())
.uri("/echo")
.set_payload("ping")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(test::read_body(resp).await, "ping");
}
#[actix_web::test]
async fn duplicate_request_headers_are_joined() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/headers")
.append_header(("x-multi", "Foo"))
.append_header(("x-multi", "Bar"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(
v["headers"]["x-multi"].as_str(),
Some("Foo, Bar"),
"duplicate header values must be joined per RFC 7230"
);
}
#[actix_web::test]
async fn status_injects_repeatable_response_headers() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/status/429?header=Retry-After:60&header=X-RateLimit-Remaining:0")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
assert_eq!(
resp.headers().get("retry-after").unwrap().to_str().unwrap(),
"60"
);
assert_eq!(
resp.headers()
.get("x-ratelimit-remaining")
.unwrap()
.to_str()
.unwrap(),
"0"
);
}
#[actix_web::test]
async fn multipart_parses_json_fields_and_captures_file_content_type() {
let boundary = "----httpcan-test";
let body = format!(
"--{b}\r\n\
Content-Disposition: form-data; name=\"obj\"\r\n\
Content-Type: application/json\r\n\r\n\
{{\"k\":\"v\"}}\r\n\
--{b}\r\n\
Content-Disposition: form-data; name=\"f\"; filename=\"a.txt\"\r\n\
Content-Type: text/plain\r\n\r\n\
hello\r\n\
--{b}--\r\n",
b = boundary
);
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/post")
.insert_header((
"content-type",
format!("multipart/form-data; boundary={boundary}"),
))
.set_payload(body)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["form"]["obj"]["k"].as_str(), Some("v"));
assert_eq!(v["files"]["f"]["content_type"].as_str(), Some("text/plain"));
assert_eq!(v["files"]["f"]["filename"].as_str(), Some("a.txt"));
assert_eq!(v["files"]["f"]["content"].as_str(), Some("hello"));
}
#[actix_web::test]
async fn healthz_returns_ok() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/healthz").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["status"].as_str(), Some("ok"));
}
#[actix_web::test]
async fn tags_endpoint_returns_object_and_404_for_missing() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/tags").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON object");
assert!(v.is_object());
let req = test::TestRequest::get()
.uri("/tags/__definitely_not_set__")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[actix_web::test]
async fn response_carries_version_and_server_timing_headers() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/get").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.headers().contains_key("x-httpcan-version"));
assert!(
resp.headers()
.get("server-timing")
.unwrap()
.to_str()
.unwrap()
.starts_with("app;dur="),
"Server-Timing must start with app;dur="
);
}
#[actix_web::test]
async fn method_endpoint_echoes_arbitrary_method() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/method").to_request();
let resp = test::call_service(&app, req).await;
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["method"].as_str(), Some("GET"));
let custom = actix_web::http::Method::from_bytes(b"BREW").unwrap();
let req = test::TestRequest::default()
.method(custom)
.uri("/method")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["method"].as_str(), Some("BREW"));
}
#[actix_web::test]
async fn head_endpoint_echoes_request_headers() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::default()
.method(actix_web::http::Method::HEAD)
.uri("/head")
.insert_header(("X-Test", "hello"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert!(
test::read_body(resp).await.is_empty(),
"HEAD response must have an empty body"
);
}
#[actix_web::test]
async fn base64_post_decodes_body() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/base64")
.set_payload("aGVsbG8=") .to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(test::read_body(resp).await, "hello");
}
#[actix_web::test]
async fn compression_post_returns_encoded_body() {
let app = test::init_service(create_app(cfg())).await;
let payload = "the quick brown fox";
let req = test::TestRequest::post()
.uri("/zstd")
.set_payload(payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-encoding")
.unwrap()
.to_str()
.unwrap(),
"zstd"
);
let decoded = zstd::decode_all(&test::read_body(resp).await[..]).expect("valid zstd");
assert_eq!(decoded, payload.as_bytes());
}
#[actix_web::test]
async fn iso_8859_1_endpoint_returns_latin1() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/encoding/iso-8859-1")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"text/html; charset=iso-8859-1"
);
assert!(
test::read_body(resp).await.contains(&0xe9),
"body must contain a Latin-1 byte"
);
}
#[actix_web::test]
async fn basic_auth_accepts_post() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/basic-auth/user/passwd")
.insert_header(("Authorization", "Basic dXNlcjpwYXNzd2Q=")) .to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_web::test]
async fn etag_weak_match_and_multisegment_route() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/etag/abc")
.insert_header(("If-None-Match", "W/\"abc\""))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
let req = test::TestRequest::get()
.uri("/etag/W/%22abc%22")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_web::test]
async fn cookies_set_supports_attributes() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/cookies/set?token=secret&httponly=true&samesite=Lax")
.to_request();
let resp = test::call_service(&app, req).await;
let set_cookie = resp
.headers()
.get("set-cookie")
.unwrap()
.to_str()
.unwrap()
.to_lowercase();
assert!(set_cookie.contains("token=secret"));
assert!(set_cookie.contains("httponly"));
assert!(set_cookie.contains("samesite=lax"));
}
#[actix_web::test]
async fn status_ignores_trailing_path() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/status/200/extra/path")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_web::test]
async fn response_headers_custom_body() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/response-headers?body=hello&X-Test=bar")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get("x-test").unwrap().to_str().unwrap(),
"bar"
);
assert_eq!(test::read_body(resp).await, "hello");
}
#[actix_web::test]
async fn base64_returns_binary_as_octet_stream() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/base64/8A%3D%3D")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/octet-stream"
);
assert_eq!(test::read_body(resp).await, vec![0xf0]);
}
#[actix_web::test]
async fn drip_chunked_streams_unknown_length_body() {
use actix_web::body::{BodySize, MessageBody};
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/drip?numbytes=5&duration=0&delay=0&chunked=true")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert!(
matches!(resp.response().body().size(), BodySize::Stream),
"chunked response must be an unknown-length stream body"
);
assert_eq!(test::read_body(resp).await, "*****");
let req = test::TestRequest::get()
.uri("/drip?numbytes=5&duration=0&delay=0")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert!(
matches!(resp.response().body().size(), BodySize::Sized(5)),
"non-chunked response must be a sized body of numbytes"
);
assert_eq!(test::read_body(resp).await, "*****");
}
#[actix_web::test]
async fn digest_auth_sha_512_256_succeeds() {
let app = test::init_service(create_app(cfg())).await;
let uri = "/digest-auth/auth/user/passwd/SHA-512-256";
let req = test::TestRequest::get().uri(uri).to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let challenge = resp
.headers()
.get("www-authenticate")
.unwrap()
.to_str()
.unwrap()
.to_string();
let field = |k: &str| -> String {
let pat = format!("{k}=\"");
let start = challenge.find(&pat).unwrap() + pat.len();
let end = challenge[start..].find('"').unwrap();
challenge[start..start + end].to_string()
};
let realm = field("realm");
let nonce = field("nonce");
let opaque = field("opaque");
let hex = |data: &[u8]| -> String {
crate::handlers::auth::digest_hash_hex("SHA-512-256", data).unwrap()
};
let ha1 = hex(format!("user:{realm}:passwd").as_bytes());
let ha2 = hex(format!("GET:{uri}").as_bytes());
let nc = "00000001";
let cnonce = "0a4f113b";
let response = hex(format!("{ha1}:{nonce}:{nc}:{cnonce}:auth:{ha2}").as_bytes());
let auth = format!(
r#"Digest username="user", realm="{realm}", nonce="{nonce}", uri="{uri}", qop=auth, nc={nc}, cnonce="{cnonce}", response="{response}", opaque="{opaque}", algorithm=SHA-512-256"#
);
let req = test::TestRequest::get()
.uri(uri)
.insert_header(("Authorization", auth))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value = serde_json::from_slice(&test::read_body(resp).await).unwrap();
assert_eq!(v.get("authenticated").and_then(|x| x.as_bool()), Some(true));
assert_eq!(
v.get("algorithm").and_then(|x| x.as_str()),
Some("SHA-512-256")
);
}
#[actix_web::test]
async fn digest_auth_omits_qop_rfc2069() {
let app = test::init_service(create_app(cfg())).await;
let uri = "/digest-auth/none/user/passwd";
let req = test::TestRequest::get().uri(uri).to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let challenge = resp
.headers()
.get("www-authenticate")
.unwrap()
.to_str()
.unwrap()
.to_string();
assert!(
!challenge.contains("qop="),
"RFC 2069 challenge must omit qop, got: {challenge}"
);
let field = |k: &str| -> String {
let pat = format!("{k}=\"");
let start = challenge.find(&pat).unwrap() + pat.len();
let end = challenge[start..].find('"').unwrap();
challenge[start..start + end].to_string()
};
let realm = field("realm");
let nonce = field("nonce");
let opaque = field("opaque");
let ha1 = format!("{:x}", md5::compute(format!("user:{realm}:passwd")));
let ha2 = format!("{:x}", md5::compute(format!("GET:{uri}")));
let response = format!("{:x}", md5::compute(format!("{ha1}:{nonce}:{ha2}")));
let auth = format!(
r#"Digest username="user", realm="{realm}", nonce="{nonce}", uri="{uri}", response="{response}", opaque="{opaque}""#
);
let req = test::TestRequest::get()
.uri(uri)
.insert_header(("Authorization", auth))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value = serde_json::from_slice(&test::read_body(resp).await).unwrap();
assert_eq!(v.get("authenticated").and_then(|x| x.as_bool()), Some(true));
}
#[actix_web::test]
async fn cors_exposes_www_authenticate() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/bearer")
.insert_header(("Origin", "https://example.com"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
assert!(resp.headers().get("www-authenticate").is_some());
let expose = resp
.headers()
.get("access-control-expose-headers")
.expect("expose-headers present on cross-origin response")
.to_str()
.unwrap();
assert!(
expose.to_lowercase().contains("www-authenticate"),
"WWW-Authenticate must be listed as exposed: {expose}"
);
}
#[actix_web::test]
async fn bytes_returns_requested_bytes_under_limit() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/bytes/16?seed=1")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/octet-stream"
);
assert_eq!(test::read_body(resp).await.len(), 16);
}
#[actix_web::test]
async fn bytes_rejects_over_limit_with_404() {
let limit = crate::config::DEFAULT_MAX_BYTES;
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri(&format!("/bytes/{}", limit + 1))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let body_bytes = test::read_body(resp).await;
let body = std::str::from_utf8(&body_bytes).unwrap();
assert!(
body.contains(&format!("(0, {}]", limit)),
"error must state the range, got: {body}"
);
}
#[actix_web::test]
async fn stream_bytes_rejects_over_limit_with_404() {
let limit = crate::config::DEFAULT_MAX_BYTES;
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri(&format!("/stream-bytes/{}", limit + 1))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[actix_web::test]
async fn bytes_honors_custom_max_bytes() {
let custom = ServerConfig::default().max_bytes(10);
let app = test::init_service(create_app(custom)).await;
let req = test::TestRequest::get().uri("/bytes/20").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let req = test::TestRequest::get().uri("/bytes/5?seed=2").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(test::read_body(resp).await.len(), 5);
}
#[actix_web::test]
async fn echo_get_args_headers_origin_url() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/get?foo=bar")
.insert_header(("x-echo-core", "sentinel"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["args"]["foo"].as_str(), Some("bar"));
assert_eq!(v["headers"]["x-echo-core"].as_str(), Some("sentinel"));
let origin = v["origin"].as_str().expect("origin is a string");
assert!(!origin.is_empty(), "origin must not be empty");
let url = v["url"].as_str().expect("url is a string");
assert!(url.contains("/get"), "url should reference /get: {url}");
assert!(url.contains("foo=bar"), "url should carry the query: {url}");
assert!(v.get("method").is_none(), "/get must not expose method");
assert!(v.get("data").is_none(), "/get must not expose data");
}
#[actix_web::test]
async fn echo_post_json_body() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/post")
.insert_header(("content-type", "application/json"))
.set_payload(r#"{"k":"v"}"#)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(
v["json"]["k"].as_str(),
Some("v"),
"json.k should be parsed"
);
assert_eq!(v["data"].as_str(), Some(r#"{"k":"v"}"#));
assert!(v.get("url").is_some());
let origin = v["origin"].as_str().expect("origin is a string");
assert!(!origin.is_empty());
assert!(
v.get("method").is_none(),
"/post must not expose method (HttpMethodsRequestInfo)"
);
}
#[actix_web::test]
async fn echo_post_form_urlencoded() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/post")
.insert_header(("content-type", "application/x-www-form-urlencoded"))
.set_payload("a=1&b=2")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["form"]["a"].as_str(), Some("1"));
assert_eq!(v["form"]["b"].as_str(), Some("2"));
assert_eq!(v["data"].as_str(), Some(""));
assert!(v["json"].is_null(), "form body must not populate json");
}
#[actix_web::test]
async fn echo_put_echoes_body() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::put()
.uri("/put")
.set_payload("put-payload")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["data"].as_str(), Some("put-payload"));
assert!(v.get("url").is_some());
}
#[actix_web::test]
async fn echo_patch_echoes_body() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::patch()
.uri("/patch")
.set_payload("patch-payload")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["data"].as_str(), Some("patch-payload"));
assert!(v.get("url").is_some());
}
#[actix_web::test]
async fn echo_delete_echoes_body() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::delete()
.uri("/delete")
.set_payload("delete-payload")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["data"].as_str(), Some("delete-payload"));
assert!(v.get("url").is_some());
}
#[actix_web::test]
async fn echo_anything_get_method() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/anything").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["method"].as_str(), Some("GET"));
assert!(v.get("url").is_some());
let origin = v["origin"].as_str().expect("origin is a string");
assert!(!origin.is_empty());
}
#[actix_web::test]
async fn echo_anything_post_echoes_body() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/anything")
.insert_header(("content-type", "application/json"))
.set_payload(r#"{"k":"v"}"#)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["method"].as_str(), Some("POST"));
assert_eq!(v["json"]["k"].as_str(), Some("v"));
assert_eq!(v["data"].as_str(), Some(r#"{"k":"v"}"#));
}
#[actix_web::test]
async fn echo_anything_path_param_multisegment() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/anything/foo/bar")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["args"]["anything"].as_str(), Some("foo/bar"));
assert_eq!(v["method"].as_str(), Some("GET"));
}
#[actix_web::test]
async fn echo_get_returns_empty_body() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/echo").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
assert!(body.is_empty(), "GET /echo body must be empty");
}
#[actix_web::test]
async fn echo_post_body_verbatim() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/echo")
.set_payload("hello")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
assert_eq!(&body[..], b"hello", "POST /echo must mirror body bytes");
}
#[actix_web::test]
async fn echo_put_patch_delete_body_verbatim() {
let app = test::init_service(create_app(cfg())).await;
let cases: &[(Method, &str)] = &[
(Method::PUT, "put-echo"),
(Method::PATCH, "patch-echo"),
(Method::DELETE, "delete-echo"),
];
for (method, payload) in cases {
let req = test::TestRequest::default()
.method(method.clone())
.uri("/echo")
.set_payload(*payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK, "status for {method:?}");
let body = test::read_body(resp).await;
assert_eq!(
&body[..],
payload.as_bytes(),
"{method:?} /echo must mirror body bytes verbatim"
);
}
}
#[actix_web::test]
async fn fmt_json_returns_slideshow() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/json").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/json"
);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert!(
v.get("slideshow").is_some(),
"body must contain slideshow key"
);
assert_eq!(v["slideshow"]["author"].as_str(), Some("Yours Truly"));
assert!(v["slideshow"]["slides"].is_array(), "slides is an array");
assert_eq!(v["slideshow"]["title"].as_str(), Some("Sample Slide Show"));
}
#[actix_web::test]
async fn fmt_xml_body_and_content_type() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/xml").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/xml"
);
let body = test::read_body(resp).await;
let text = std::str::from_utf8(&body).expect("utf8 body");
assert!(text.contains("<slideshow"), "body contains <slideshow");
assert!(text.contains("Sample Slide Show"));
}
#[actix_web::test]
async fn fmt_html_body_and_content_type() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/html").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"text/html; charset=utf-8"
);
let body = test::read_body(resp).await;
let text = std::str::from_utf8(&body).expect("utf8 body");
assert!(text.contains("<html"), "body contains <html");
assert!(text.contains("Moby-Dick"));
}
#[actix_web::test]
async fn fmt_robots_txt_body_and_content_type() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/robots.txt").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"text/plain"
);
let body = test::read_body(resp).await;
let text = std::str::from_utf8(&body).expect("utf8 body");
assert!(text.contains("Disallow"), "body contains Disallow");
assert!(text.contains("User-agent: *"));
assert!(text.contains("/deny"));
}
#[actix_web::test]
async fn fmt_deny_body_and_content_type() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/deny").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"text/plain"
);
let body = test::read_body(resp).await;
assert_eq!(&body[..], b"YOU SHOULDN'T BE HERE");
}
#[actix_web::test]
async fn fmt_utf8_content_type() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/encoding/utf8").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"text/html; charset=utf-8"
);
let body = test::read_body(resp).await;
let text = std::str::from_utf8(&body).expect("utf8 body");
assert!(text.contains("<html"), "body contains <html");
assert!(text.contains("UTF-8") || text.contains("utf-8") || text.contains("Unicode"));
}
#[actix_web::test]
async fn fmt_gzip_get_decodes() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/gzip").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-encoding")
.unwrap()
.to_str()
.unwrap(),
"gzip"
);
let body = test::read_body(resp).await;
let mut decoded = Vec::new();
std::io::Read::read_to_end(&mut flate2::read::GzDecoder::new(&body[..]), &mut decoded)
.expect("gzip decode");
let v: serde_json::Value = serde_json::from_slice(&decoded).expect("decompressed body is JSON");
assert_eq!(v["method"].as_str(), Some("GET"));
assert_eq!(v["gzipped"].as_bool(), Some(true));
}
#[actix_web::test]
async fn fmt_deflate_get_decodes() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/deflate").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-encoding")
.unwrap()
.to_str()
.unwrap(),
"deflate"
);
let body = test::read_body(resp).await;
let mut decoded = Vec::new();
std::io::Read::read_to_end(
&mut flate2::read::DeflateDecoder::new(&body[..]),
&mut decoded,
)
.expect("raw deflate decode");
let v: serde_json::Value = serde_json::from_slice(&decoded).expect("decompressed body is JSON");
assert_eq!(v["method"].as_str(), Some("GET"));
assert_eq!(v["deflated"].as_bool(), Some(true));
}
#[actix_web::test]
async fn fmt_brotli_get_decodes() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/brotli").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-encoding")
.unwrap()
.to_str()
.unwrap(),
"br"
);
let body = test::read_body(resp).await;
let mut decoded = Vec::new();
std::io::Read::read_to_end(
&mut brotli::Decompressor::new(&body[..], 4096),
&mut decoded,
)
.expect("brotli decode");
let v: serde_json::Value = serde_json::from_slice(&decoded).expect("decompressed body is JSON");
assert_eq!(v["method"].as_str(), Some("GET"));
assert_eq!(v["brotli"].as_bool(), Some(true));
}
#[actix_web::test]
async fn redirect_one_targets_get() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/redirect/1").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
let location = resp
.headers()
.get("location")
.expect("location header present")
.to_str()
.unwrap();
assert_eq!(location, "/get");
}
#[actix_web::test]
async fn redirect_two_decrements_to_relative_redirect_one() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/redirect/2").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
let location = resp
.headers()
.get("location")
.expect("location header present")
.to_str()
.unwrap();
assert_eq!(location, "/relative-redirect/1");
}
#[actix_web::test]
async fn redirect_zero_is_bad_request() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/redirect/0").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert!(resp.headers().get("location").is_none());
}
#[actix_web::test]
async fn relative_redirect_one_is_relative_get() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/relative-redirect/1")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
let location = resp
.headers()
.get("location")
.expect("location header present")
.to_str()
.unwrap();
assert!(location.starts_with('/'));
assert!(!location.starts_with("http"));
assert_eq!(location, "/get");
}
#[actix_web::test]
async fn relative_redirect_two_decrements() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/relative-redirect/2")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
let location = resp
.headers()
.get("location")
.expect("location header present")
.to_str()
.unwrap();
assert_eq!(location, "/relative-redirect/1");
}
#[actix_web::test]
async fn relative_redirect_zero_is_bad_request() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/relative-redirect/0")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert!(resp.headers().get("location").is_none());
}
#[actix_web::test]
async fn absolute_redirect_one_is_absolute_get() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/absolute-redirect/1")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
let location = resp
.headers()
.get("location")
.expect("location header present")
.to_str()
.unwrap();
assert!(
location.starts_with("http://") || location.starts_with("https://"),
"expected absolute URL, got {location}"
);
assert!(location.ends_with("/get"));
}
#[actix_web::test]
async fn absolute_redirect_two_decrements_absolute() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/absolute-redirect/2")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
let location = resp
.headers()
.get("location")
.expect("location header present")
.to_str()
.unwrap();
assert!(
location.starts_with("http://") || location.starts_with("https://"),
"expected absolute URL, got {location}"
);
assert!(location.ends_with("/absolute-redirect/1"));
}
#[actix_web::test]
async fn absolute_redirect_zero_is_bad_request() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/absolute-redirect/0")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert!(resp.headers().get("location").is_none());
}
#[actix_web::test]
async fn redirect_to_get_with_query_url() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/redirect-to?url=https%3A%2F%2Fexample.com")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
let location = resp
.headers()
.get("location")
.expect("location header present")
.to_str()
.unwrap();
assert_eq!(location, "https://example.com");
}
#[actix_web::test]
async fn redirect_to_get_missing_url_is_bad_request() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/redirect-to").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert!(resp.headers().get("location").is_none());
}
#[actix_web::test]
async fn redirect_to_post_form_url() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/redirect-to")
.insert_header(("content-type", "application/x-www-form-urlencoded"))
.set_payload("url=https%3A%2F%2Fexample.org")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
let location = resp
.headers()
.get("location")
.expect("location header present")
.to_str()
.unwrap();
assert_eq!(location, "https://example.org");
}
#[actix_web::test]
async fn redirect_to_post_json_url() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/redirect-to")
.insert_header(("content-type", "application/json"))
.set_payload(r#"{"url":"https://json.example"}"#)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
let location = resp
.headers()
.get("location")
.expect("location header present")
.to_str()
.unwrap();
assert_eq!(location, "https://json.example");
}
#[actix_web::test]
async fn redirect_to_post_missing_url_is_bad_request() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/redirect-to")
.insert_header(("content-type", "application/x-www-form-urlencoded"))
.set_payload("")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert!(resp.headers().get("location").is_none());
}
#[actix_web::test]
async fn sse_simple_count_frames() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/sse?count=3&delay=0&format=simple")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"text/event-stream"
);
let text = String::from_utf8(test::read_body(resp).await.to_vec()).expect("utf8 body");
assert_eq!(text.matches("data: {").count(), 3);
assert_eq!(text.matches("data: ").count(), 5);
assert!(text.contains("event: end"));
assert!(text.contains("data: [DONE]"));
assert!(text.contains("Hello from HTTPCan SSE! Event 1/3"));
assert!(text.contains("Hello from HTTPCan SSE! Event 3/3"));
}
#[actix_web::test]
async fn sse_path_count() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/sse/2?delay=0").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"text/event-stream"
);
let text = String::from_utf8(test::read_body(resp).await.to_vec()).expect("utf8 body");
assert_eq!(text.matches("data: {").count(), 2);
assert_eq!(text.matches("data: ").count(), 4);
assert!(text.contains("data: [DONE]"));
assert!(text.contains("Hello from HTTPCan SSE! Event 1/2"));
assert!(text.contains("Hello from HTTPCan SSE! Event 2/2"));
}
#[actix_web::test]
async fn sse_openai_format() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/sse?count=2&delay=0&format=openai")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"text/event-stream"
);
let text = String::from_utf8(test::read_body(resp).await.to_vec()).expect("utf8 body");
assert_eq!(text.matches("\"choices\"").count(), 3);
assert!(text.contains("\"object\":\"chat.completion.chunk\""));
assert!(text.contains("\"model\":\"httpcan-sse\""));
assert!(text.contains("\"role\":\"assistant\""));
assert!(text.contains("\"finish_reason\":\"stop\""));
assert!(text.contains("data: [DONE]"));
assert!(text.contains("event: message"));
}
#[actix_web::test]
async fn ndjson_simple_count() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/ndjson?count=3&delay=0&format=simple")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/x-ndjson"
);
let text = String::from_utf8(test::read_body(resp).await.to_vec()).expect("utf8 body");
let objects: Vec<&str> = text.split('\n').filter(|l| !l.is_empty()).collect();
assert_eq!(
objects.len(),
3,
"exactly 3 NDJSON objects; body was:\n{text}"
);
for (i, line) in objects.iter().enumerate() {
let v: serde_json::Value = serde_json::from_str(line).expect("valid JSON object");
assert_eq!(v["event"].as_u64(), Some((i + 1) as u64));
assert!(v["message"]
.as_str()
.unwrap()
.contains("Hello from HTTPCan NDJSON!"));
assert!(v.get("timestamp").is_some());
}
}
#[actix_web::test]
async fn ndjson_path_count() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/ndjson/2?delay=0")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/x-ndjson"
);
let text = String::from_utf8(test::read_body(resp).await.to_vec()).expect("utf8 body");
let objects: Vec<&str> = text.split('\n').filter(|l| !l.is_empty()).collect();
assert_eq!(
objects.len(),
2,
"exactly 2 NDJSON objects; body was:\n{text}"
);
let first: serde_json::Value = serde_json::from_str(objects[0]).expect("valid JSON");
assert_eq!(first["event"].as_u64(), Some(1));
}
#[actix_web::test]
async fn ndjson_ollama_format() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/ndjson?count=1&delay=0&format=ollama")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"application/x-ndjson"
);
let text = String::from_utf8(test::read_body(resp).await.to_vec()).expect("utf8 body");
let objects: Vec<&str> = text.split('\n').filter(|l| !l.is_empty()).collect();
assert_eq!(objects.len(), 1);
let v: serde_json::Value = serde_json::from_str(objects[0]).expect("valid JSON object");
assert_eq!(v["model"].as_str(), Some("llama2"));
assert!(v["response"].is_string());
assert_eq!(v["done"].as_bool(), Some(true));
assert!(v.get("created_at").is_some());
assert!(v.get("context").is_some());
assert!(v.get("eval_count").is_some());
assert!(v.get("total_duration").is_some());
}
#[actix_web::test]
async fn auth_basic_correct_creds() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/basic-auth/user/passwd")
.insert_header(("Authorization", "Basic dXNlcjpwYXNzd2Q=")) .to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v.get("authenticated").and_then(|x| x.as_bool()), Some(true));
assert_eq!(v.get("user").and_then(|x| x.as_str()), Some("user"));
}
#[actix_web::test]
async fn auth_basic_no_creds() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/basic-auth/user/passwd")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let www = resp
.headers()
.get("WWW-Authenticate")
.expect("WWW-Authenticate present");
assert!(www.to_str().unwrap().starts_with("Basic realm="));
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(
v.get("authenticated").and_then(|x| x.as_bool()),
Some(false)
);
}
#[actix_web::test]
async fn auth_basic_wrong_creds() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/basic-auth/user/passwd")
.insert_header(("Authorization", "Basic d3Jvbmc6d3Jvbmc=")) .to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
assert!(resp.headers().contains_key("WWW-Authenticate"));
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(
v.get("authenticated").and_then(|x| x.as_bool()),
Some(false)
);
}
#[actix_web::test]
async fn auth_basic_user_only_empty_password() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/basic-auth/user")
.insert_header(("Authorization", "Basic dXNlcjo=")) .to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v.get("authenticated").and_then(|x| x.as_bool()), Some(true));
assert_eq!(v.get("user").and_then(|x| x.as_str()), Some("user"));
}
#[actix_web::test]
async fn auth_hidden_basic_correct_creds() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/hidden-basic-auth/user/passwd")
.insert_header(("Authorization", "Basic dXNlcjpwYXNzd2Q=")) .to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v.get("authenticated").and_then(|x| x.as_bool()), Some(true));
assert_eq!(v.get("user").and_then(|x| x.as_str()), Some("user"));
}
#[actix_web::test]
async fn auth_hidden_basic_no_creds_is_404() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/hidden-basic-auth/user/passwd")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert!(resp.headers().get("WWW-Authenticate").is_none());
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(
v.get("authenticated").and_then(|x| x.as_bool()),
Some(false)
);
}
#[actix_web::test]
async fn auth_hidden_basic_wrong_creds_is_404() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/hidden-basic-auth/user/passwd")
.insert_header(("Authorization", "Basic d3Jvbmc6d3Jvbmc=")) .to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert!(resp.headers().get("WWW-Authenticate").is_none());
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(
v.get("authenticated").and_then(|x| x.as_bool()),
Some(false)
);
}
#[actix_web::test]
async fn auth_hidden_basic_user_only_succeeds_and_hides_failure() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/hidden-basic-auth/user")
.insert_header(("Authorization", "Basic dXNlcjo="))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v.get("authenticated").and_then(|x| x.as_bool()), Some(true));
let req = test::TestRequest::get()
.uri("/hidden-basic-auth/user")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
assert!(resp.headers().get("WWW-Authenticate").is_none());
}
#[actix_web::test]
async fn auth_bearer_valid_token() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/bearer")
.insert_header(("Authorization", "Bearer my-secret-token"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v.get("authenticated").and_then(|x| x.as_bool()), Some(true));
assert_eq!(
v.get("token").and_then(|x| x.as_str()),
Some("my-secret-token")
);
}
#[actix_web::test]
async fn auth_bearer_no_header() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/bearer").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let www = resp
.headers()
.get("WWW-Authenticate")
.expect("WWW-Authenticate present");
assert_eq!(www.to_str().unwrap(), "Bearer");
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(
v.get("authenticated").and_then(|x| x.as_bool()),
Some(false)
);
}
#[actix_web::test]
async fn auth_bearer_invalid_scheme() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/bearer")
.insert_header(("Authorization", "Basic dXNlcjpwYXNzd2Q="))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
assert_eq!(
resp.headers()
.get("WWW-Authenticate")
.unwrap()
.to_str()
.unwrap(),
"Bearer"
);
}
#[actix_web::test]
async fn auth_jwt_valid_decodes_claims() {
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.\
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/jwt-bearer")
.insert_header(("Authorization", format!("Bearer {}", token)))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v.get("authenticated").and_then(|x| x.as_bool()), Some(true));
assert_eq!(
v.get("token")
.and_then(|t| t.get("raw"))
.and_then(|x| x.as_str()),
Some(token)
);
assert_eq!(
v.get("token")
.and_then(|t| t.get("header"))
.and_then(|h| h.get("alg"))
.and_then(|x| x.as_str()),
Some("HS256")
);
assert_eq!(
v.get("token")
.and_then(|t| t.get("header"))
.and_then(|h| h.get("typ"))
.and_then(|x| x.as_str()),
Some("JWT")
);
assert_eq!(
v.get("token")
.and_then(|t| t.get("payload"))
.and_then(|p| p.get("sub"))
.and_then(|x| x.as_str()),
Some("1234567890")
);
assert_eq!(
v.get("token")
.and_then(|t| t.get("payload"))
.and_then(|p| p.get("name"))
.and_then(|x| x.as_str()),
Some("John Doe")
);
assert_eq!(
v.get("token")
.and_then(|t| t.get("payload"))
.and_then(|p| p.get("iat"))
.and_then(|x| x.as_i64()),
Some(1516239022)
);
assert!(
v.get("token")
.and_then(|t| t.get("payloadFormatted"))
.and_then(|p| p.get("iat"))
.map(|x| x.is_string())
.unwrap_or(false),
"payloadFormatted.iat must be a formatted timestamp string"
);
assert_eq!(
v.get("token")
.and_then(|t| t.get("validationStatus"))
.and_then(|s| s.get("structure"))
.and_then(|x| x.as_str()),
Some("valid")
);
assert_eq!(
v.get("token")
.and_then(|t| t.get("validationStatus"))
.and_then(|s| s.get("expiration"))
.and_then(|x| x.as_str()),
Some("not_present")
);
}
#[actix_web::test]
async fn auth_jwt_malformed_is_401() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/jwt-bearer")
.insert_header(("Authorization", "Bearer not-a-real-jwt")) .to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
assert_eq!(
resp.headers()
.get("WWW-Authenticate")
.unwrap()
.to_str()
.unwrap(),
"Bearer"
);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(
v.get("authenticated").and_then(|x| x.as_bool()),
Some(false)
);
assert!(v.get("error").is_some());
}
#[actix_web::test]
async fn auth_jwt_missing_header_is_401() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/jwt-bearer").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
assert_eq!(
resp.headers()
.get("WWW-Authenticate")
.unwrap()
.to_str()
.unwrap(),
"Bearer"
);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(
v.get("authenticated").and_then(|x| x.as_bool()),
Some(false)
);
assert_eq!(
v.get("error").and_then(|x| x.as_str()),
Some("Missing Authorization header")
);
}
#[actix_web::test]
async fn auth_jwt_wrong_scheme_is_401() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/jwt-bearer")
.insert_header(("Authorization", "Basic dXNlcjpwYXNzd2Q="))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
assert_eq!(
resp.headers()
.get("WWW-Authenticate")
.unwrap()
.to_str()
.unwrap(),
"Bearer"
);
}
#[actix_web::test]
async fn auth_digest_challenge_shape() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/digest-auth/auth/user/passwd")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let challenge = resp
.headers()
.get("WWW-Authenticate")
.expect("WWW-Authenticate present")
.to_str()
.unwrap()
.to_string();
assert!(
challenge.starts_with("Digest "),
"challenge must be Digest scheme: {}",
challenge
);
assert!(
challenge.contains("realm=\"httpcan@"),
"realm must be prefixed with httpcan@: {}",
challenge
);
assert!(
challenge.contains("nonce=\""),
"missing nonce: {}",
challenge
);
assert!(
challenge.contains("opaque=\""),
"missing opaque: {}",
challenge
);
assert!(
challenge.contains("qop=\"auth\""),
"missing/incorrect qop directive: {}",
challenge
);
assert!(
challenge.contains("algorithm=\"MD5\""),
"missing/incorrect algorithm: {}",
challenge
);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(
v.get("authenticated").and_then(|x| x.as_bool()),
Some(false)
);
}
#[actix_web::test]
async fn cookie_get_parses_multiple_pairs() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/cookies")
.insert_header(("Cookie", "k1=v1; k2=v2"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert!(v.get("cookies").is_some(), "response has cookies object");
assert_eq!(v["cookies"]["k1"].as_str(), Some("v1"));
assert_eq!(v["cookies"]["k2"].as_str(), Some("v2"));
}
#[actix_web::test]
async fn cookie_get_empty_when_no_header() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/cookies").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert!(v["cookies"].is_object(), "cookies is a JSON object");
assert!(v["cookies"].as_object().unwrap().is_empty());
}
#[actix_web::test]
async fn cookie_set_named_redirects_and_sets_cookie() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/cookies/set/foo/bar")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
let set_cookie = resp
.headers()
.get("set-cookie")
.expect("Set-Cookie header present")
.to_str()
.unwrap();
assert!(
set_cookie.contains("foo=bar"),
"Set-Cookie sets foo=bar: {set_cookie}"
);
assert!(
set_cookie.contains("Path=/"),
"cookie scoped to Path=/: {set_cookie}"
);
assert_eq!(
resp.headers().get("location").unwrap().to_str().unwrap(),
"/cookies",
"redirects to /cookies"
);
}
#[actix_web::test]
async fn cookie_delete_redirects_and_clears_cookie() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/cookies/delete?foo=")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
let set_cookie = resp
.headers()
.get("set-cookie")
.expect("Set-Cookie header present")
.to_str()
.unwrap();
assert!(
set_cookie.contains("foo=;"),
"Set-Cookie clears foo with empty value: {set_cookie}"
);
assert!(
set_cookie.contains("Max-Age=0"),
"Set-Cookie uses Max-Age=0 to expire: {set_cookie}"
);
assert_eq!(
resp.headers().get("location").unwrap().to_str().unwrap(),
"/cookies",
"redirects to /cookies"
);
}
#[actix_web::test]
async fn dyn_uuid_returns_valid_v4_uuid() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/uuid").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
let uuid_str = v["uuid"].as_str().expect("uuid field is a string");
assert_eq!(uuid_str.len(), 36, "uuid must be 36 chars");
assert_eq!(
uuid_str.chars().filter(|c| *c == '-').count(),
4,
"uuid must contain 4 dashes"
);
assert!(
uuid_str.chars().all(|c| c.is_ascii_hexdigit() || c == '-'),
"uuid must be hex+dashes only"
);
assert_eq!(
uuid_str.as_bytes()[14],
b'4',
"uuid version nibble must be 4"
);
let variant = uuid_str.as_bytes()[19] as char;
assert!(
matches!(variant, '8' | '9' | 'a' | 'b'),
"uuid variant nibble must be one of 8/9/a/b, got {variant}"
);
}
#[actix_web::test]
async fn dyn_stream_ndjson_objects() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/stream/3").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let ct = resp
.headers()
.get("content-type")
.expect("content-type header")
.to_str()
.unwrap();
assert!(
ct.starts_with("application/json"),
"stream content-type should be application/json, got {ct}"
);
let body = test::read_body(resp).await;
let text = std::str::from_utf8(&body).expect("utf8 body");
let objects: Vec<&str> = text.lines().collect();
assert_eq!(objects.len(), 3, "expected 3 ndjson objects");
for (idx, line) in objects.iter().enumerate() {
let obj: serde_json::Value = serde_json::from_str(line).expect("each line is JSON");
assert_eq!(obj["id"].as_u64(), Some(idx as u64), "sequential id");
assert!(obj.get("url").is_some(), "url field present");
assert!(obj.get("args").is_some(), "args field present");
assert!(obj.get("headers").is_some(), "headers field present");
assert!(obj.get("origin").is_some(), "origin field present");
}
}
#[actix_web::test]
async fn dyn_range_full_content_200() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/range/1024").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let ct = resp
.headers()
.get("content-type")
.expect("content-type header")
.to_str()
.unwrap();
assert!(
ct.starts_with("application/octet-stream"),
"range content-type should be application/octet-stream, got {ct}"
);
assert_eq!(
resp.headers()
.get("accept-ranges")
.and_then(|v| v.to_str().ok()),
Some("bytes")
);
let body = test::read_body(resp).await;
assert_eq!(body.len(), 1024, "full range body must be 1024 bytes");
assert_eq!(body[0], b'a');
assert_eq!(body[25], b'z');
assert_eq!(body[26], b'a');
}
#[actix_web::test]
async fn dyn_range_partial_206() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/range/1024")
.insert_header(("Range", "bytes=10-19"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::PARTIAL_CONTENT);
assert_eq!(
resp.headers()
.get("content-range")
.and_then(|v| v.to_str().ok()),
Some("bytes 10-19/1024")
);
assert_eq!(
resp.headers()
.get("content-length")
.and_then(|v| v.to_str().ok()),
Some("10")
);
let body = test::read_body(resp).await;
assert_eq!(body.len(), 10, "partial body must be 10 bytes");
for (offset, byte) in body.iter().enumerate() {
let i = 10 + offset;
assert_eq!(*byte, b'a' + (i % 26) as u8, "byte at position {i}");
}
}
#[actix_web::test]
async fn dyn_links_redirect_302() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/links/2").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
assert_eq!(
resp.headers().get("location").and_then(|v| v.to_str().ok()),
Some("/links/2/0")
);
}
#[actix_web::test]
async fn dyn_links_page_html_entries() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/links/2/0").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let ct = resp
.headers()
.get("content-type")
.expect("content-type header")
.to_str()
.unwrap();
assert!(
ct.starts_with("text/html"),
"links content-type should be text/html, got {ct}"
);
let body = test::read_body(resp).await;
let html = std::str::from_utf8(&body).expect("utf8 html");
assert!(html.contains("<!DOCTYPE html>"), "html doctype");
assert!(html.contains("0<br>"), "offset entry rendered as text");
assert!(
html.contains("<a href='/links/2/1'>1</a><br>"),
"non-offset entry rendered as anchor"
);
let anchor_count = html.matches("<a ").count();
assert_eq!(anchor_count, 1, "only the non-offset entry is a link");
}
#[actix_web::test]
async fn dyn_delay_get_echoes_request() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/delay/0?x=1")
.insert_header(("x-test-echo", "yes"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["method"].as_str(), Some("GET"));
assert!(
v["url"].as_str().unwrap_or("").contains("/delay/0"),
"url echoed: {}",
v["url"]
);
assert_eq!(v["args"]["x"].as_str(), Some("1"), "query args echoed");
assert!(v.get("headers").is_some(), "headers echoed");
assert!(v.get("origin").is_some(), "origin echoed");
assert_eq!(v["data"].as_str(), Some(""), "no body data for GET");
}
#[actix_web::test]
async fn dyn_delay_post_echoes_body() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/delay/0")
.insert_header(("content-type", "text/plain"))
.set_payload("hello-dyn")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["method"].as_str(), Some("POST"));
assert_eq!(v["data"].as_str(), Some("hello-dyn"), "body echoed in data");
assert!(
v["url"].as_str().unwrap_or("").contains("/delay/0"),
"url echoed"
);
}
#[actix_web::test]
async fn img_png_returns_valid_png() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/image/png").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"image/png"
);
let body = test::read_body(resp).await;
assert!(!body.is_empty(), "png body must not be empty");
assert!(
body.starts_with(&[0x89, b'P', b'N', b'G']),
"png body must start with PNG magic bytes, got: {:?}",
&body[..body.len().min(8)]
);
}
#[actix_web::test]
async fn img_jpeg_returns_valid_jpeg() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/image/jpeg").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"image/jpeg"
);
let body = test::read_body(resp).await;
assert!(!body.is_empty(), "jpeg body must not be empty");
assert!(
body.starts_with(&[0xff, 0xd8, 0xff]),
"jpeg body must start with JPEG magic bytes, got: {:?}",
&body[..body.len().min(4)]
);
}
#[actix_web::test]
async fn img_webp_returns_valid_webp() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/image/webp").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"image/webp"
);
let body = test::read_body(resp).await;
assert!(body.len() > 12, "webp body must be at least 13 bytes");
assert_eq!(&body[..4], b"RIFF", "webp body must start with RIFF marker");
assert_eq!(
&body[8..12],
b"WEBP",
"webp body must carry WEBP fourcc at offset 8..12"
);
}
#[actix_web::test]
async fn img_svg_returns_valid_svg() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/image/svg").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"image/svg+xml"
);
let body = test::read_body(resp).await;
assert!(!body.is_empty(), "svg body must not be empty");
let text = std::str::from_utf8(&body).expect("svg body must be valid UTF-8");
assert!(
text.contains("<svg"),
"svg body must contain the <svg element"
);
}
#[actix_web::test]
async fn image_accept_png_negotiated() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/image")
.insert_header(("Accept", "image/png"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"image/png"
);
let body = test::read_body(resp).await;
assert!(
body.starts_with(&[0x89, b'P', b'N', b'G']),
"negotiated png body must start with PNG magic bytes"
);
}
#[actix_web::test]
async fn image_accept_wildcard_random_image_type() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/image")
.insert_header(("Accept", "*/*"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let ct = resp
.headers()
.get("content-type")
.expect("content-type header present")
.to_str()
.unwrap()
.to_owned();
assert!(
ct.starts_with("image/"),
"default /image content-type must be an image type, got: {ct}"
);
}
#[actix_web::test]
async fn image_no_accept_header_random_image_type() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/image").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let ct = resp
.headers()
.get("content-type")
.expect("content-type header present")
.to_str()
.unwrap()
.to_owned();
assert!(
ct.starts_with("image/"),
"default /image content-type must be an image type, got: {ct}"
);
}
#[actix_web::test]
async fn inspect_user_agent() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/user-agent")
.insert_header(("User-Agent", "TestAgent/1.0"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["user-agent"].as_str(), Some("TestAgent/1.0"));
}
#[actix_web::test]
async fn inspect_ip_origin_present() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/ip").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
let origin = v["origin"]
.as_str()
.unwrap_or_else(|| panic!("origin not a string: {:?}", v["origin"]));
assert!(!origin.is_empty(), "origin should be a non-empty string");
}
#[actix_web::test]
async fn inspect_headers_custom_and_standard() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/headers")
.insert_header(("X-Foo", "bar"))
.insert_header(("host", "example.com"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
let headers = v["headers"]
.as_object()
.expect("headers should be an object");
assert_eq!(headers.get("x-foo").and_then(|h| h.as_str()), Some("bar"));
assert_eq!(
headers.get("host").and_then(|h| h.as_str()),
Some("example.com")
);
}
#[actix_web::test]
async fn cache_no_conditional_returns_200_with_cache_headers() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/cache").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let last_modified = resp
.headers()
.get("last-modified")
.expect("Last-Modified header present");
assert!(!last_modified.is_empty());
let etag = resp.headers().get("etag").expect("ETag header present");
assert!(!etag.is_empty());
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["method"].as_str(), Some("GET"));
assert!(v["headers"].is_object(), "headers object present");
assert!(v["url"].is_string(), "url field present");
assert!(v["origin"].is_string(), "origin field present");
}
#[actix_web::test]
async fn cache_if_modified_since_returns_304() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/cache")
.insert_header(("If-Modified-Since", "Wed, 21 Oct 2015 07:28:00 GMT"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
assert!(resp.headers().contains_key("last-modified"));
assert!(resp.headers().contains_key("etag"));
}
#[actix_web::test]
async fn cache_if_none_match_returns_304() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/cache")
.insert_header(("If-None-Match", "\"abc123\""))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
assert!(resp.headers().contains_key("last-modified"));
assert!(resp.headers().contains_key("etag"));
}
#[actix_web::test]
async fn cache_control_max_age_60() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/cache/60").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("cache-control")
.unwrap()
.to_str()
.unwrap(),
"public, max-age=60"
);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
assert_eq!(v["method"].as_str(), Some("GET"));
assert!(v["headers"].is_object());
}
#[actix_web::test]
async fn cache_control_max_age_0() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/cache/0").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("cache-control")
.unwrap()
.to_str()
.unwrap(),
"public, max-age=0"
);
}
#[actix_web::test]
async fn mw_exclude_header_removes_named_header() {
let app = test::init_service(create_app(
ServerConfig::default().exclude_header("x-secret"),
))
.await;
let req = test::TestRequest::get()
.uri("/headers")
.insert_header(("X-Secret", "hidden"))
.insert_header(("X-Other", "v"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
let headers = v
.get("headers")
.and_then(|h| h.as_object())
.expect("headers object");
assert!(
headers.get("x-secret").is_none(),
"excluded x-secret must not appear in headers"
);
assert_eq!(
headers.get("x-other").and_then(|v| v.as_str()),
Some("v"),
"non-excluded x-other must be present"
);
}
#[actix_web::test]
async fn mw_exclude_header_wildcard_suffix() {
let app = test::init_service(create_app(
ServerConfig::default().exclude_header("x-prod-*"),
))
.await;
let req = test::TestRequest::get()
.uri("/get")
.insert_header(("X-Prod-Token", "t"))
.insert_header(("X-Dev-Token", "d"))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
let headers = v
.get("headers")
.and_then(|h| h.as_object())
.expect("headers object");
assert!(
headers.get("x-prod-token").is_none(),
"wildcard-matched x-prod-token must not appear in headers"
);
assert_eq!(
headers.get("x-dev-token").and_then(|v| v.as_str()),
Some("d"),
"x-dev-token is outside the wildcard prefix and must remain"
);
}
#[actix_web::test]
async fn root_always_serves_html() {
let app = test::init_service(create_app(cfg())).await;
for accept in ["text/html", "application/json", "*/*"] {
let req = test::TestRequest::get()
.uri("/")
.insert_header(("Accept", accept))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let ct = resp
.headers()
.get("content-type")
.expect("content-type header")
.to_str()
.unwrap();
assert!(
ct.starts_with("text/html"),
"expected text/html content-type for Accept: {accept}, got {ct}"
);
let body = test::read_body(resp).await;
let body_str = std::str::from_utf8(&body).expect("utf-8 html body");
assert!(
body_str.to_lowercase().contains("<html"),
"html body must contain an <html> tag"
);
}
}
#[actix_web::test]
async fn root_serves_html_without_accept_header() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let ct = resp
.headers()
.get("content-type")
.expect("content-type header")
.to_str()
.unwrap();
assert!(ct.starts_with("text/html"));
}
#[actix_web::test]
async fn root_homepage_lists_endpoints_and_links_to_openapi() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/").to_request();
let resp = test::call_service(&app, req).await;
let body = test::read_body(resp).await;
let body_str = std::str::from_utf8(&body).expect("utf-8 html body");
assert!(body_str.contains("/get"), "homepage should list /get");
assert!(body_str.contains("/sse"), "homepage should list /sse");
assert!(
body_str.contains("/openapi.json"),
"homepage should link to the OpenAPI spec"
);
}
#[actix_web::test]
async fn root_homepage_has_copy_curl_buttons() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/")
.insert_header(("Host", "example.com"))
.to_request();
let resp = test::call_service(&app, req).await;
let body = test::read_body(resp).await;
let body_str = std::str::from_utf8(&body).expect("utf-8 html body");
assert!(
body_str.contains(r#"class="copy-btn""#),
"homepage should render a copy-curl button for each endpoint"
);
assert!(
body_str.contains(r#"data-curl="curl http://example.com/uuid""#),
"copy button for /uuid should target the request's own origin"
);
assert!(
body_str.contains("navigator.clipboard"),
"homepage should include the copy-to-clipboard script"
);
}
#[actix_web::test]
async fn openapi_json_returns_valid_spec() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/openapi.json").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let ct = resp
.headers()
.get("content-type")
.expect("content-type header")
.to_str()
.unwrap();
assert!(
ct.starts_with("application/json"),
"expected application/json content-type, got {ct}"
);
let v: serde_json::Value =
serde_json::from_slice(&test::read_body(resp).await).expect("JSON body");
let version = v
.get("openapi")
.and_then(|o| o.as_str())
.expect("openapi version string");
assert!(
version.starts_with("3."),
"openapi version should be 3.x, got {version}"
);
let paths = v
.get("paths")
.and_then(|p| p.as_object())
.expect("paths object");
assert!(
paths.contains_key("/get"),
"paths must include /get, got keys: {:?}",
paths.keys().collect::<Vec<_>>()
);
}
#[actix_web::test]
async fn sse_count_delay_path_variant() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/sse/2/0").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"text/event-stream"
);
let body = String::from_utf8(test::read_body(resp).await.to_vec()).expect("utf-8");
let data_lines = body.lines().filter(|l| l.starts_with("data:")).count();
assert_eq!(
data_lines, 4,
"expected 4 data: lines (2 events + end + DONE): {body}"
);
}
#[actix_web::test]
async fn ndjson_count_delay_path_variant() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/ndjson/2/0").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = String::from_utf8(test::read_body(resp).await.to_vec()).expect("utf-8");
let objs: Vec<&str> = body.lines().filter(|l| !l.is_empty()).collect();
assert_eq!(objs.len(), 2, "expected exactly 2 NDJSON objects: {body}");
for o in &objs {
serde_json::from_str::<serde_json::Value>(o).expect("each line is valid JSON");
}
}
#[actix_web::test]
async fn status_comma_separated_picks_a_listed_code() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get().uri("/status/200,418").to_request();
let resp = test::call_service(&app, req).await;
let code = resp.status().as_u16();
assert!(
matches!(code, 200 | 418),
"must pick one of the listed codes, got {code}"
);
}
#[actix_web::test]
async fn status_post_echoes_request_body() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/status/200")
.set_payload("hello-body")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(test::read_body(resp).await, "hello-body");
}
#[actix_web::test]
async fn status_options_returns_cors_preflight() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::default()
.method(actix_web::http::Method::OPTIONS)
.uri("/status/200")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let allow = resp
.headers()
.get("access-control-allow-methods")
.expect("CORS allow-methods header")
.to_str()
.unwrap();
assert!(
allow.contains("GET") && allow.contains("POST"),
"preflight must list allowed methods: {allow}"
);
assert!(resp.headers().contains_key("access-control-max-age"));
}
#[actix_web::test]
async fn response_headers_post_sets_headers() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/response-headers?X-Post-Test=bar")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get("x-post-test").unwrap().to_str().unwrap(),
"bar"
);
}
#[actix_web::test]
async fn digest_auth_full_five_segment_sets_stale_after_cookie() {
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::get()
.uri("/digest-auth/auth/user/passwd/MD5/never")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let challenge = resp
.headers()
.get("www-authenticate")
.expect("Digest challenge")
.to_str()
.unwrap();
assert!(
challenge.starts_with("Digest"),
"challenge must be Digest scheme: {challenge}"
);
let set_cookie = resp
.headers()
.get_all("set-cookie")
.map(|v| v.to_str().unwrap_or(""))
.collect::<Vec<_>>()
.join("; ");
assert!(
set_cookie.contains("stale_after=never"),
"5-segment route must set stale_after cookie: {set_cookie}"
);
}
#[actix_web::test]
async fn compress_post_gzip_encodes_body() {
let payload = "compress-me-gzip";
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/gzip")
.set_payload(payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-encoding")
.unwrap()
.to_str()
.unwrap(),
"gzip"
);
let mut decoded = Vec::new();
flate2::read::GzDecoder::new(&test::read_body(resp).await[..])
.read_to_end(&mut decoded)
.expect("valid gzip");
assert_eq!(decoded, payload.as_bytes());
}
#[actix_web::test]
async fn compress_post_deflate_encodes_body() {
let payload = "compress-me-deflate";
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/deflate")
.set_payload(payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-encoding")
.unwrap()
.to_str()
.unwrap(),
"deflate"
);
let mut decoded = Vec::new();
flate2::read::DeflateDecoder::new(&test::read_body(resp).await[..])
.read_to_end(&mut decoded)
.expect("valid raw deflate");
assert_eq!(decoded, payload.as_bytes());
}
#[actix_web::test]
async fn compress_post_brotli_encodes_body() {
let payload = "compress-me-brotli";
let app = test::init_service(create_app(cfg())).await;
let req = test::TestRequest::post()
.uri("/brotli")
.set_payload(payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-encoding")
.unwrap()
.to_str()
.unwrap(),
"br"
);
let mut decoded = Vec::new();
brotli::Decompressor::new(&test::read_body(resp).await[..], 4096)
.read_to_end(&mut decoded)
.expect("valid brotli");
assert_eq!(decoded, payload.as_bytes());
}