use rstest::rstest;
use super::*;
#[test]
fn test_is_http1_request() {
assert!(is_http1_request(b"GET / HTTP/1.1\r\n"));
assert!(is_http1_request(b"POST /api HTTP/1.1\r\n"));
assert!(is_http1_request(b"PUT /resource HTTP/1.1\r\n"));
assert!(is_http1_request(b"DELETE /item HTTP/1.1\r\n"));
assert!(is_http1_request(b"HEAD / HTTP/1.1\r\n"));
assert!(is_http1_request(b"OPTIONS * HTTP/1.1\r\n"));
assert!(is_http1_request(b"PATCH /update HTTP/1.1\r\n"));
assert!(is_http1_request(b"CONNECT host:443 HTTP/1.1\r\n"));
assert!(!is_http1_request(b"HTTP/1.1 200 OK\r\n"));
assert!(!is_http1_request(b"PRI * HTTP/2.0\r\n"));
}
#[test]
fn test_is_http1_response() {
assert!(is_http1_response(b"HTTP/1.1 200 OK\r\n"));
assert!(is_http1_response(b"HTTP/1.0 404 Not Found\r\n"));
assert!(!is_http1_response(b"GET / HTTP/1.1\r\n"));
assert!(!is_http1_response(b"HTTP/2 200 OK\r\n"));
}
#[test]
fn test_try_parse_request_incomplete_headers() {
let data = b"GET / HTTP/1.1\r\nHost: example.com\r\n";
assert!(
try_parse_http1_request(data, TimestampNs(0)).is_none(),
"Should return None for incomplete headers"
);
}
#[test]
fn test_try_parse_request_complete_no_body() {
let data = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
let result = try_parse_http1_request(data, TimestampNs(12345));
assert!(result.is_some(), "Should parse complete GET request");
let req = result.unwrap();
assert_eq!(req.method, Method::GET);
assert_eq!(req.timestamp_ns, TimestampNs(12345));
assert!(req.body.is_empty());
}
#[test]
fn test_try_parse_request_content_length_complete() {
let data = b"POST /api HTTP/1.1\r\nContent-Length: 5\r\n\r\nhello";
let result = try_parse_http1_request(data, TimestampNs(0));
assert!(result.is_some(), "Should parse complete POST with body");
let req = result.unwrap();
assert_eq!(req.method, Method::POST);
assert_eq!(req.body, b"hello");
}
#[test]
fn test_try_parse_request_content_length_incomplete() {
let data = b"POST /api HTTP/1.1\r\nContent-Length: 10\r\n\r\nhello";
assert!(
try_parse_http1_request(data, TimestampNs(0)).is_none(),
"Should return None when body is incomplete"
);
}
#[test]
fn test_try_parse_request_content_length_case_insensitive() {
let data = b"POST /api HTTP/1.1\r\ncontent-length: 5\r\n\r\nhello";
let result = try_parse_http1_request(data, TimestampNs(0));
assert!(
result.is_some(),
"Should handle case-insensitive Content-Length"
);
assert_eq!(result.unwrap().body, b"hello");
}
#[test]
fn test_try_parse_request_chunked_complete() {
let data = b"POST /api HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
let result = try_parse_http1_request(data, TimestampNs(0));
assert!(result.is_some(), "Should parse complete chunked request");
assert_eq!(
result.unwrap().body,
b"hello",
"Chunked body should be decoded"
);
}
#[test]
fn test_try_parse_request_chunked_incomplete() {
let data = b"POST /api HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n";
assert!(
try_parse_http1_request(data, TimestampNs(0)).is_none(),
"Should return None for incomplete chunked"
);
}
#[test]
fn test_try_parse_response_incomplete_headers() {
let data = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n";
assert!(
try_parse_http1_response(data, TimestampNs(0)).is_none(),
"Should return None for incomplete response headers"
);
}
#[test]
fn test_try_parse_response_complete_no_body() {
let data = b"HTTP/1.1 204 No Content\r\n\r\n";
let result = try_parse_http1_response(data, TimestampNs(67890));
assert!(result.is_some(), "Should parse complete 204 response");
let resp = result.unwrap();
assert_eq!(resp.status, StatusCode::NO_CONTENT);
assert_eq!(resp.timestamp_ns, TimestampNs(67890));
}
#[test]
fn test_try_parse_response_content_length_complete() {
let data = b"HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHello World";
let result = try_parse_http1_response(data, TimestampNs(0));
assert!(result.is_some(), "Should parse complete response with body");
let resp = result.unwrap();
assert_eq!(resp.status, StatusCode::OK);
assert_eq!(resp.body, b"Hello World");
}
#[test]
fn test_try_parse_response_content_length_incomplete() {
let data = b"HTTP/1.1 200 OK\r\nContent-Length: 20\r\n\r\nHello";
assert!(
try_parse_http1_response(data, TimestampNs(0)).is_none(),
"Should return None when response body is incomplete"
);
}
#[test]
fn test_try_parse_response_chunked_complete() {
let data = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
let result = try_parse_http1_response(data, TimestampNs(0));
assert!(result.is_some(), "Should parse complete chunked response");
}
#[test]
fn test_try_parse_response_chunked_incomplete() {
let data = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n";
assert!(
try_parse_http1_response(data, TimestampNs(0)).is_none(),
"Should return None for incomplete chunked response"
);
}
#[test]
fn test_try_parse_request_with_path_and_headers() {
let data = b"GET /path HTTP/1.1\r\nHost: example.com\r\n\r\n";
let request = try_parse_http1_request(data, TimestampNs(12345)).unwrap();
assert_eq!(request.method, Method::GET);
assert_eq!(request.uri.path(), "/path");
assert_eq!(
request.headers.get("host").unwrap().to_str().unwrap(),
"example.com"
);
assert!(request.body.is_empty());
assert_eq!(request.timestamp_ns, TimestampNs(12345));
}
#[test]
fn test_try_parse_response_with_content_type() {
let data =
b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 11\r\n\r\nHello World";
let response = try_parse_http1_response(data, TimestampNs(67890)).unwrap();
assert_eq!(response.status, StatusCode::OK);
assert_eq!(
response
.headers
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"text/plain"
);
assert_eq!(response.body, b"Hello World");
assert_eq!(response.timestamp_ns, TimestampNs(67890));
}
#[test]
fn test_try_parse_response_404_without_content_length_is_incomplete() {
let data = b"HTTP/1.1 404 Not Found\r\n\r\n";
assert!(
try_parse_http1_response(data, TimestampNs(0)).is_none(),
"404 without framing should be incomplete (read-until-close)"
);
}
#[test]
fn test_try_parse_response_404_with_content_length() {
let data = b"HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot Found";
let response = try_parse_http1_response(data, TimestampNs(0)).unwrap();
assert_eq!(response.status, StatusCode::NOT_FOUND);
assert_eq!(response.body, b"Not Found");
}
#[test]
fn test_get_request_with_trailing_data_body_empty() {
let data = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\nEXTRA";
let result = try_parse_http1_request(data, TimestampNs(0));
assert!(result.is_some());
let req = result.unwrap();
assert!(
req.body.is_empty(),
"GET body should be empty, not trailing data"
);
}
#[test]
fn test_post_content_length_ignores_trailing_data() {
let data = b"POST /api HTTP/1.1\r\nContent-Length: 5\r\n\r\nhelloEXTRA";
let result = try_parse_http1_request(data, TimestampNs(0));
assert!(result.is_some());
assert_eq!(
result.unwrap().body,
b"hello",
"Should truncate to Content-Length"
);
}
#[test]
fn test_chunked_body_decoded_correctly() {
let data = b"POST /api HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
let result = try_parse_http1_request(data, TimestampNs(0));
assert!(result.is_some());
assert_eq!(result.unwrap().body, b"hello");
}
#[test]
fn test_chunked_false_positive_0_in_content() {
let data = b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\nc\r\n0\r\n\r\nhello\r\n\r\n0\r\n\r\n";
let result = try_parse_http1_request(data, TimestampNs(0));
assert!(
result.is_some(),
"Should parse chunked body with embedded 0\\r\\n\\r\\n"
);
assert_eq!(result.unwrap().body, b"0\r\n\r\nhello\r\n");
}
#[test]
fn test_chunked_multi_chunk() {
let data =
b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nhel\r\n2\r\nlo\r\n0\r\n\r\n";
let result = try_parse_http1_request(data, TimestampNs(0));
assert!(result.is_some());
assert_eq!(result.unwrap().body, b"hello");
}
#[test]
fn test_chunked_with_extensions() {
let data =
b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5;ext=val\r\nhello\r\n0\r\n\r\n";
let result = try_parse_http1_request(data, TimestampNs(0));
assert!(result.is_some(), "Should handle chunk extensions");
assert_eq!(result.unwrap().body, b"hello");
}
#[test]
fn test_chunked_incomplete_missing_terminator() {
let data = b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n";
assert!(
try_parse_http1_request(data, TimestampNs(0)).is_none(),
"Should be None for incomplete chunked"
);
}
#[test]
fn test_chunked_response_decoded() {
let data = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n";
let result = try_parse_http1_response(data, TimestampNs(0));
assert!(result.is_some(), "Should parse complete chunked response");
assert_eq!(result.unwrap().body, b"hello");
}
#[test]
fn test_multi_valued_headers_preserved_in_request() {
let data = b"GET / HTTP/1.1\r\nHost: example.com\r\nCookie: a=1\r\nCookie: b=2\r\n\r\n";
let req = try_parse_http1_request(data, TimestampNs(0)).unwrap();
let cookies: Vec<_> = req.headers.get_all("cookie").iter().collect();
assert_eq!(cookies.len(), 2, "Both Cookie headers should be preserved");
}
#[test]
fn test_multi_valued_headers_preserved_in_response() {
let data =
b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nSet-Cookie: a=1\r\nSet-Cookie: b=2\r\n\r\n";
let resp = try_parse_http1_response(data, TimestampNs(0)).unwrap();
let cookies: Vec<_> = resp.headers.get_all("set-cookie").iter().collect();
assert_eq!(
cookies.len(),
2,
"Both Set-Cookie headers should be preserved"
);
}
#[test]
fn test_response_without_framing_is_incomplete() {
let data = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\npartial body";
assert!(
try_parse_http1_response(data, TimestampNs(0)).is_none(),
"Response without framing should be incomplete"
);
}
#[rstest]
#[case::status_204(204, "No Content")]
#[case::status_304(304, "Not Modified")]
fn test_no_body_status_without_framing_is_complete(#[case] status_code: u16, #[case] reason: &str) {
let data = format!("HTTP/1.1 {status_code} {reason}\r\n\r\n");
assert!(
try_parse_http1_response(data.as_bytes(), TimestampNs(0)).is_some(),
"{status_code} should be complete without framing"
);
}
#[test]
fn test_try_finalize_http1_response_takes_all_remaining_data() {
let data = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nfull body here";
let resp = try_finalize_http1_response(data, TimestampNs(12345)).unwrap();
assert_eq!(resp.status, StatusCode::OK);
assert_eq!(resp.body, b"full body here");
assert_eq!(resp.timestamp_ns, TimestampNs(12345));
}
#[test]
fn test_try_finalize_http1_response_empty_body() {
let data = b"HTTP/1.1 200 OK\r\n\r\n";
let resp = try_finalize_http1_response(data, TimestampNs(0)).unwrap();
assert!(resp.body.is_empty());
}
#[test]
fn test_try_finalize_incomplete_headers_returns_none() {
let data = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n";
assert!(try_finalize_http1_response(data, TimestampNs(0)).is_none());
}