use std::borrow::Borrow;
use std::env;
use std::fs::File;
use std::io::Read;
use file_ext::FileExt;
use crate::header::Header;
use crate::http::VERSION;
use crate::mime_type::MimeType;
use crate::range::{ContentRange, Range};
use crate::request::{METHOD, Request};
use crate::response::{Error, Response, STATUS_CODE_REASON_PHRASE};
use crate::symbol::SYMBOL;
#[test]
fn check_is_multipart_byteranges_content_type() {
let content_type = Header {
name: Header::_CONTENT_TYPE.into(),
value: "multipart/byteranges; boundary=String_separator".into(),
};
let is_multipart = Response::_is_multipart_byteranges_content_type(&content_type);
assert_eq!(true, is_multipart);
for (value, expected) in [
("multipart/byteranges", true),
("multipart/form-data; boundary=x", false),
("multipart", false),
("text/multipart/byteranges", false),
("Multipart/byteranges", false), ("", false),
] {
let header = Header { name: Header::_CONTENT_TYPE.into(), value: value.into() };
assert_eq!(expected, Response::_is_multipart_byteranges_content_type(&header), "{value:?}");
}
}
#[test]
fn json_helper_builds_application_json_response() {
let body = br#"{"ok":true}"#.to_vec();
let response = Response::json(STATUS_CODE_REASON_PHRASE.n200_ok, body.clone());
assert_eq!(*STATUS_CODE_REASON_PHRASE.n200_ok.status_code, response.status_code);
assert_eq!(STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase, response.reason_phrase);
assert_eq!(1, response.content_range_list.len());
let content_range = response.content_range_list.get(0).unwrap();
assert_eq!(MimeType::APPLICATION_JSON, content_range.content_type);
assert_eq!(body, content_range.body);
}
#[test]
fn json_helper_with_error_status_and_empty_body() {
let response = Response::json(STATUS_CODE_REASON_PHRASE.n500_internal_server_error, vec![]);
assert_eq!(*STATUS_CODE_REASON_PHRASE.n500_internal_server_error.status_code, response.status_code);
let content_range = response.content_range_list.get(0).unwrap();
assert_eq!(MimeType::APPLICATION_JSON, content_range.content_type);
assert!(content_range.body.is_empty());
}
#[test]
fn text_helper_builds_text_plain_response() {
let response = Response::text(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid input");
assert_eq!(*STATUS_CODE_REASON_PHRASE.n400_bad_request.status_code, response.status_code);
assert_eq!(STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase, response.reason_phrase);
assert_eq!(1, response.content_range_list.len());
let content_range = response.content_range_list.get(0).unwrap();
assert_eq!(MimeType::TEXT_PLAIN, content_range.content_type);
assert_eq!("invalid input".as_bytes().to_vec(), content_range.body);
}
#[test]
fn text_helper_with_empty_body() {
let response = Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, "");
let content_range = response.content_range_list.get(0).unwrap();
assert_eq!(MimeType::TEXT_PLAIN, content_range.content_type);
assert!(content_range.body.is_empty());
}
#[test]
fn error() {
let error = Error {
status_code_reason_phrase: STATUS_CODE_REASON_PHRASE.n200_ok,
message: "some msg".to_string()
};
let clone = error.clone();
assert_eq!(error, clone);
assert_eq!("some msg", error.message);
assert_eq!(STATUS_CODE_REASON_PHRASE.n200_ok, error.status_code_reason_phrase);
}
#[test]
fn http_version_and_status_code_and_reason_phrase_404_regex() {
let http_version_and_status_code_and_reason_phrase = "HTTP/1.1 404 Not Found";
let boxed_parse = Response::_parse_http_version_status_code_reason_phrase_string(http_version_and_status_code_and_reason_phrase);
assert!(boxed_parse.is_ok());
let (http_version, status_code, reason_phrase) = boxed_parse.unwrap();
assert_eq!(VERSION.http_1_1, http_version);
assert_eq!(STATUS_CODE_REASON_PHRASE.n404_not_found.status_code, &status_code);
assert_eq!(STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase, reason_phrase);
}
#[test]
fn http_version_and_status_code_and_reason_phrase_200_regex() {
let http_version_and_status_code_and_reason_phrase = "HTTP/1.1 200 OK";
let boxed_parse = Response::_parse_http_version_status_code_reason_phrase_string(http_version_and_status_code_and_reason_phrase);
assert!(boxed_parse.is_ok());
let (http_version, status_code, reason_phrase) = boxed_parse.unwrap();
assert_eq!(VERSION.http_1_1, http_version);
assert_eq!(STATUS_CODE_REASON_PHRASE.n200_ok.status_code, &status_code);
assert_eq!(STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase, reason_phrase);
}
#[test]
fn http_version_and_status_code_and_reason_phrase_200_regex_random_case() {
let http_version_and_status_code_and_reason_phrase = "hTTp/1.1 200 Ok";
let boxed_parse = Response::_parse_http_version_status_code_reason_phrase_string(http_version_and_status_code_and_reason_phrase);
assert!(boxed_parse.is_ok());
let (http_version, status_code, reason_phrase) = boxed_parse.unwrap();
assert_eq!(VERSION.http_1_1.to_uppercase(), http_version.to_uppercase());
assert_eq!(STATUS_CODE_REASON_PHRASE.n200_ok.status_code, &status_code);
assert_eq!(STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_uppercase(), reason_phrase.to_uppercase());
}
#[test]
fn http_version_and_status_code_and_reason_phrase_empty_string() {
let http_version_and_status_code_and_reason_phrase = "";
let boxed_parse = Response::_parse_http_version_status_code_reason_phrase_string(http_version_and_status_code_and_reason_phrase);
assert!(!boxed_parse.is_ok());
let error_msg = boxed_parse.err().unwrap();
assert_eq!("Unable to parse status code", error_msg);
}
#[test]
fn http_version_and_status_code_and_reason_phrase_empty_string_newline() {
let http_version_and_status_code_and_reason_phrase = "hTTp/1.1 200 Ok\r\n";
let boxed_parse = Response::_parse_http_version_status_code_reason_phrase_string(http_version_and_status_code_and_reason_phrase);
assert!(boxed_parse.is_ok());
let (http_version, status_code, reason_phrase) = boxed_parse.unwrap();
assert_eq!(VERSION.http_1_1, http_version.to_uppercase());
assert_eq!(STATUS_CODE_REASON_PHRASE.n200_ok.status_code, &status_code);
assert_eq!(STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase, reason_phrase.to_uppercase());
}
#[test]
fn it_generates_successful_response_with_additional_headers() {
let response_http_version = VERSION.http_1_1.to_string();
let response_status_code = 401;
let response_reason_phrase = "Unauthorized";
let message_body = SYMBOL.empty_string;
let content_range = ContentRange {
unit: Range::BYTES.to_string(),
range: Range {
start: 0,
end: message_body.as_bytes().len() as u64
},
size: message_body.as_bytes().len().to_string(),
body: message_body.as_bytes().to_vec(),
content_type: MimeType::TEXT_PLAIN.to_string()
};
let headers = vec![];
let response = Response {
http_version: response_http_version.to_string(),
status_code: response_status_code,
reason_phrase: response_reason_phrase.to_string(),
headers,
content_range_list: vec![content_range],
stream_file: None,
stream_pipe: None,
trailers: None,
};
let response_content_length_header_name = "Content-Length";
let response_content_length_header_value = message_body.len().to_string();
let request = Request {
method: METHOD.get.to_string(),
request_uri: "/some-route".to_string(),
http_version: VERSION.http_1_1.to_string(),
headers: vec![],
body: Default::default(),
};
let raw_response = Response::generate_response(response, request);
let response = Response::_parse_response(raw_response.borrow());
let content_length_header = response._get_header(response_content_length_header_name.to_string()).unwrap();
assert_eq!(response_content_length_header_value, content_length_header.value);
assert_eq!(response_http_version, response.http_version);
assert_eq!(response_status_code, response.status_code);
assert_eq!(response_reason_phrase, response.reason_phrase);
assert_eq!(message_body.as_bytes().to_vec(), response.content_range_list.get(0).unwrap().body);
let response_clone = response.clone();
assert_eq!(response, response_clone);
}
#[test]
fn it_generates_successful_response_with_additional_headers_and_non_utf8_file() {
let response_http_version = VERSION.http_1_1.to_string();
let response_status_code = STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
let response_reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase;
let filepath = "/static/content.png";
let dir = env::current_dir().unwrap();
let working_directory = dir.as_path().to_str().unwrap();
let mut response_filepath = [working_directory, filepath].join(SYMBOL.empty_string);
let mut contents = Vec::new();
let mut file = File::open(response_filepath).unwrap();
file.read_to_end(&mut contents).expect("Unable to read");
let response_content_length_header_name = "Content-Length";
let response_content_length_header_value = contents.len().to_string();
let headers = vec![];
let content_range = ContentRange {
unit: Range::BYTES.to_string(),
range: Range {
start: 0,
end: contents.len() as u64
},
size: contents.len().to_string(),
body: contents.to_vec(),
content_type: MimeType::IMAGE_PNG.to_string()
};
let response = Response {
http_version: response_http_version.to_string(),
status_code: *response_status_code,
reason_phrase: response_reason_phrase.to_string(),
headers,
content_range_list: vec![content_range],
stream_file: None,
stream_pipe: None,
trailers: None,
};
let request = Request {
method: METHOD.get.to_string(),
request_uri: "/some-route".to_string(),
http_version: VERSION.http_1_1.to_string(),
headers: vec![],
body: Default::default(),
};
let raw_response = Response::generate_response(response, request);
let response = Response::_parse_response(raw_response.borrow());
let content_length_header = response._get_header(response_content_length_header_name.to_string()).unwrap();
assert_eq!(response_content_length_header_value, content_length_header.value);
assert_eq!(response_http_version, response.http_version);
assert_eq!(*response_status_code, response.status_code);
assert_eq!(response_reason_phrase, response.reason_phrase);
contents = Vec::new();
response_filepath = [working_directory, filepath].join(SYMBOL.empty_string);
file = File::open(response_filepath).unwrap();
file.read_to_end(&mut contents).expect("Unable to read");
assert_eq!(contents, response.content_range_list.get(0).unwrap().body);
}
#[test]
fn status_code_reason_phrase() {
assert_eq!(STATUS_CODE_REASON_PHRASE.n500_internal_server_error.status_code, &500);
assert_eq!(STATUS_CODE_REASON_PHRASE.n500_internal_server_error.reason_phrase, "Internal Server Error");
assert_eq!(STATUS_CODE_REASON_PHRASE.n501_not_implemented.status_code, &501);
assert_eq!(STATUS_CODE_REASON_PHRASE.n501_not_implemented.reason_phrase, "Not Implemented");
assert_eq!(STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code, &502);
assert_eq!(STATUS_CODE_REASON_PHRASE.n502_bad_gateway.reason_phrase, "Bad Gateway");
assert_eq!(STATUS_CODE_REASON_PHRASE.n503_service_unavailable.status_code, &503);
assert_eq!(STATUS_CODE_REASON_PHRASE.n503_service_unavailable.reason_phrase, "Service Unavailable");
assert_eq!(STATUS_CODE_REASON_PHRASE.n504_gateway_timeout.status_code, &504);
assert_eq!(STATUS_CODE_REASON_PHRASE.n504_gateway_timeout.reason_phrase, "Gateway Timeout");
assert_eq!(STATUS_CODE_REASON_PHRASE.n505_http_version_not_supported.status_code, &505);
assert_eq!(STATUS_CODE_REASON_PHRASE.n505_http_version_not_supported.reason_phrase, "HTTP Version Not Supported");
assert_eq!(STATUS_CODE_REASON_PHRASE.n506_variant_also_negotiates.status_code, &506);
assert_eq!(STATUS_CODE_REASON_PHRASE.n506_variant_also_negotiates.reason_phrase, "Variant Also Negotiates");
assert_eq!(STATUS_CODE_REASON_PHRASE.n507_insufficient_storage.status_code, &507);
assert_eq!(STATUS_CODE_REASON_PHRASE.n507_insufficient_storage.reason_phrase, "Insufficient Storage");
assert_eq!(STATUS_CODE_REASON_PHRASE.n508_loop_detected.status_code, &508);
assert_eq!(STATUS_CODE_REASON_PHRASE.n508_loop_detected.reason_phrase, "Loop Detected");
assert_eq!(STATUS_CODE_REASON_PHRASE.n510_not_extended.status_code, &510);
assert_eq!(STATUS_CODE_REASON_PHRASE.n510_not_extended.reason_phrase, "Not Extended");
assert_eq!(STATUS_CODE_REASON_PHRASE.n511_network_authentication_required.status_code, &511);
assert_eq!(STATUS_CODE_REASON_PHRASE.n511_network_authentication_required.reason_phrase, "Network Authentication Required");
assert_eq!(STATUS_CODE_REASON_PHRASE.n200_ok.status_code, &200);
assert_eq!(STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase, "OK");
assert_eq!(STATUS_CODE_REASON_PHRASE.n201_created.status_code, &201);
assert_eq!(STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase, "Created");
assert_eq!(STATUS_CODE_REASON_PHRASE.n202_accepted.status_code, &202);
assert_eq!(STATUS_CODE_REASON_PHRASE.n202_accepted.reason_phrase, "Accepted");
assert_eq!(STATUS_CODE_REASON_PHRASE.n203_non_authoritative_information.status_code, &203);
assert_eq!(STATUS_CODE_REASON_PHRASE.n203_non_authoritative_information.reason_phrase, "Non Authoritative Information");
assert_eq!(STATUS_CODE_REASON_PHRASE.n204_no_content.status_code, &204);
assert_eq!(STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase, "No Content");
assert_eq!(STATUS_CODE_REASON_PHRASE.n205_reset_content.status_code, &205);
assert_eq!(STATUS_CODE_REASON_PHRASE.n205_reset_content.reason_phrase, "Reset Content");
assert_eq!(STATUS_CODE_REASON_PHRASE.n206_partial_content.status_code, &206);
assert_eq!(STATUS_CODE_REASON_PHRASE.n206_partial_content.reason_phrase, "Partial Content");
assert_eq!(STATUS_CODE_REASON_PHRASE.n207_multi_status.status_code, &207);
assert_eq!(STATUS_CODE_REASON_PHRASE.n207_multi_status.reason_phrase, "Multi-Status");
assert_eq!(STATUS_CODE_REASON_PHRASE.n208_already_reported.status_code, &208);
assert_eq!(STATUS_CODE_REASON_PHRASE.n208_already_reported.reason_phrase, "Already Reported");
assert_eq!(STATUS_CODE_REASON_PHRASE.n226_im_used.status_code, &226);
assert_eq!(STATUS_CODE_REASON_PHRASE.n226_im_used.reason_phrase, "IM Used");
assert_eq!(STATUS_CODE_REASON_PHRASE.n100_continue.status_code, &100);
assert_eq!(STATUS_CODE_REASON_PHRASE.n100_continue.reason_phrase, "Continue");
assert_eq!(STATUS_CODE_REASON_PHRASE.n101_switching_protocols.status_code, &101);
assert_eq!(STATUS_CODE_REASON_PHRASE.n101_switching_protocols.reason_phrase, "Switching Protocols");
assert_eq!(STATUS_CODE_REASON_PHRASE.n102_processing.status_code, &102);
assert_eq!(STATUS_CODE_REASON_PHRASE.n102_processing.reason_phrase, "Processing");
assert_eq!(STATUS_CODE_REASON_PHRASE.n103_early_hints.status_code, &103);
assert_eq!(STATUS_CODE_REASON_PHRASE.n103_early_hints.reason_phrase, "Early Hints");
assert_eq!(STATUS_CODE_REASON_PHRASE.n300_multiple_choices.status_code, &300);
assert_eq!(STATUS_CODE_REASON_PHRASE.n300_multiple_choices.reason_phrase, "Multiple Choices");
assert_eq!(STATUS_CODE_REASON_PHRASE.n301_moved_permanently.status_code, &301);
assert_eq!(STATUS_CODE_REASON_PHRASE.n301_moved_permanently.reason_phrase, "Moved Permanently");
assert_eq!(STATUS_CODE_REASON_PHRASE.n302_found.status_code, &302);
assert_eq!(STATUS_CODE_REASON_PHRASE.n302_found.reason_phrase, "Found");
assert_eq!(STATUS_CODE_REASON_PHRASE.n303_see_other.status_code, &303);
assert_eq!(STATUS_CODE_REASON_PHRASE.n303_see_other.reason_phrase, "See Other");
assert_eq!(STATUS_CODE_REASON_PHRASE.n304_not_modified.status_code, &304);
assert_eq!(STATUS_CODE_REASON_PHRASE.n304_not_modified.reason_phrase, "Not Modified");
assert_eq!(STATUS_CODE_REASON_PHRASE.n307_temporary_redirect.status_code, &307);
assert_eq!(STATUS_CODE_REASON_PHRASE.n307_temporary_redirect.reason_phrase, "Temporary Redirect");
assert_eq!(STATUS_CODE_REASON_PHRASE.n308_permanent_redirect.status_code, &308);
assert_eq!(STATUS_CODE_REASON_PHRASE.n308_permanent_redirect.reason_phrase, "Permanent Redirect");
assert_eq!(STATUS_CODE_REASON_PHRASE.n400_bad_request.status_code, &400);
assert_eq!(STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase, "Bad Request");
assert_eq!(STATUS_CODE_REASON_PHRASE.n401_unauthorized.status_code, &401);
assert_eq!(STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase, "Unauthorized");
assert_eq!(STATUS_CODE_REASON_PHRASE.n402_payment_required.status_code, &402);
assert_eq!(STATUS_CODE_REASON_PHRASE.n402_payment_required.reason_phrase, "Payment Required");
assert_eq!(STATUS_CODE_REASON_PHRASE.n403_forbidden.status_code, &403);
assert_eq!(STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase, "Forbidden");
assert_eq!(STATUS_CODE_REASON_PHRASE.n404_not_found.status_code, &404);
assert_eq!(STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase, "Not Found");
assert_eq!(STATUS_CODE_REASON_PHRASE.n405_method_not_allowed.status_code, &405);
assert_eq!(STATUS_CODE_REASON_PHRASE.n405_method_not_allowed.reason_phrase, "Method Not Allowed");
assert_eq!(STATUS_CODE_REASON_PHRASE.n406_not_acceptable.status_code, &406);
assert_eq!(STATUS_CODE_REASON_PHRASE.n406_not_acceptable.reason_phrase, "Not Acceptable");
assert_eq!(STATUS_CODE_REASON_PHRASE.n407_proxy_authentication_required.status_code, &407);
assert_eq!(STATUS_CODE_REASON_PHRASE.n407_proxy_authentication_required.reason_phrase, "Proxy Authentication Required");
assert_eq!(STATUS_CODE_REASON_PHRASE.n408_request_timeout.status_code, &408);
assert_eq!(STATUS_CODE_REASON_PHRASE.n408_request_timeout.reason_phrase, "Request Timeout");
assert_eq!(STATUS_CODE_REASON_PHRASE.n409_conflict.status_code, &409);
assert_eq!(STATUS_CODE_REASON_PHRASE.n409_conflict.reason_phrase, "Conflict");
assert_eq!(STATUS_CODE_REASON_PHRASE.n410_gone.status_code, &410);
assert_eq!(STATUS_CODE_REASON_PHRASE.n410_gone.reason_phrase, "Gone");
assert_eq!(STATUS_CODE_REASON_PHRASE.n411_length_required.status_code, &411);
assert_eq!(STATUS_CODE_REASON_PHRASE.n411_length_required.reason_phrase, "Length Required");
assert_eq!(STATUS_CODE_REASON_PHRASE.n412_precondition_failed.status_code, &412);
assert_eq!(STATUS_CODE_REASON_PHRASE.n412_precondition_failed.reason_phrase, "Precondition Failed");
assert_eq!(STATUS_CODE_REASON_PHRASE.n413_payload_too_large.status_code, &413);
assert_eq!(STATUS_CODE_REASON_PHRASE.n413_payload_too_large.reason_phrase, "Payload Too Large");
assert_eq!(STATUS_CODE_REASON_PHRASE.n414_uri_too_long.status_code, &414);
assert_eq!(STATUS_CODE_REASON_PHRASE.n414_uri_too_long.reason_phrase, "URI Too Long");
assert_eq!(STATUS_CODE_REASON_PHRASE.n415_unsupported_media_type.status_code, &415);
assert_eq!(STATUS_CODE_REASON_PHRASE.n415_unsupported_media_type.reason_phrase, "Unsupported Media Type");
assert_eq!(STATUS_CODE_REASON_PHRASE.n416_range_not_satisfiable.status_code, &416);
assert_eq!(STATUS_CODE_REASON_PHRASE.n416_range_not_satisfiable.reason_phrase, "Range Not Satisfiable");
assert_eq!(STATUS_CODE_REASON_PHRASE.n417_expectation_failed.status_code, &417);
assert_eq!(STATUS_CODE_REASON_PHRASE.n417_expectation_failed.reason_phrase, "Expectation Failed");
assert_eq!(STATUS_CODE_REASON_PHRASE.n418_im_a_teapot.status_code, &418);
assert_eq!(STATUS_CODE_REASON_PHRASE.n418_im_a_teapot.reason_phrase, "I'm A Teapot");
assert_eq!(STATUS_CODE_REASON_PHRASE.n421_misdirected_request.status_code, &421);
assert_eq!(STATUS_CODE_REASON_PHRASE.n421_misdirected_request.reason_phrase, "Misdirected Request");
assert_eq!(STATUS_CODE_REASON_PHRASE.n422_unprocessable_entity.status_code, &422);
assert_eq!(STATUS_CODE_REASON_PHRASE.n422_unprocessable_entity.reason_phrase, "Unprocessable Entity");
assert_eq!(STATUS_CODE_REASON_PHRASE.n423_locked.status_code, &423);
assert_eq!(STATUS_CODE_REASON_PHRASE.n423_locked.reason_phrase, "Locked");
assert_eq!(STATUS_CODE_REASON_PHRASE.n424_failed_dependency.status_code, &424);
assert_eq!(STATUS_CODE_REASON_PHRASE.n424_failed_dependency.reason_phrase, "Failed Dependency");
assert_eq!(STATUS_CODE_REASON_PHRASE.n425_too_early.status_code, &425);
assert_eq!(STATUS_CODE_REASON_PHRASE.n425_too_early.reason_phrase, "Too Early");
assert_eq!(STATUS_CODE_REASON_PHRASE.n426_upgrade_required.status_code, &426);
assert_eq!(STATUS_CODE_REASON_PHRASE.n426_upgrade_required.reason_phrase, "Upgrade Required");
assert_eq!(STATUS_CODE_REASON_PHRASE.n428_precondition_required.status_code, &428);
assert_eq!(STATUS_CODE_REASON_PHRASE.n428_precondition_required.reason_phrase, "Precondition Required");
assert_eq!(STATUS_CODE_REASON_PHRASE.n429_too_many_requests.status_code, &429);
assert_eq!(STATUS_CODE_REASON_PHRASE.n429_too_many_requests.reason_phrase, "Too Many Requests");
assert_eq!(STATUS_CODE_REASON_PHRASE.n431_request_header_fields_too_large.status_code, &431);
assert_eq!(STATUS_CODE_REASON_PHRASE.n431_request_header_fields_too_large.reason_phrase, "Request Header Fields Too Large");
assert_eq!(STATUS_CODE_REASON_PHRASE.n451_unavailable_for_legal_reasons.status_code, &451);
assert_eq!(STATUS_CODE_REASON_PHRASE.n451_unavailable_for_legal_reasons.reason_phrase, "Unavailable For Legal Reasons");
assert_eq!(STATUS_CODE_REASON_PHRASE, STATUS_CODE_REASON_PHRASE.clone());
}
#[test]
fn parse_no_boundary_at_the_end() {
let path = FileExt::build_path(&["src", "response", "response.multipart.no_boundary_at_the_end.txt"]);
let pwd = FileExt::working_directory().unwrap();
let absolute_file_path = FileExt::build_path(&[pwd.as_str(), path.as_str()]);
let response_raw_bytes : Vec<u8> = FileExt::read_file(absolute_file_path.as_str()).unwrap();
let response_parse : Result<Response, String> = Response::parse(response_raw_bytes.as_ref());
if response_parse.is_err() {
let message = response_parse.clone().err().unwrap();
assert_eq!("Unable to parse multipart form body, reached the end of stream and it does not contain boundary", message);
}
}
#[test]
fn parse_no_boundary_at_the_beginning() {
let path = FileExt::build_path(&["src", "response", "response.multipart.no_boundary_at_the_beginning.txt"]);
let pwd = FileExt::working_directory().unwrap();
let absolute_file_path = FileExt::build_path(&[pwd.as_str(), path.as_str()]);
let response_raw_bytes : Vec<u8> = FileExt::read_file(absolute_file_path.as_str()).unwrap();
let response_parse : Result<Response, String> = Response::parse(response_raw_bytes.as_ref());
if response_parse.is_err() {
let message = response_parse.clone().err().unwrap();
assert_eq!("Response body doesn't start with a boundary", message);
}
}
#[test]
fn status_line_accepts_any_reason_phrase_and_any_three_digit_code() {
let parse = crate::response::Response::_parse_http_version_status_code_reason_phrase_string;
assert_eq!(Ok(("HTTP/1.0".to_string(), 404, "File not found".to_string())), parse("HTTP/1.0 404 File not found\r\n"));
assert_eq!(Ok(("HTTP/1.1".to_string(), 200, "".to_string())), parse("HTTP/1.1 200\r\n"), "reason phrase is optional");
assert_eq!(Ok(("HTTP/1.1".to_string(), 418, "I'm a teapot".to_string())), parse("HTTP/1.1 418 I'm a teapot\r\n"));
assert_eq!(Ok(("HTTP/1.1".to_string(), 599, "Custom".to_string())), parse("HTTP/1.1 599 Custom\r\n"));
for bad in ["HTTP/1.1 20 OK\r\n", "HTTP/1.1 2000 OK\r\n", "HTTP/1.1 abc OK\r\n", "HTTP/1.1 099 Low\r\n", "HTTP/9.9 200 OK\r\n"] {
assert!(parse(bad).is_err(), "{bad:?} should be rejected");
}
}
#[test]
fn invalid_upstream_content_length_is_an_error_not_a_panic() {
let raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\rD\r\n\r\nhello";
assert!(crate::response::Response::parse(raw).is_err());
}
#[test]
fn multipart_part_header_without_separator_does_not_panic() {
let raw = b"HTTP/1.1 206 Partial Content\r\nContent-Type: multipart/byteranges; boundary=XX\r\n\r\n--XX\r\nnot-a-header\r\n\r\nab\r\n--XX--\r\n";
let _ = crate::response::Response::parse(raw);
let header = crate::response::Response::_parse_http_response_header_string("no-separator\r\n");
assert_eq!("no-separator", header.name.trim_end());
}
#[test]
fn generate_response_parts_splits_head_and_body() {
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
let request = |m: &str| crate::request::Request { method: m.to_string(), request_uri: "/".to_string(), http_version: "HTTP/1.1".to_string(), headers: vec![], body: Default::default() };
let (head, body) = Response::generate_response_parts(Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, "hello"), request("GET"));
let head = String::from_utf8(head).unwrap();
assert!(head.starts_with("HTTP/1.1 200 OK\r\n") && head.ends_with("\r\n\r\n"), "{head:?}");
assert!(head.contains("Content-Length: 5\r\n"));
assert_eq!(b"hello".to_vec(), body);
let (_, head_body) = Response::generate_response_parts(Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, "hello"), request("HEAD"));
assert!(head_body.is_empty(), "HEAD has no body");
let whole = Response::generate_response(Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, "hello"), request("GET"));
assert!(String::from_utf8(whole).unwrap().ends_with("\r\n\r\nhello"));
}
#[test]
fn generate_response_coalesced_appends_small_bodies_only() {
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
let request = |m: &str| crate::request::Request { method: m.to_string(), request_uri: "/".to_string(), http_version: "HTTP/1.1".to_string(), headers: vec![], body: Default::default() };
let response = || {
let mut r = Response::get_response(
STATUS_CODE_REASON_PHRASE.n200_ok,
Some(vec![Header { name: "X-A".into(), value: "1".into() }]),
Some(vec![Range::get_content_range(b"hello".to_vec(), "text/plain".to_string())]),
);
r.headers.push(Header { name: "X-B".into(), value: "2".into() });
r
};
let expected_head = "HTTP/1.1 200 OK\r\nX-A: 1\r\nX-B: 2\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\n";
let (head, body) = Response::generate_response_coalesced(response(), request("GET"), 5);
assert_eq!(format!("{expected_head}hello").into_bytes(), head);
assert!(body.is_empty());
assert_eq!(head.len(), head.capacity(), "sized for head and body up front");
let (head, body) = Response::generate_response_coalesced(response(), request("GET"), 4);
assert_eq!(expected_head.as_bytes(), &head[..]);
assert_eq!(b"hello".to_vec(), body);
let (head, body) = Response::generate_response_parts(response(), request("GET"));
assert_eq!(expected_head.as_bytes(), &head[..]);
assert_eq!(b"hello".to_vec(), body);
let (head, body) = Response::generate_response_coalesced(response(), request("HEAD"), usize::MAX);
assert_eq!(expected_head.as_bytes(), &head[..]);
assert!(body.is_empty());
let (head, _) = Response::generate_response_coalesced(response(), request("OPTIONS"), usize::MAX);
assert_eq!(format!("{expected_head}hello").into_bytes(), head);
assert_eq!(format!("{expected_head}hello").into_bytes(), Response::generate_response(response(), request("GET")));
}
#[test]
fn generate_response_parts_partial_and_multipart_headers() {
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
let request = crate::request::Request { method: "GET".to_string(), request_uri: "/".to_string(), http_version: "HTTP/1.1".to_string(), headers: vec![], body: Default::default() };
let part = |start: u64, end: u64, body: &[u8]| ContentRange {
unit: Range::BYTES.to_string(),
range: Range { start, end },
size: "10".to_string(),
body: body.to_vec(),
content_type: "text/plain".to_string(),
};
let single = Response::get_response(STATUS_CODE_REASON_PHRASE.n206_partial_content, None, Some(vec![part(2, 4, b"cde")]));
let (head, body) = Response::generate_response_parts(single, request.clone());
assert_eq!(
"HTTP/1.1 206 Partial Content\r\nContent-Type: text/plain\r\nContent-Range: bytes 2-4/10\r\nContent-Length: 3\r\n\r\n",
String::from_utf8(head).unwrap()
);
assert_eq!(b"cde".to_vec(), body);
let multi = Response::get_response(STATUS_CODE_REASON_PHRASE.n206_partial_content, None, Some(vec![part(0, 0, b"a"), part(9, 9, b"j")]));
let boundary = Range::multipart_boundary(&multi.content_range_list);
let (head, body) = Response::generate_response_parts(multi, request.clone());
assert_eq!(
format!("HTTP/1.1 206 Partial Content\r\nContent-Type: multipart/byteranges; boundary={boundary}\r\nContent-Length: {}\r\n\r\n", body.len()),
String::from_utf8(head).unwrap()
);
assert!(String::from_utf8_lossy(&body).contains("Content-Range: bytes 0-0/10"));
let mut odd = Response::get_response(STATUS_CODE_REASON_PHRASE.n200_ok, None, None);
odd.status_code = 599;
odd.reason_phrase = "Custom".to_string();
let (head, body) = Response::generate_response_parts(odd, request.clone());
assert_eq!("HTTP/1.1 599 Custom\r\nContent-Length: 0\r\n\r\n", String::from_utf8(head).unwrap());
assert!(body.is_empty());
let mut negative = Response::get_response(STATUS_CODE_REASON_PHRASE.n200_ok, None, None);
negative.status_code = -1;
let (head, _) = Response::generate_response_parts(negative, request);
assert!(String::from_utf8(head).unwrap().starts_with("HTTP/1.1 -1 OK\r\n"));
}
#[test]
fn set_status_and_empty() {
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
let mut response = Response::get_response(STATUS_CODE_REASON_PHRASE.n501_not_implemented, None, None);
response.set_status(STATUS_CODE_REASON_PHRASE.n304_not_modified);
assert_eq!(304, response.status_code);
assert_eq!("Not Modified", response.reason_phrase);
let empty = Response::empty();
assert_eq!(0, empty.http_version.capacity() + empty.reason_phrase.capacity() + empty.headers.capacity());
}
#[test]
fn response_parse_matches_the_reference_parser() {
fn summary(result: &Result<crate::response::Response, String>) -> String {
match result {
Err(e) => format!("Err({e})"),
Ok(r) => {
let mut s = format!("{:?} {} {:?}\n", r.http_version, r.status_code, r.reason_phrase);
for h in &r.headers {
s.push_str(&format!("{:?}={:?}\n", h.name, h.value));
}
for cr in &r.content_range_list {
s.push_str(&format!("[{} {}-{} {} {:?} {:?}]\n", cr.unit, cr.range.start, cr.range.end, cr.size, cr.content_type, cr.body));
}
s
}
}
}
let corpus: Vec<&[u8]> = vec![
b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhello",
b"HTTP/1.1 404 File not found\r\nServer: x\r\n\r\n",
b"HTTP/1.1 204\r\n\r\n",
b"HTTP/1.0 599 \r\nX: two spaces \r\n\r\nbody\r\nmore",
b"HTTP/1.1 200 OK\nLF-Only: yes\n\nbody",
b"HTTP/1.1 200 OK\r\nNo-Separator\r\n\r\n",
b"HTTP/1.1 200 OK\r\nName:\r\n\r\n",
b"HTTP/1.1 200 OK\r\nName: \r\n\r\n",
b"HTTP/1.1 200 OK\r\nA:b: c\r\nD:: e\r\n\r\n",
b"HTTP/1.1 200 OK\r\nX: mid\rcr\r\n\r\n",
b"HTTP/1.1 200 O\rK\r\n\r\n",
b"HTTP/1.1 200 OK\r\nContent-Length: nope\r\n\r\n",
b"HTTP/1.1 200 OK\r\ncontent-length: 7 \r\n\r\n1234567",
b"HTTP/1.1 200 OK\r\nX: y",
b"HTTP/1.1 200 OK",
b"HTTP/1.1 200 OK\r\n \t \r\nafter-blank",
b"HTTP/2 200 OK\r\n\r\n",
b"HTTP/1.1 20 OK\r\n\r\n",
b"HTTP/1.1 099 OK\r\n\r\n",
b"HTTP/1.1 -10 OK\r\n\r\n",
b"HTTP/1.1 200 OK\r\n\r\n",
b"HTTP/9.9 200 OK\r\n\r\n",
b"\r\n\r\n",
b"",
b"HTTP/1.1 200 OK\r\nX: \xff\xfe\r\n\r\n",
b"HTTP/1.1 200 OK\r\nX: \xc3\xa9t\xc3\xa9\r\n\r\n\xff\x00binary",
b"HTTP/1.1 206 Partial Content\r\nContent-Type: multipart/byteranges; boundary=XX\r\n\r\n--XX\r\nContent-Type: text/plain\r\nContent-Range: bytes 0-1/10\r\n\r\nab\r\n--XX\r\nContent-Type: text/plain\r\nContent-Range: bytes 8-9/10\r\n\r\nij\r\n--XX--\r\n",
b"HTTP/1.1 206 Partial Content\r\nContent-Type: multipart/byteranges\r\n\r\n--XX\r\n",
b"HTTP/1.1 206 Partial Content\r\ncontent-type: multipart/byteranges; boundary=XX\r\n\r\n--XX\r\nnot-a-header\r\n\r\nab\r\n--XX--\r\n",
b"HTTP/1.1 200 OK\r\nContent-Type: a\r\ncontent-type: b\r\n\r\nx",
];
let mut seed: u64 = 0x9E37_79B9_7F4A_7C15;
let mut next = move || { seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17; seed };
let interesting = [b'\r', b'\n', b' ', b':', b'\t', 0xff, 0xc3, b'0', b'-'];
let mut checked = 0;
for input in &corpus {
let mut cases = vec![input.to_vec()];
for _ in 0..200 {
let mut m = input.to_vec();
for _ in 0..1 + next() % 3 {
let r = next();
match r % 3 {
0 if !m.is_empty() => { let i = (r >> 8) as usize % m.len(); m[i] = interesting[(r >> 32) as usize % interesting.len()]; }
1 => { let i = (r >> 8) as usize % (m.len() + 1); m.insert(i, interesting[(r >> 32) as usize % interesting.len()]); }
_ if !m.is_empty() => { let i = (r >> 8) as usize % m.len(); m.remove(i); }
_ => {}
}
}
cases.push(m);
}
for case in cases {
let ours = crate::response::Response::parse(&case);
let reference = super::reference_parser::parse(&case);
assert_eq!(summary(&reference), summary(&ours), "input {:?}", String::from_utf8_lossy(&case));
checked += 1;
}
}
assert!(checked > 5000);
}
#[test]
fn split_name_value_and_without_cr_lf() {
use crate::response::{split_name_value, without_cr_lf};
for line in ["a: b", "a:b: c", "a:", ": x", "a : b", "no", "", "a:: b", "é: ü"] {
assert_eq!(line.split_once(": "), split_name_value(line), "{line:?}");
}
for s in ["abc\r\n", "abc\n", "abc\r", "abc", "a\rb\r\n", "a\nb", "\r\n", "", "abc\r\r\n"] {
assert_eq!(s.replace('\r', "").replace('\n', ""), without_cr_lf(s), "{s:?}");
}
}
fn split_attempt() -> Response {
let mut response = Response::text(STATUS_CODE_REASON_PHRASE.n200_ok, "ok");
response.reason_phrase = "OK\r\nX-Injected-Status: 1".to_string();
response.headers.push(Header { name: "Location".into(), value: "/next\r\nSet-Cookie: session=evil\r\n\r\nHTTP/1.1 200 OK".into() });
response.headers.push(Header { name: "X-Name\r\nX-Injected-Name".into(), value: "v".into() });
response
}
fn head_lines(raw: &[u8]) -> Vec<String> {
let text = String::from_utf8_lossy(raw);
let head = text.split("\r\n\r\n").next().unwrap_or("").to_string();
head.split("\r\n").map(|l| l.to_string()).collect()
}
#[test]
fn header_values_cannot_split_the_response() {
let request = Request { method: METHOD.get.to_string(), request_uri: "/".to_string(), http_version: VERSION.http_1_1.to_string(), headers: vec![], body: Default::default() };
let (head, _) = Response::generate_response_parts(split_attempt(), request.clone());
let full = Response::generate_response(split_attempt(), request);
for raw in [head, full] {
let lines = head_lines(&raw);
assert!(!lines.iter().skip(1).any(|l| l.starts_with("Set-Cookie") || l.starts_with("X-Injected") || l.starts_with("HTTP/")), "{lines:?}");
assert_eq!(1, lines.iter().filter(|l| l.starts_with("HTTP/")).count(), "{lines:?}");
assert!(lines.iter().any(|l| l.starts_with("Location: /next Set-Cookie: session=evil")), "{lines:?}");
}
}
#[test]
fn single_line_borrows_clean_values() {
assert!(matches!(Header::single_line("plain"), std::borrow::Cow::Borrowed("plain")));
assert_eq!("a b c", Header::single_line("a\r\nb\rc"));
}
fn two_parts(first: &[u8], second: &[u8]) -> Response {
let part = |body: &[u8], start: u64| {
let mut cr = Range::get_content_range(body.to_vec(), MimeType::TEXT_PLAIN.to_string());
cr.range.start = start;
cr.range.end = start + body.len() as u64 - 1;
cr.size = "1000".to_string();
cr
};
let mut response = Response::get_response(STATUS_CODE_REASON_PHRASE.n206_partial_content, None, None);
response.content_range_list = vec![part(first, 0), part(second, 500)];
response
}
fn get_request() -> Request {
Request { method: METHOD.get.to_string(), request_uri: "/f".to_string(), http_version: VERSION.http_1_1.to_string(), headers: vec![], body: Default::default() }
}
#[test]
fn multipart_byteranges_has_a_length_and_a_closed_delimiter() {
let (head, body) = Response::generate_response_parts(two_parts(b"first", b"second"), get_request());
let head = String::from_utf8(head).unwrap();
let length = head.lines().find_map(|l| l.strip_prefix("Content-Length: ")).expect("no Content-Length");
assert_eq!(body.len().to_string(), length);
let boundary = head.lines().find_map(|l| l.strip_prefix("Content-Type: multipart/byteranges; boundary=")).unwrap();
let body = String::from_utf8(body).unwrap();
assert!(body.starts_with(&format!("--{boundary}\r\n")), "{body}");
assert!(body.ends_with(&format!("\r\n--{boundary}--\r\n")), "{body}");
assert_eq!(3, body.matches(boundary).count());
assert!(!head.contains("Transfer-Encoding"));
}
#[test]
fn the_boundary_never_occurs_in_a_part() {
let plain = two_parts(b"aaaa", b"bbbbbb");
let natural = Range::multipart_boundary(&plain.content_range_list);
let tricky = two_parts(format!("x{natural}x").as_bytes(), b"--String_separator\r\n");
let boundary = Range::multipart_boundary(&tricky.content_range_list);
assert_ne!(natural, boundary);
for cr in &tricky.content_range_list {
assert!(!cr.body.windows(boundary.len()).any(|w| w == boundary.as_bytes()));
}
let (head, body) = Response::generate_response_parts(tricky, get_request());
let head = String::from_utf8(head).unwrap();
assert!(head.contains(&format!("boundary={boundary}\r\n")));
assert!(String::from_utf8_lossy(&body).ends_with(&format!("--{boundary}--\r\n")));
}
#[test]
fn a_generated_multipart_response_parses_back() {
let raw = Response::generate_response(two_parts(b"first", b"second"), get_request());
let parsed = Response::parse(&raw).unwrap();
let bodies: Vec<&[u8]> = parsed.content_range_list.iter().map(|cr| &cr.body[..]).collect();
assert_eq!(vec![&b"first"[..], &b"second"[..]], bodies);
}
fn with_body(status: &'static crate::response::StatusCodeReasonPhrase) -> Response {
Response::get_response(status, None, Some(vec![Range::get_content_range(b"abc".to_vec(), MimeType::TEXT_PLAIN.to_string())]))
}
fn request_with(method: &str) -> Request {
Request { method: method.to_string(), request_uri: "/".to_string(), http_version: VERSION.http_1_1.to_string(), headers: vec![], body: Default::default() }
}
#[test]
fn no_content_statuses_send_no_body_or_content_length() {
for status in [STATUS_CODE_REASON_PHRASE.n204_no_content, STATUS_CODE_REASON_PHRASE.n304_not_modified, STATUS_CODE_REASON_PHRASE.n100_continue] {
for method in ["GET", "OPTIONS"] {
let mut response = with_body(status);
response.headers.push(Header { name: "Content-Length".into(), value: "3".into() });
let (head, body) = Response::generate_response_parts(response, request_with(method));
let head = String::from_utf8(head).unwrap();
assert!(body.is_empty(), "{} {method}", status.status_code);
assert!(!head.to_ascii_lowercase().contains("content-length"), "{head}");
assert!(!head.to_ascii_lowercase().contains("transfer-encoding"), "{head}");
}
}
}
#[test]
fn options_with_content_sends_it() {
let (head, body) = Response::generate_response_parts(with_body(STATUS_CODE_REASON_PHRASE.n200_ok), request_with("OPTIONS"));
assert!(String::from_utf8(head).unwrap().contains("Content-Length: 3\r\n"));
assert_eq!(b"abc".to_vec(), body);
}
#[test]
fn head_keeps_the_length_but_sends_no_body() {
let (head, body) = Response::generate_response_parts(with_body(STATUS_CODE_REASON_PHRASE.n200_ok), request_with("HEAD"));
assert!(String::from_utf8(head).unwrap().contains("Content-Length: 3\r\n"));
assert!(body.is_empty());
}
#[test]
fn drop_content_for_status_clears_streamed_bodies() {
let mut response = with_body(STATUS_CODE_REASON_PHRASE.n204_no_content);
response.stream_file = Some("/tmp/x".to_string());
response.stream_pipe = Some(Box::new(std::io::empty()));
response.headers.push(Header { name: "Transfer-Encoding".into(), value: "chunked".into() });
response.drop_content_for_status();
assert!(response.stream_file.is_none() && response.stream_pipe.is_none() && response.content_range_list.is_empty());
assert!(response.headers.iter().all(|h| !h.name.eq_ignore_ascii_case("transfer-encoding")));
let mut ok = with_body(STATUS_CODE_REASON_PHRASE.n200_ok);
ok.drop_content_for_status();
assert_eq!(1, ok.content_range_list.len());
}
#[test]
fn a_bodiless_response_says_its_length_is_zero() {
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
let request = crate::request::Request { method: "GET".to_string(), request_uri: "/".to_string(), http_version: "HTTP/1.1".to_string(), headers: vec![], body: Default::default() };
let head = |response: Response| String::from_utf8(Response::generate_response_parts(response, request.clone()).0).unwrap();
let empty = Response::get_response(STATUS_CODE_REASON_PHRASE.n200_ok, None, None);
assert!(head(empty).ends_with("Content-Length: 0\r\n\r\n"));
for status in [STATUS_CODE_REASON_PHRASE.n204_no_content, STATUS_CODE_REASON_PHRASE.n304_not_modified] {
assert!(!head(Response::get_response(status, None, None)).contains("Content-Length"));
}
let mut streamed = Response::get_response(STATUS_CODE_REASON_PHRASE.n200_ok, None, None);
streamed.stream_pipe = Some(Box::new(std::io::empty()));
assert!(!head(streamed).contains("Content-Length"));
let mut set = Response::get_response(STATUS_CODE_REASON_PHRASE.n200_ok, None, None);
set.headers.push(crate::header::Header::new("Content-Length", "0".to_string()));
assert_eq!(1, head(set).matches("Content-Length").count());
}