use std::collections::BTreeMap;
use foundation_netio::shared::http::{
SimpleHeader, SimpleHeaders, SimpleIncomingRequest, SimpleMethod,
};
use foundation_proxy::forward::{
forward_request_headers, is_streaming_request, is_upgrade_request, strip_hop_by_hop,
strip_hop_by_hop_request,
};
#[test]
fn test_strip_removes_hop_by_hop_and_connection_listed() {
let mut headers: SimpleHeaders = BTreeMap::new();
headers.insert(SimpleHeader::CONNECTION, vec!["close, x-custom".to_string()]);
headers.insert(SimpleHeader::custom("x-custom"), vec!["secret".to_string()]);
headers.insert(SimpleHeader::TRANSFER_ENCODING, vec!["chunked".to_string()]);
headers.insert(SimpleHeader::KEEP_ALIVE, vec!["timeout=5".to_string()]);
headers.insert(SimpleHeader::CONTENT_TYPE, vec!["text/plain".to_string()]);
headers.insert(SimpleHeader::HOST, vec!["app.example.com".to_string()]);
let stripped = strip_hop_by_hop(&headers);
assert!(!stripped.contains_key(&SimpleHeader::CONNECTION), "Connection is hop-by-hop");
assert!(
!stripped.contains_key(&SimpleHeader::custom("x-custom")),
"Connection-listed header must be dropped"
);
assert!(!stripped.contains_key(&SimpleHeader::TRANSFER_ENCODING), "Transfer-Encoding is hop-by-hop");
assert!(!stripped.contains_key(&SimpleHeader::KEEP_ALIVE), "Keep-Alive is hop-by-hop");
assert!(stripped.contains_key(&SimpleHeader::CONTENT_TYPE), "Content-Type is end-to-end");
assert!(stripped.contains_key(&SimpleHeader::HOST), "Host is preserved");
}
#[test]
fn test_upgrade_detection() {
let ws = SimpleIncomingRequest::builder()
.with_plain_url("/ws")
.with_method(SimpleMethod::GET)
.add_header(SimpleHeader::CONNECTION, "Upgrade")
.add_header(SimpleHeader::UPGRADE, "websocket")
.build()
.expect("build ws request");
assert!(is_upgrade_request(&ws), "Connection: upgrade + Upgrade header is an upgrade");
let plain = SimpleIncomingRequest::builder()
.with_plain_url("/")
.with_method(SimpleMethod::GET)
.add_header(SimpleHeader::CONNECTION, "keep-alive")
.build()
.expect("build plain request");
assert!(!is_upgrade_request(&plain), "no Upgrade header โ not an upgrade");
let missing_conn = SimpleIncomingRequest::builder()
.with_plain_url("/")
.with_method(SimpleMethod::GET)
.add_header(SimpleHeader::UPGRADE, "websocket")
.build()
.expect("build request");
assert!(
!is_upgrade_request(&missing_conn),
"Upgrade header without Connection: upgrade โ not an upgrade"
);
}
#[test]
fn test_request_strip_preserves_grpc_trailer_headers() {
let mut headers: SimpleHeaders = BTreeMap::new();
headers.insert(SimpleHeader::TE, vec!["trailers".to_string()]);
headers.insert(SimpleHeader::TRAILER, vec!["grpc-status".to_string()]);
headers.insert(SimpleHeader::custom("trailers"), vec!["grpc-message".to_string()]);
headers.insert(SimpleHeader::CONNECTION, vec!["close".to_string()]);
headers.insert(SimpleHeader::TRANSFER_ENCODING, vec!["chunked".to_string()]);
headers.insert(SimpleHeader::KEEP_ALIVE, vec!["timeout=5".to_string()]);
headers.insert(SimpleHeader::CONTENT_TYPE, vec!["application/grpc+json".to_string()]);
let stripped = strip_hop_by_hop_request(&headers);
assert!(stripped.contains_key(&SimpleHeader::TE), "te: trailers must survive");
assert!(stripped.contains_key(&SimpleHeader::TRAILER), "trailer must survive");
assert!(
stripped.contains_key(&SimpleHeader::custom("trailers")),
"trailers must survive"
);
assert!(!stripped.contains_key(&SimpleHeader::CONNECTION), "Connection is hop-by-hop");
assert!(!stripped.contains_key(&SimpleHeader::TRANSFER_ENCODING), "Transfer-Encoding is hop-by-hop");
assert!(!stripped.contains_key(&SimpleHeader::KEEP_ALIVE), "Keep-Alive is hop-by-hop");
assert!(stripped.contains_key(&SimpleHeader::CONTENT_TYPE), "Content-Type is end-to-end");
}
#[test]
fn test_request_strip_drops_non_trailers_te() {
let mut headers: SimpleHeaders = BTreeMap::new();
headers.insert(SimpleHeader::TE, vec!["gzip".to_string()]);
let stripped = strip_hop_by_hop_request(&headers);
assert!(!stripped.contains_key(&SimpleHeader::TE), "te: gzip must be stripped");
}
fn headers_with_content_type(ct: &str) -> SimpleHeaders {
let mut headers: SimpleHeaders = BTreeMap::new();
headers.insert(SimpleHeader::CONTENT_TYPE, vec![ct.to_string()]);
headers.insert(SimpleHeader::HOST, vec!["app.example.com".to_string()]);
headers
}
#[test]
fn test_is_streaming_request() {
for ct in [
"application/grpc",
"application/grpc+proto",
"application/grpc+json",
"application/connect+proto",
"application/connect+json",
"application/grpc-web",
"application/grpc-web+proto",
"application/grpc-web-text",
] {
assert!(is_streaming_request(&headers_with_content_type(ct)), "{ct} is a streaming RPC type");
}
for ct in ["application/json", "application/proto", "text/plain", "application/octet-stream"] {
assert!(!is_streaming_request(&headers_with_content_type(ct)), "{ct} is not streaming RPC");
}
}
#[test]
fn test_forward_headers_relax_connection_close_for_streams() {
let plain = forward_request_headers(&headers_with_content_type("text/plain"), "1.2.3.4", "http");
assert!(
plain.contains_key(&SimpleHeader::CONNECTION),
"plain requests still get Connection: close"
);
for ct in [
"application/grpc",
"application/grpc+proto",
"application/connect+proto",
"application/grpc-web+proto",
] {
let out = forward_request_headers(&headers_with_content_type(ct), "1.2.3.4", "http");
assert!(
!out.contains_key(&SimpleHeader::CONNECTION),
"{ct} must not get Connection: close"
);
}
}