mini-static 0.18.0

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

use hyper::Method;
use mini_static::Server;
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 = common::get(&server, "/file.txt").await;

    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 = common::get(&server, &format!("/{}", filename)).await;

        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 = common::get(&server, "/empty.txt").await;

    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 = common::get(&server, "/large.bin").await;

    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 = common::request(&server, &Method::HEAD, "/file.txt").await;

    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 = common::get(&server, "/docs/").await;

    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);
}

#[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 = common::get(&server, "/file%20with%20spaces.txt").await;

    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);
}