mini-static 0.5.2

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
use mini_static::Server;
use hyper::Method;
use std::fs;
use tempfile::TempDir;

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

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

    assert_eq!(response.status().as_u16(), 301);
    assert_eq!(
        response
            .headers()
            .get("Location")
            .and_then(|v| v.to_str().ok()),
        Some("/docs/"),
        "redirect location should be /docs/"
    );
}

#[test]
fn directory_with_slash_serves_index() {
    let root = TempDir::new().unwrap();
    fs::create_dir(root.path().join("docs")).unwrap();
    fs::write(root.path().join("docs/index.html"), b"<html>Index</html>").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);
    assert!(response.headers().get("Location").is_none());
}

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

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

    assert_eq!(response.status().as_u16(), 301);
    assert_eq!(
        response
            .headers()
            .get("X-Content-Type-Options")
            .and_then(|v| v.to_str().ok()),
        Some("nosniff")
    );
}

#[test]
fn nested_directory_redirect() {
    let root = TempDir::new().unwrap();
    fs::create_dir_all(root.path().join("api/v1/users")).unwrap();
    fs::write(root.path().join("api/v1/users/index.html"), b"<html>Users</html>").unwrap();

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

    assert_eq!(response.status().as_u16(), 301);
    assert_eq!(
        response
            .headers()
            .get("Location")
            .and_then(|v| v.to_str().ok()),
        Some("/api/v1/users/")
    );
}

#[test]
fn root_directory_redirect() {
    let root = TempDir::new().unwrap();
    fs::write(root.path().join("index.html"), b"<html>Home</html>").unwrap();

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

    // Empty path is treated as "/" which should already have the slash
    // So this might not redirect; let's check what happens
    let status = response.status().as_u16();
    // Status should be either 301 (redirect) or 200 (served)
    assert!(status == 301 || status == 200);
}

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

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

    assert_eq!(response.status().as_u16(), 200);
    assert!(response.headers().get("Location").is_none());
}

#[test]
fn percent_encoded_explicit_index_html_request_no_redirect() {
    // Regression test: `/docs/index.htm%6c` decodes to `/docs/index.html`. The redirect
    // decision must compare against the *decoded* path, not the raw encoded string, or
    // this incorrectly produces a broken `Location: /docs/index.htm%6c/`.
    let root = TempDir::new().unwrap();
    fs::create_dir(root.path().join("docs")).unwrap();
    fs::write(root.path().join("docs/index.html"), b"<html>Index</html>").unwrap();

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

    assert_eq!(response.status().as_u16(), 200);
    assert!(
        response.headers().get("Location").is_none(),
        "percent-encoded explicit index.html request should not redirect"
    );
}

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

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

    assert_eq!(response.status().as_u16(), 301);
    assert_eq!(
        response
            .headers()
            .get("Location")
            .and_then(|v| v.to_str().ok()),
        Some("/docs/")
    );
}

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

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

    assert_eq!(response.status().as_u16(), 301);
    assert_eq!(
        response
            .headers()
            .get("Location")
            .and_then(|v| v.to_str().ok()),
        Some("/my%20docs/")
    );
}

#[test]
fn directory_without_index_html_returns_404() {
    let root = TempDir::new().unwrap();
    fs::create_dir(root.path().join("empty")).unwrap();

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

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

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

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

    let location = response
        .headers()
        .get("Location")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("");

    // Should be /docs/, not //docs/ or /docs//
    assert_eq!(location, "/docs/");
    assert!(!location.contains("//"));
}