mod common;
use mini_static::Server;
use hyper::Method;
use std::fs;
use std::time::Duration;
use tempfile::TempDir;
#[tokio::test]
async fn multi_range_request_returns_200_with_full_body() {
let root = TempDir::new().unwrap();
let content = b"0123456789abcdefghij";
fs::write(root.path().join("file.txt"), content).unwrap();
let server = Server::new(root.path()).unwrap();
let response = server.handle_request_with_headers(
&Method::GET,
"/file.txt",
Some("bytes=0-5,10-15"),
None,
);
assert_eq!(response.status().as_u16(), 200);
let body = common::body_bytes(response).await;
assert_eq!(&body[..], content, "multi-range should return the full body, not a partial slice");
}
#[test]
fn multi_range_request_includes_etag() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("file.txt"), b"content").unwrap();
let server = Server::new(root.path()).unwrap();
let response = server.handle_request_with_headers(
&Method::GET,
"/file.txt",
Some("bytes=0-5,10-15"),
None,
);
assert!(response.headers().get("ETag").is_some());
}
#[test]
fn single_range_without_if_range_returns_200() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("file.txt"), b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let response = server.handle_request_with_headers(
&Method::GET,
"/file.txt",
Some("bytes=0-5"),
None,
);
assert_eq!(response.status().as_u16(), 200);
}
#[test]
fn single_range_with_matching_if_range_returns_200() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("file.txt"), b"content").unwrap();
let server = Server::new(root.path()).unwrap();
let first_response = server.handle_request_with_headers(
&Method::GET,
"/file.txt",
None,
None,
);
let etag = first_response
.headers()
.get("ETag")
.and_then(|v| v.to_str().ok())
.unwrap()
.to_string();
let second_response = server.handle_request_with_headers(
&Method::GET,
"/file.txt",
Some("bytes=0-5"),
Some(&etag),
);
assert_eq!(second_response.status().as_u16(), 200);
}
#[test]
fn single_range_with_stale_if_range_returns_200() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("file.txt"), b"original content").unwrap();
let server = Server::new(root.path()).unwrap();
let first_response = server.handle_request_with_headers(
&Method::GET,
"/file.txt",
None,
None,
);
let initial_etag = first_response
.headers()
.get("ETag")
.and_then(|v| v.to_str().ok())
.unwrap()
.to_string();
std::thread::sleep(Duration::from_millis(100));
fs::write(root.path().join("file.txt"), b"modified content now").unwrap();
let server = Server::new(root.path()).unwrap();
let response = server.handle_request_with_headers(
&Method::GET,
"/file.txt",
Some("bytes=0-5"),
Some(&initial_etag),
);
assert_eq!(response.status().as_u16(), 200);
}
#[test]
fn etag_header_present_on_all_200_responses() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("file.txt"), b"content").unwrap();
let server = Server::new(root.path()).unwrap();
let response1 = server.handle_request_with_method(&Method::GET, "/file.txt");
let response2 = server.handle_request_with_headers(
&Method::GET,
"/file.txt",
Some("bytes=0-5"),
None,
);
assert!(response1.headers().get("ETag").is_some());
assert!(response2.headers().get("ETag").is_some());
}
#[test]
fn etag_changes_when_file_modified() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("file.txt"), b"original content here").unwrap();
let server = Server::new(root.path()).unwrap();
let response1 = server.handle_request_with_method(&Method::GET, "/file.txt");
let etag1 = response1
.headers()
.get("ETag")
.and_then(|v| v.to_str().ok())
.unwrap()
.to_string();
std::thread::sleep(Duration::from_millis(100));
fs::write(root.path().join("file.txt"), b"x").unwrap();
let server = Server::new(root.path()).unwrap();
let response2 = server.handle_request_with_method(&Method::GET, "/file.txt");
let etag2 = response2
.headers()
.get("ETag")
.and_then(|v| v.to_str().ok())
.unwrap()
.to_string();
assert_ne!(etag1, etag2, "ETag should change when file is modified");
}
#[test]
fn multi_range_with_if_range_returns_200() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("file.txt"), b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let initial = server.handle_request_with_method(&Method::GET, "/file.txt");
let etag = initial
.headers()
.get("ETag")
.and_then(|v| v.to_str().ok())
.unwrap();
let response = server.handle_request_with_headers(
&Method::GET,
"/file.txt",
Some("bytes=0-5,10-15"),
Some(etag),
);
assert_eq!(response.status().as_u16(), 200);
}
#[tokio::test]
async fn head_method_with_multi_range_returns_200_empty_body() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("file.txt"), b"content").unwrap();
let server = Server::new(root.path()).unwrap();
let response = server.handle_request_with_headers(
&Method::HEAD,
"/file.txt",
Some("bytes=0-5,10-15"),
None,
);
assert_eq!(response.status().as_u16(), 200);
let body = common::body_bytes(response).await;
assert_eq!(body.len(), 0, "HEAD response must not have a body");
}
#[test]
fn etag_format_is_valid() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("file.txt"), b"content").unwrap();
let server = Server::new(root.path()).unwrap();
let response = server.handle_request_with_method(&Method::GET, "/file.txt");
let etag = response
.headers()
.get("ETag")
.and_then(|v| v.to_str().ok())
.unwrap();
assert!(etag.starts_with('"'));
assert!(etag.ends_with('"'));
assert!(etag.contains('-'));
}
#[test]
fn no_etag_on_404_response() {
let root = TempDir::new().unwrap();
let server = Server::new(root.path()).unwrap();
let response = server.handle_request_with_method(&Method::GET, "/missing.txt");
assert_eq!(response.status().as_u16(), 404);
assert!(response.headers().get("ETag").is_none());
}
#[test]
fn no_etag_on_405_response() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("file.txt"), b"content").unwrap();
let server = Server::new(root.path()).unwrap();
let response = server.handle_request_with_method(&Method::DELETE, "/file.txt");
assert_eq!(response.status().as_u16(), 405);
assert!(response.headers().get("ETag").is_none());
}