what-core 1.7.5

Core framework for What - an HTML-first web framework powered by Rust
Documentation
//! Integration tests for the dev-mode inspector (/w-inspector).

mod common;

use axum::http::StatusCode;
use common::*;
use what_core::server::create_router;
use what_core::{CollectionPolicyConfig, Config};

#[tokio::test]
async fn inspector_renders_in_dev_with_panels() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    // A protected sub-route via application.what inheritance.
    proj.add_app_config("admin", "auth = \"user\"\n");
    proj.add_page("admin/index.html", "<h1>Admin</h1>");

    // A collection with an explicit policy.
    let mut config = Config::default();
    config.collections.insert(
        "notes".to_string(),
        CollectionPolicyConfig {
            read: Some("owner".to_string()),
            ..Default::default()
        },
    );
    let (_dir, state) = proj.build_state_dev_with_config(config);
    let router = create_router(state);

    let resp = get(&router, "/w-inspector").await;
    assert_eq!(resp.status, StatusCode::OK);
    assert!(resp.content_type().unwrap_or("").contains("text/html"));

    // All eight panels present.
    for marker in ["Overview", "Routes", "Collections", "Inheritance", "Sessions", "Scopes", "Lints", "Activity"] {
        resp.assert_contains(marker);
    }
    // Framework version.
    resp.assert_contains(env!("CARGO_PKG_VERSION"));
    // The protected route shows resolved "user" auth.
    resp.assert_contains("/admin");
    resp.assert_contains("user");
    // The collection and its rule text.
    resp.assert_contains("notes");
    resp.assert_contains("read=");
    resp.assert_contains("owner");
}

#[tokio::test]
async fn inspector_404s_in_non_dev() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    let (_dir, state) = proj.build_state(); // dev_mode = false
    let router = create_router(state);

    let resp = get(&router, "/w-inspector").await;
    assert_eq!(resp.status, StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn inspector_surfaces_template_lints() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    // Unclosed <if> — a lint.
    proj.add_page("broken.html", "<if x>oops");
    let (_dir, state) = proj.build_state_dev();
    let router = create_router(state);

    let resp = get(&router, "/w-inspector").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_contains("/broken");
    resp.assert_contains("unclosed");
}

#[tokio::test]
async fn inspector_handles_sessions_disabled() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    let mut config = Config::default();
    config.session.enabled = false;
    let (_dir, state) = proj.build_state_dev_with_config(config);
    let router = create_router(state);

    let resp = get(&router, "/w-inspector").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_contains("Sessions disabled");
}

// ---------------------------------------------------------------------------
// Activity feed (v1.7)
// ---------------------------------------------------------------------------

#[tokio::test]
async fn activity_feed_records_requests() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    let (_dir, state) = proj.build_state_dev();
    let router = create_router(state);

    get(&router, "/").await;
    get(&router, "/missing-page").await;

    let resp = get(&router, "/w-inspector").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_contains("<code>GET /</code> → 200");
    resp.assert_contains("<code>GET /missing-page</code> → 404");
}

#[tokio::test]
async fn activity_feed_skips_inspector_requests() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    let (_dir, state) = proj.build_state_dev();
    let router = create_router(state);

    // Viewing the inspector must not fill the feed with itself.
    get(&router, "/w-inspector").await;
    let resp = get(&router, "/w-inspector").await;
    resp.assert_not_contains("<code>GET /w-inspector</code>");
}

#[tokio::test]
async fn activity_feed_records_policy_denials() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    let (_dir, state) = proj.build_state_dev();
    let router = create_router(state);

    // Visitor A creates a note; visitor B (fresh session) tries to update it —
    // denied by the v1.4 owner-protected default.
    post_form(&router, "/w-action/notes?w-redirect=/", "title=Mine").await;
    post_form(&router, "/w-action/notes/1?w-redirect=/", "title=Hacked").await;

    let resp = get(&router, "/w-inspector").await;
    resp.assert_contains("deny");
    resp.assert_contains("collection=notes");
    resp.assert_contains("action=update");
}

#[tokio::test]
async fn activity_feed_records_fetches() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "list.html",
        "<what>\nfetch.notes = \"local:notes\"\n</what>\n<loop data=\"#notes#\" as=\"n\"><li>#n.title#</li></loop>",
    );
    let (_dir, state) = proj.build_state_dev();
    let router = create_router(state);

    get(&router, "/list").await;

    let resp = get(&router, "/w-inspector").await;
    resp.assert_contains("fetch");
    resp.assert_contains("local:notes");
}

#[tokio::test]
async fn activity_feed_escapes_untrusted_detail() {
    use what_core::server::ActivityEvent;

    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    let (_dir, state) = proj.build_state_dev();
    state.record_activity(ActivityEvent::PolicyDenial {
        time: chrono::Local::now(),
        detail: "collection=x<script>alert(1)</script>y; action=create".to_string(),
    });
    let router = create_router(state);

    let resp = get(&router, "/w-inspector").await;
    resp.assert_not_contains("<script>alert(1)</script>");
    resp.assert_contains("&lt;script&gt;alert(1)&lt;/script&gt;");
}

#[tokio::test]
async fn activity_feed_not_recorded_in_production() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    let (_dir, state) = proj.build_state(); // dev_mode = false
    let router = create_router(state.clone());

    get(&router, "/").await;

    assert!(state.activity_log.lock().unwrap().is_empty());
}

#[tokio::test]
async fn activity_feed_ring_buffer_caps_at_200() {
    use what_core::server::ActivityEvent;

    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    let (_dir, state) = proj.build_state_dev();

    for i in 0..250 {
        state.record_activity(ActivityEvent::PolicyDenial {
            time: chrono::Local::now(),
            detail: format!("event-{i}"),
        });
    }

    let log = state.activity_log.lock().unwrap();
    assert_eq!(log.len(), 200);
    // Oldest 50 evicted — the front is now event 50.
    match log.front().unwrap() {
        ActivityEvent::PolicyDenial { detail, .. } => assert_eq!(detail, "event-50"),
        _ => panic!("unexpected event kind"),
    }
}

#[tokio::test]
async fn inspector_refresh_param_injects_meta_refresh() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    let (_dir, state) = proj.build_state_dev();
    let router = create_router(state);

    let plain = get(&router, "/w-inspector").await;
    plain.assert_not_contains("http-equiv=\"refresh\"");

    let auto = get(&router, "/w-inspector?refresh=2").await;
    auto.assert_contains("<meta http-equiv=\"refresh\" content=\"2\">");

    // Out-of-range values are ignored.
    let bogus = get(&router, "/w-inspector?refresh=999").await;
    bogus.assert_not_contains("http-equiv=\"refresh\"");
}

#[tokio::test]
async fn inspector_escapes_untrusted_values() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    // A collection name containing HTML — must be escaped in the dashboard.
    let mut config = Config::default();
    config.collections.insert(
        "x<script>y".to_string(),
        CollectionPolicyConfig::default(),
    );
    let (_dir, state) = proj.build_state_dev_with_config(config);
    let router = create_router(state);

    let resp = get(&router, "/w-inspector").await;
    assert_eq!(resp.status, StatusCode::OK);
    // The raw script tag must not appear; escaped form must.
    resp.assert_not_contains("x<script>y");
    resp.assert_contains("&lt;script&gt;");
}