use mini_static::Server;
use std::fs;
use std::time::Duration;
use tempfile::TempDir;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::test]
async fn range_simple_start_to_end() {
let root = TempDir::new().unwrap();
let file_path = root.path().join("data.bin");
fs::write(&file_path, b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, server_handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let addr = format!("127.0.0.1:{}", port);
let mut conn = tokio::net::TcpStream::connect(&addr).await.unwrap();
conn.write_all(b"GET /data.bin HTTP/1.1\r\nHost: localhost\r\nRange: bytes=0-4\r\n\r\n")
.await
.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("206 Partial Content"), "got: {response_str}");
assert!(response_str.contains("Content-Range: bytes 0-4/10"), "got: {response_str}");
assert!(response_str.contains("Content-Length: 5"), "got: {response_str}");
assert!(response_str.contains("01234"), "got: {response_str}");
server_handle.shutdown().await;
}
#[tokio::test]
async fn range_open_ended() {
let root = TempDir::new().unwrap();
let file_path = root.path().join("data.bin");
fs::write(&file_path, b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, server_handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let addr = format!("127.0.0.1:{}", port);
let mut conn = tokio::net::TcpStream::connect(&addr)
.await
.unwrap();
conn.write_all(b"GET /data.bin HTTP/1.1\r\nHost: localhost\r\nRange: bytes=5-\r\n\r\n")
.await
.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("206 Partial Content"), "got: {response_str}");
assert!(response_str.contains("Content-Range: bytes 5-9/10"), "got: {response_str}");
assert!(response_str.contains("Content-Length: 5"), "got: {response_str}");
assert!(response_str.contains("56789"), "got: {response_str}");
server_handle.shutdown().await;
}
#[tokio::test]
async fn range_suffix() {
let root = TempDir::new().unwrap();
let file_path = root.path().join("data.bin");
fs::write(&file_path, b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, server_handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let addr = format!("127.0.0.1:{}", port);
let mut conn = tokio::net::TcpStream::connect(&addr)
.await
.unwrap();
conn.write_all(b"GET /data.bin HTTP/1.1\r\nHost: localhost\r\nRange: bytes=-3\r\n\r\n")
.await
.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("206 Partial Content"), "got: {response_str}");
assert!(response_str.contains("Content-Range: bytes 7-9/10"), "got: {response_str}");
assert!(response_str.contains("Content-Length: 3"), "got: {response_str}");
assert!(response_str.contains("789"), "got: {response_str}");
server_handle.shutdown().await;
}
#[tokio::test]
async fn range_out_of_bounds_returns_416() {
let root = TempDir::new().unwrap();
let file_path = root.path().join("data.bin");
fs::write(&file_path, b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, server_handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let addr = format!("127.0.0.1:{}", port);
let mut conn = tokio::net::TcpStream::connect(&addr)
.await
.unwrap();
conn.write_all(b"GET /data.bin HTTP/1.1\r\nHost: localhost\r\nRange: bytes=100-200\r\n\r\n")
.await
.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("416 Range Not Satisfiable"), "got: {response_str}");
assert!(response_str.contains("Content-Range: bytes */10"), "got: {response_str}");
server_handle.shutdown().await;
}
#[tokio::test]
async fn range_clamped_end() {
let root = TempDir::new().unwrap();
let file_path = root.path().join("data.bin");
fs::write(&file_path, b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, server_handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let addr = format!("127.0.0.1:{}", port);
let mut conn = tokio::net::TcpStream::connect(&addr)
.await
.unwrap();
conn.write_all(b"GET /data.bin HTTP/1.1\r\nHost: localhost\r\nRange: bytes=5-100\r\n\r\n")
.await
.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("206 Partial Content"), "got: {response_str}");
assert!(response_str.contains("Content-Range: bytes 5-9/10"), "got: {response_str}");
assert!(response_str.contains("Content-Length: 5"), "got: {response_str}");
assert!(response_str.contains("56789"), "got: {response_str}");
server_handle.shutdown().await;
}
#[tokio::test]
async fn range_if_range_matching_etag() {
let root = TempDir::new().unwrap();
let file_path = root.path().join("data.bin");
fs::write(&file_path, b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, server_handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let addr = format!("127.0.0.1:{}", port);
let mut conn = tokio::net::TcpStream::connect(&addr)
.await
.unwrap();
conn.write_all(b"GET /data.bin HTTP/1.1\r\nHost: localhost\r\n\r\n")
.await
.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
let etag = response_str
.lines()
.find(|l| l.starts_with("ETag: "))
.unwrap()
.strip_prefix("ETag: ")
.unwrap()
.to_string();
let mut conn = tokio::net::TcpStream::connect(&addr)
.await
.unwrap();
let request = format!(
"GET /data.bin HTTP/1.1\r\nHost: localhost\r\nRange: bytes=0-4\r\nIf-Range: {}\r\n\r\n",
etag
);
conn.write_all(request.as_bytes()).await.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("206 Partial Content"), "got: {response_str}");
assert!(response_str.contains("Content-Range: bytes 0-4/10"), "got: {response_str}");
server_handle.shutdown().await;
}
#[tokio::test]
async fn range_if_range_mismatched_etag_returns_200() {
let root = TempDir::new().unwrap();
let file_path = root.path().join("data.bin");
fs::write(&file_path, b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, server_handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let addr = format!("127.0.0.1:{}", port);
let mut conn = tokio::net::TcpStream::connect(&addr)
.await
.unwrap();
conn.write_all(
b"GET /data.bin HTTP/1.1\r\nHost: localhost\r\nRange: bytes=0-4\r\nIf-Range: \"stale-etag\"\r\n\r\n",
)
.await
.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("200 OK"), "got: {response_str}");
assert!(!response_str.contains("206"), "got: {response_str}");
assert!(response_str.contains("Content-Length: 10"), "got: {response_str}");
assert!(response_str.contains("0123456789"), "got: {response_str}");
server_handle.shutdown().await;
}
#[tokio::test]
async fn range_multi_range_returns_200() {
let root = TempDir::new().unwrap();
let file_path = root.path().join("data.bin");
fs::write(&file_path, b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, server_handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let addr = format!("127.0.0.1:{}", port);
let mut conn = tokio::net::TcpStream::connect(&addr)
.await
.unwrap();
conn.write_all(b"GET /data.bin HTTP/1.1\r\nHost: localhost\r\nRange: bytes=0-2,5-7\r\n\r\n")
.await
.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("200 OK"), "got: {response_str}");
assert!(!response_str.contains("206"), "got: {response_str}");
assert!(response_str.contains("Content-Length: 10"), "got: {response_str}");
assert!(response_str.contains("0123456789"), "got: {response_str}");
server_handle.shutdown().await;
}
#[tokio::test]
async fn head_with_range_returns_206_with_empty_body() {
let root = TempDir::new().unwrap();
let file_path = root.path().join("data.bin");
fs::write(&file_path, b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, server_handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let addr = format!("127.0.0.1:{}", port);
let mut conn = tokio::net::TcpStream::connect(&addr)
.await
.unwrap();
conn.write_all(b"HEAD /data.bin HTTP/1.1\r\nHost: localhost\r\nRange: bytes=0-4\r\n\r\n")
.await
.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("206 Partial Content"), "got: {response_str}");
assert!(response_str.contains("Content-Range: bytes 0-4/10"), "got: {response_str}");
assert!(response_str.contains("Content-Length: 5"), "got: {response_str}");
let parts: Vec<&str> = response_str.split("\r\n\r\n").collect();
assert_eq!(parts.len(), 2, "HEAD response should have headers and empty body");
assert!(parts[1].is_empty(), "HEAD response body must be empty, got: {}", parts[1]);
server_handle.shutdown().await;
}
#[tokio::test]
async fn accept_ranges_header_on_200_response() {
let root = TempDir::new().unwrap();
let file_path = root.path().join("data.bin");
fs::write(&file_path, b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, server_handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let addr = format!("127.0.0.1:{}", port);
let mut conn = tokio::net::TcpStream::connect(&addr)
.await
.unwrap();
conn.write_all(b"GET /data.bin HTTP/1.1\r\nHost: localhost\r\n\r\n")
.await
.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("200 OK"), "got: {response_str}");
assert!(response_str.contains("Accept-Ranges: bytes"), "got: {response_str}");
server_handle.shutdown().await;
}
#[tokio::test]
async fn accept_ranges_header_on_206_response() {
let root = TempDir::new().unwrap();
let file_path = root.path().join("data.bin");
fs::write(&file_path, b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, server_handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let addr = format!("127.0.0.1:{}", port);
let mut conn = tokio::net::TcpStream::connect(&addr)
.await
.unwrap();
conn.write_all(b"GET /data.bin HTTP/1.1\r\nHost: localhost\r\nRange: bytes=0-4\r\n\r\n")
.await
.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("206 Partial Content"), "got: {response_str}");
assert!(response_str.contains("Accept-Ranges: bytes"), "got: {response_str}");
server_handle.shutdown().await;
}
#[tokio::test]
async fn accept_ranges_header_on_416_response() {
let root = TempDir::new().unwrap();
let file_path = root.path().join("data.bin");
fs::write(&file_path, b"0123456789").unwrap();
let server = Server::new(root.path()).unwrap();
let (port, server_handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let addr = format!("127.0.0.1:{}", port);
let mut conn = tokio::net::TcpStream::connect(&addr)
.await
.unwrap();
conn.write_all(b"GET /data.bin HTTP/1.1\r\nHost: localhost\r\nRange: bytes=100-200\r\n\r\n")
.await
.unwrap();
let mut response = Vec::new();
conn.read_to_end(&mut response).await.unwrap();
let response_str = String::from_utf8_lossy(&response);
assert!(response_str.contains("416 Range Not Satisfiable"), "got: {response_str}");
assert!(response_str.contains("Accept-Ranges: bytes"), "got: {response_str}");
server_handle.shutdown().await;
}