mini-static 0.15.0

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
use std::fs;
use std::time::Duration;

use mini_static::Server;
use tempfile::TempDir;

#[tokio::test]
async fn js_source_folder_minifies_per_file_into_output() {
    let src = TempDir::new().unwrap();
    let out = TempDir::new().unwrap();
    let source_js = "const greeting = 'hello';\n\nfunction greet() {\n    console.log(greeting);\n}\n\ngreet();\n";
    fs::write(src.path().join("app.js"), source_js).unwrap();

    let server = Server::new(out.path())
        .unwrap()
        .with_source_folder(src.path())
        .unwrap();
    let (_port, handle) = server.run_ephemeral().await.unwrap();

    tokio::time::sleep(Duration::from_millis(200)).await;

    let out_js = fs::read(out.path().join("app.js")).unwrap();
    assert!(
        !out_js.is_empty(),
        "expected a mirrored minified app.js in the output dir"
    );
    assert!(
        out_js.len() < source_js.len(),
        "expected minified output ({} bytes) to be smaller than source ({} bytes)",
        out_js.len(),
        source_js.len()
    );

    handle.shutdown().await;
}

#[tokio::test]
async fn js_source_change_rebuilds_the_changed_file() {
    let src = TempDir::new().unwrap();
    let out = TempDir::new().unwrap();
    fs::write(src.path().join("app.js"), "const value = 1;\n").unwrap();

    let server = Server::new(out.path())
        .unwrap()
        .with_live_reload()
        .with_source_folder(src.path())
        .unwrap();
    let (_port, handle) = server.run_ephemeral().await.unwrap();

    // Let the watcher's first 500ms poll pass so the write below is seen as a change.
    tokio::time::sleep(Duration::from_millis(800)).await;
    fs::write(src.path().join("app.js"), "const value = 2;\n").unwrap();
    tokio::time::sleep(Duration::from_millis(1500)).await;

    let out_js = fs::read_to_string(out.path().join("app.js")).unwrap();
    assert!(
        out_js.contains("value=2"),
        "expected the changed source to be re-minified into the output, got: {out_js}"
    );

    handle.shutdown().await;
}

#[tokio::test]
async fn malformed_js_source_degrades_to_raw_output() {
    let src = TempDir::new().unwrap();
    let out = TempDir::new().unwrap();
    let malformed = b"function( {{{ !!!";
    fs::write(src.path().join("broken.js"), malformed).unwrap();

    let server = Server::new(out.path())
        .unwrap()
        .with_source_folder(src.path())
        .unwrap();
    let (_port, handle) = server.run_ephemeral().await.unwrap();

    tokio::time::sleep(Duration::from_millis(200)).await;

    let out_js = fs::read(out.path().join("broken.js")).unwrap();
    assert_eq!(
        out_js, malformed,
        "a file the minifier cannot parse must still be mirrored (raw) so the page stays in sync"
    );

    handle.shutdown().await;
}

#[tokio::test]
async fn prune_output_removes_stale_css_bundle_when_no_css_sources_remain() {
    let src = TempDir::new().unwrap();
    let out = TempDir::new().unwrap();
    fs::write(out.path().join("styles.css"), "/* stale */").unwrap();

    let server = Server::new(out.path())
        .unwrap()
        .with_source_folder(src.path())
        .unwrap()
        .with_css_bundle()
        .with_prune_output();
    let (_port, handle) = server.run_ephemeral().await.unwrap();

    tokio::time::sleep(Duration::from_millis(200)).await;

    assert!(
        !out.path().join("styles.css").exists(),
        "prune is opt-in: with_prune_output should delete the stale bundle produced by no css sources"
    );

    handle.shutdown().await;
}

#[tokio::test]
async fn without_prune_stale_css_bundle_is_left_in_place() {
    let src = TempDir::new().unwrap();
    let out = TempDir::new().unwrap();
    fs::write(out.path().join("styles.css"), "/* stale */").unwrap();

    let server = Server::new(out.path())
        .unwrap()
        .with_source_folder(src.path())
        .unwrap()
        .with_css_bundle();
    let (_port, handle) = server.run_ephemeral().await.unwrap();

    tokio::time::sleep(Duration::from_millis(200)).await;

    assert!(
        out.path().join("styles.css").exists(),
        "without with_prune_output the stale bundle must be left in place"
    );

    handle.shutdown().await;
}