mini-static 0.7.1

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
mod common;

use mini_static::Server;
use hyper::Method;
use std::fs;
use tempfile::TempDir;

#[tokio::test]
async fn content_length_on_200_response() {
    let root = TempDir::new().unwrap();
    let content = b"hello world";
    fs::write(root.path().join("file.txt"), content).unwrap();

    let server = Server::new(root.path()).unwrap();
    let response = server.handle_request_with_method(&Method::GET, "/file.txt");

    assert_eq!(response.status().as_u16(), 200);

    let content_length = response
        .headers()
        .get("Content-Length")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<u64>().ok());

    assert_eq!(
        content_length,
        Some(content.len() as u64),
        "Content-Length should match file size"
    );

    let body = common::body_bytes(response).await;
    assert_eq!(&body[..], content, "body should be the file's actual bytes");
}

#[tokio::test]
async fn content_length_matches_exact_file_size() {
    let root = TempDir::new().unwrap();

    // Test various file sizes
    let sizes = vec![0, 1, 10, 100, 1000, 1024, 4096];

    for size in sizes {
        let content = vec![0u8; size];
        let filename = format!("file_{}.bin", size);
        fs::write(root.path().join(&filename), &content).unwrap();

        let server = Server::new(root.path()).unwrap();
        let response = server.handle_request_with_method(&Method::GET, &format!("/{}", filename));

        assert_eq!(response.status().as_u16(), 200);

        let content_length = response
            .headers()
            .get("Content-Length")
            .and_then(|v| v.to_str().ok())
            .and_then(|s| s.parse::<u64>().ok());

        assert_eq!(
            content_length,
            Some(size as u64),
            "Content-Length for {} bytes should be {}",
            size,
            size
        );

        let body = common::body_bytes(response).await;
        assert_eq!(body.len(), size, "body length should match for {} bytes", size);
    }
}

#[tokio::test]
async fn content_length_for_empty_file() {
    let root = TempDir::new().unwrap();
    fs::write(root.path().join("empty.txt"), b"").unwrap();

    let server = Server::new(root.path()).unwrap();
    let response = server.handle_request_with_method(&Method::GET, "/empty.txt");

    assert_eq!(response.status().as_u16(), 200);

    let content_length = response
        .headers()
        .get("Content-Length")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<u64>().ok());

    assert_eq!(content_length, Some(0));

    let body = common::body_bytes(response).await;
    assert_eq!(body.len(), 0);
}

#[tokio::test]
async fn content_length_for_large_file() {
    let root = TempDir::new().unwrap();
    let large_size = 10_000_000u64; // 10 MB
    let content = vec![0u8; large_size as usize];
    fs::write(root.path().join("large.bin"), &content).unwrap();

    let server = Server::new(root.path()).unwrap();
    let response = server.handle_request_with_method(&Method::GET, "/large.bin");

    assert_eq!(response.status().as_u16(), 200);

    let content_length = response
        .headers()
        .get("Content-Length")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<u64>().ok());

    assert_eq!(content_length, Some(large_size));

    let body = common::body_bytes(response).await;
    assert_eq!(body.len() as u64, large_size);
}

#[tokio::test]
async fn head_method_includes_content_length_but_empty_body() {
    let root = TempDir::new().unwrap();
    let content = b"test content for head";
    fs::write(root.path().join("file.txt"), content).unwrap();

    let server = Server::new(root.path()).unwrap();
    let response = server.handle_request_with_method(&Method::HEAD, "/file.txt");

    assert_eq!(response.status().as_u16(), 200);

    let content_length = response
        .headers()
        .get("Content-Length")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<u64>().ok());

    assert_eq!(content_length, Some(content.len() as u64));

    let body = common::body_bytes(response).await;
    assert_eq!(body.len(), 0, "HEAD response must not have a body");
}

#[tokio::test]
async fn content_length_with_directory_index() {
    let root = TempDir::new().unwrap();
    fs::create_dir(root.path().join("docs")).unwrap();
    let index_content = b"<html><body>Index</body></html>";
    fs::write(root.path().join("docs/index.html"), index_content).unwrap();

    let server = Server::new(root.path()).unwrap();
    let response = server.handle_request_with_method(&Method::GET, "/docs/");

    assert_eq!(response.status().as_u16(), 200);

    let content_length = response
        .headers()
        .get("Content-Length")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<u64>().ok());

    assert_eq!(content_length, Some(index_content.len() as u64));

    let body = common::body_bytes(response).await;
    assert_eq!(&body[..], index_content);
}

#[test]
fn no_content_length_on_404() {
    let root = TempDir::new().unwrap();

    let server = Server::new(root.path()).unwrap();
    let response = server.handle_request_with_method(&Method::GET, "/nonexistent.txt");

    assert_eq!(response.status().as_u16(), 404);

    // 404 responses should not have Content-Length (or have a specific length for the error message)
    // For now, we're not setting it, so it should be absent
    let has_content_length = response.headers().get("Content-Length").is_some();
    // This test documents the current behavior; feel free to adjust if a specific error format is desired
    let _ = has_content_length;
}

#[test]
fn no_content_length_on_405() {
    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);

    // 405 responses should not have Content-Length
    let has_content_length = response.headers().get("Content-Length").is_some();
    let _ = has_content_length;
}

#[tokio::test]
async fn content_length_with_special_chars_in_filename() {
    let root = TempDir::new().unwrap();
    let content = b"special file content";
    let filename = "file with spaces.txt";
    fs::write(root.path().join(filename), content).unwrap();

    let server = Server::new(root.path()).unwrap();
    let response =
        server.handle_request_with_method(&Method::GET, "/file%20with%20spaces.txt");

    assert_eq!(response.status().as_u16(), 200);

    let content_length = response
        .headers()
        .get("Content-Length")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<u64>().ok());

    assert_eq!(content_length, Some(content.len() as u64));

    let body = common::body_bytes(response).await;
    assert_eq!(&body[..], content);
}