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>");
proj.add_app_config("admin", "auth = \"user\"\n");
proj.add_page("admin/index.html", "<h1>Admin</h1>");
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"));
for marker in ["Overview", "Routes", "Collections", "Inheritance", "Sessions", "Scopes", "Lints", "Activity"] {
resp.assert_contains(marker);
}
resp.assert_contains(env!("CARGO_PKG_VERSION"));
resp.assert_contains("/admin");
resp.assert_contains("user");
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(); 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>");
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");
}
#[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);
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);
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("<script>alert(1)</script>");
}
#[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(); 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);
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\">");
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>");
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);
resp.assert_not_contains("x<script>y");
resp.assert_contains("<script>");
}