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 std::fs;
use tempfile::TempDir;

#[test]
fn traversal_and_missing_file_both_return_404() {
    let root = TempDir::new().unwrap();
    fs::write(root.path().join("exists.txt"), b"content").unwrap();
    let server = Server::new(root.path()).unwrap();

    let traversal_response = server.handle_request("/../../../etc/passwd");
    let missing_response = server.handle_request("/nonexistent.txt");

    assert_eq!(traversal_response.status().as_u16(), 404);
    assert_eq!(missing_response.status().as_u16(), 404);
}

#[test]
fn traversal_and_missing_file_have_identical_bodies() {
    let root = TempDir::new().unwrap();
    let server = Server::new(root.path()).unwrap();

    let traversal_response = server.handle_request("/../../../etc/passwd");
    let missing_response = server.handle_request("/nonexistent.txt");

    let traversal_status = traversal_response.status().as_u16();
    let missing_status = missing_response.status().as_u16();

    assert_eq!(traversal_status, missing_status, "status codes should match");
    assert_eq!(traversal_status, 404, "both should be 404");
}

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

    let traversal_response = server.handle_request("/../etc/passwd");
    let missing_response = server.handle_request("/nonexistent.txt");
    let success_response = server.handle_request("/test.txt");

    let traversal_nosniff = traversal_response.headers().get("X-Content-Type-Options").cloned();
    let missing_nosniff = missing_response.headers().get("X-Content-Type-Options").cloned();
    let success_nosniff = success_response.headers().get("X-Content-Type-Options").cloned();

    assert_eq!(
        traversal_nosniff.as_ref().map(|v| v.to_str().unwrap()),
        Some("nosniff"),
        "traversal response should have nosniff"
    );
    assert_eq!(
        missing_nosniff.as_ref().map(|v| v.to_str().unwrap()),
        Some("nosniff"),
        "missing file response should have nosniff"
    );
    assert_eq!(
        success_nosniff.as_ref().map(|v| v.to_str().unwrap()),
        Some("nosniff"),
        "successful response should have nosniff"
    );

    // The success case must return the file's actual content, not a placeholder —
    // this is the check that would have caught `handle_request` answering every
    // successful resolve with a hardcoded "ok\n" body regardless of the file's
    // real content.
    assert_eq!(success_response.status().as_u16(), 200);
    let body = common::body_bytes(success_response).await;
    assert_eq!(&body[..], b"content", "success response body should be the file's real content");
}

#[test]
fn no_information_leak_between_traversal_and_missing() {
    let root = TempDir::new().unwrap();
    let server = Server::new(root.path()).unwrap();

    let traversal_response = server.handle_request("/../../../etc/passwd");
    let missing_response = server.handle_request("/definitely-not-a-real-file-xyz.txt");

    assert_eq!(traversal_response.status(), missing_response.status());

    // Both responses should contain the same user-facing message
    let traversal_msg = traversal_response.headers().get("X-Content-Type-Options");
    let missing_msg = missing_response.headers().get("X-Content-Type-Options");

    assert_eq!(traversal_msg, missing_msg);
}