mini-static 0.20.0

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
use super::*;
use std::fs;
use tempfile::TempDir;
use tokio::time::{sleep, timeout};

#[tokio::test]
async fn broadcaster_delivers_events_to_subscribers() {
    let broadcaster = Broadcaster::new();
    let mut rx = broadcaster.subscribe();

    let event = ChangeEvent {
        path: PathBuf::from("style.css"),
        change_type: ChangeType::Css,
    };
    broadcaster.broadcast(event.clone());

    let received = timeout(Duration::from_secs(1), rx.recv())
        .await
        .expect("timeout")
        .expect("channel closed");
    assert_eq!(received.path, event.path);
    assert_eq!(received.change_type, event.change_type);
}

#[tokio::test]
async fn broadcaster_tracks_subscriber_count() {
    let broadcaster = Broadcaster::new();
    assert_eq!(broadcaster.subscriber_count(), 0);

    let _rx1 = broadcaster.subscribe();
    assert_eq!(broadcaster.subscriber_count(), 1);

    let _rx2 = broadcaster.subscribe();
    assert_eq!(broadcaster.subscriber_count(), 2);

    drop(_rx1);
    broadcaster.broadcast(ChangeEvent {
        path: PathBuf::from("file.js"),
        change_type: ChangeType::Script,
    });
    assert_eq!(broadcaster.subscriber_count(), 1);
}

#[tokio::test]
async fn watcher_detects_file_changes() {
    let temp = TempDir::new().unwrap();
    let dir_path = Arc::new(temp.path().to_path_buf());

    let broadcaster = Broadcaster::new();
    let mut rx = broadcaster.subscribe();

    start_watching(Arc::clone(&dir_path), broadcaster);

    sleep(Duration::from_millis(600)).await;

    fs::write(dir_path.join("new_file.js"), "console.log('hello');").unwrap();

    let event = timeout(Duration::from_secs(2), rx.recv())
        .await
        .expect("timeout")
        .expect("channel closed");
    assert_eq!(event.path.file_name().unwrap(), "new_file.js");
    assert_eq!(event.change_type, ChangeType::Script);
}

#[tokio::test]
async fn watcher_detects_file_modifications() {
    let temp = TempDir::new().unwrap();
    let dir_path = Arc::new(temp.path().to_path_buf());
    let file_path = dir_path.join("style.css");
    fs::write(&file_path, "body { color: red; }").unwrap();

    let broadcaster = Broadcaster::new();
    let mut rx = broadcaster.subscribe();

    start_watching(Arc::clone(&dir_path), broadcaster);

    sleep(Duration::from_millis(600)).await;

    fs::write(&file_path, "body { color: blue; }").unwrap();

    let event = timeout(Duration::from_secs(2), rx.recv())
        .await
        .expect("timeout")
        .expect("channel closed");
    assert_eq!(event.path.file_name().unwrap(), "style.css");
    assert_eq!(event.change_type, ChangeType::Css);
}

#[tokio::test]
async fn watcher_ignores_changes_in_first_pass() {
    let temp = TempDir::new().unwrap();
    let dir_path = Arc::new(temp.path().to_path_buf());
    fs::write(dir_path.join("existing.html"), "<h1>Hello</h1>").unwrap();

    let broadcaster = Broadcaster::new();
    let mut rx = broadcaster.subscribe();

    start_watching(Arc::clone(&dir_path), broadcaster);

    sleep(Duration::from_millis(600)).await;

    let result = timeout(Duration::from_millis(100), rx.recv()).await;
    assert!(
        result.is_err(),
        "first pass should not emit events for existing files"
    );
}