mod common;
use std::time::Duration;
use common::{BodyMode, Request as SReq, Response as SResp, TestServer};
use rsurl::{Error, Request};
#[test]
fn get_returns_body() {
let server = TestServer::start(|_req: SReq| SResp::ok("hello"));
let resp = Request::get(&server.url("/")).unwrap().send().unwrap();
assert_eq!(resp.status, 200);
assert_eq!(resp.reason, "OK");
assert_eq!(resp.body, b"hello");
}
#[test]
fn head_has_no_body() {
let server = TestServer::start(|_req: SReq| {
SResp {
status: 200,
reason: "OK".into(),
headers: vec![("Content-Length".into(), "5".into())],
body: Vec::new(),
mode: BodyMode::CloseDelimited, }
});
let resp = Request::new("HEAD", &server.url("/"))
.unwrap()
.send()
.unwrap();
assert_eq!(resp.status, 200);
assert!(resp.body.is_empty(), "HEAD body must be empty");
}
#[test]
fn chunked_encoding() {
let server = TestServer::start(|_req: SReq| {
SResp::ok(Vec::new()).mode(BodyMode::Chunked {
chunks: vec![b"abc".to_vec(), b"defg".to_vec(), b"hi".to_vec()],
trailers: vec![],
})
});
let resp = Request::get(&server.url("/")).unwrap().send().unwrap();
assert_eq!(resp.status, 200);
assert_eq!(resp.body, b"abcdefghi");
}
#[test]
fn chunked_with_trailers() {
let server = TestServer::start(|_req: SReq| {
SResp::ok(Vec::new()).mode(BodyMode::Chunked {
chunks: vec![b"hello ".to_vec(), b"world".to_vec()],
trailers: vec![("X-Trailer".into(), "ignored".into())],
})
});
let resp = Request::get(&server.url("/")).unwrap().send().unwrap();
assert_eq!(resp.status, 200);
assert_eq!(resp.body, b"hello world");
}
#[test]
fn content_length_mismatch_short() {
let server = TestServer::start(|_req: SReq| SResp {
status: 200,
reason: "OK".into(),
headers: vec![],
body: vec![b'a'; 100],
mode: BodyMode::ContentLengthShort {
declared: 100,
actual_len: 50,
},
});
let err = Request::get(&server.url("/")).unwrap().send().unwrap_err();
assert!(
matches!(err, Error::UnexpectedEof),
"expected UnexpectedEof, got {err:?}",
);
}
#[test]
fn close_delimited_body() {
let server = TestServer::start(|_req: SReq| SResp {
status: 200,
reason: "OK".into(),
headers: vec![],
body: b"hello".to_vec(),
mode: BodyMode::CloseDelimited,
});
let resp = Request::get(&server.url("/")).unwrap().send().unwrap();
assert_eq!(resp.status, 200);
assert_eq!(resp.body, b"hello");
}
#[test]
fn large_body_1mb() {
let payload: Vec<u8> = {
let mut v = Vec::with_capacity(1 << 20);
let mut state: u32 = 0x1234_5678;
for _ in 0..(1 << 20) {
state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
v.push((state >> 24) as u8);
}
v
};
let payload_clone = payload.clone();
let server = TestServer::start(move |_req: SReq| SResp::ok(payload_clone.clone()));
let resp = Request::get(&server.url("/")).unwrap().send().unwrap();
assert_eq!(resp.status, 200);
assert_eq!(resp.body.len(), payload.len());
assert!(resp.body == payload, "1 MiB body did not round-trip");
}
#[test]
fn status_204_no_body() {
let server = TestServer::start(|_req: SReq| SResp {
status: 204,
reason: "No Content".into(),
headers: vec![],
body: Vec::new(),
mode: BodyMode::CloseDelimited,
});
let resp = Request::get(&server.url("/")).unwrap().send().unwrap();
assert_eq!(resp.status, 204);
assert_eq!(resp.reason, "No Content");
assert!(resp.body.is_empty());
}
#[test]
fn request_headers_propagate() {
let server = TestServer::start(|req: SReq| {
let mut body = Vec::new();
for (k, v) in &req.headers {
body.extend_from_slice(k.as_bytes());
body.extend_from_slice(b": ");
body.extend_from_slice(v.as_bytes());
body.push(b'\n');
}
SResp::ok(body)
});
let resp = Request::get(&server.url("/probe")).unwrap().send().unwrap();
let text = String::from_utf8(resp.body).expect("ascii reflected headers");
let expected_ua = concat!("User-Agent: rsurl/", env!("CARGO_PKG_VERSION"));
assert!(text.contains(expected_ua), "missing default UA in: {text}");
assert!(text.contains("Accept: */*\n"), "missing Accept in: {text}");
let expected_host = format!("Host: {}\n", server.addr);
assert!(
text.contains(&expected_host),
"missing/wrong Host in: {text}",
);
assert!(
text.contains("Connection: close\n"),
"missing Connection: close in: {text}",
);
}
#[test]
fn custom_user_agent_overrides() {
let server = TestServer::start(|req: SReq| {
let ua = req.header("User-Agent").unwrap_or("").to_string();
SResp::ok(ua)
});
let resp = Request::get(&server.url("/"))
.unwrap()
.header("User-Agent", "test/1")
.send()
.unwrap();
assert_eq!(resp.body, b"test/1");
let default_ua = concat!("rsurl/", env!("CARGO_PKG_VERSION"));
assert!(
!resp
.body
.windows(default_ua.len())
.any(|w| w == default_ua.as_bytes()),
"default UA leaked alongside the override",
);
}
#[test]
fn post_with_body_sets_content_length() {
let server = TestServer::start(|req: SReq| {
assert_eq!(req.method, "POST");
assert_eq!(req.header("Content-Length"), Some("5"));
assert_eq!(req.body, b"hello");
SResp::ok("ack")
});
let resp = Request::new("POST", &server.url("/echo"))
.unwrap()
.body("hello".as_bytes().to_vec())
.send()
.unwrap();
assert_eq!(resp.body, b"ack");
}
#[test]
fn verbose_trace_format() {
let server = TestServer::start(|_req: SReq| SResp::ok("hi"));
let mut trace = Vec::new();
let resp = Request::get(&server.url("/"))
.unwrap()
.send_traced(&mut trace)
.unwrap();
assert_eq!(resp.body, b"hi");
let t = String::from_utf8(trace).expect("trace should be utf-8");
assert!(
t.contains("> GET / HTTP/1.1"),
"missing request line in:\n{t}"
);
assert!(
t.contains("< HTTP/1.1 200"),
"missing response status in:\n{t}",
);
assert!(
t.contains("* Connection closed"),
"missing close epilogue in:\n{t}",
);
}
#[test]
fn connect_refused_is_io_error() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind to grab a free port");
let addr = listener.local_addr().unwrap();
drop(listener);
let url = format!("http://{addr}/");
let err = Request::get(&url)
.unwrap()
.connect_timeout(Duration::from_secs(2))
.send()
.unwrap_err();
assert!(
matches!(err, Error::Io(_)),
"expected Error::Io, got {err:?}",
);
}