mod common;
use axum::http::StatusCode;
use common::*;
use serde_json::json;
use what_core::server::create_router;
#[tokio::test]
async fn simple_variable_replacement() {
let proj = TestProject::new();
proj.add_page(
"index.html",
"<what>\ntitle: Hello World\n</what>\n<h1>#title#</h1>",
);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("<h1>Hello World</h1>");
}
#[tokio::test]
async fn variable_with_default() {
let proj = TestProject::new();
proj.add_page("index.html", r##"<p>#missing|default:"fallback"#</p>"##);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("fallback");
}
#[tokio::test]
async fn code_block_protection() {
let proj = TestProject::new();
proj.add_page(
"index.html",
"<what>\nname: Alice\n</what>\n<p>#name#</p><code>#name#</code>",
);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("<p>Alice</p>");
resp.assert_contains("#name#");
}
#[tokio::test]
async fn loop_with_array() {
let proj = TestProject::new();
proj.add_page(
"index.html",
r##"<ul><loop data="#items#" as="item"><li>#item.name#</li></loop></ul>"##,
);
let (_dir, state) = proj.build_state();
state
.store
.create("items", json!({"name": "Alpha"}))
.await
.unwrap();
state
.store
.create("items", json!({"name": "Beta"}))
.await
.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("<li>Alpha</li>");
resp.assert_contains("<li>Beta</li>");
}
#[tokio::test]
async fn loop_with_alias() {
let proj = TestProject::new();
proj.add_page(
"index.html",
r##"<loop data="#posts#" as="post"><h2>#post.title#</h2></loop>"##,
);
let (_dir, state) = proj.build_state();
state
.store
.create("posts", json!({"title": "First Post"}))
.await
.unwrap();
state
.store
.create("posts", json!({"title": "Second Post"}))
.await
.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("<h2>First Post</h2>");
resp.assert_contains("<h2>Second Post</h2>");
}
#[tokio::test]
async fn loop_index_variables() {
let proj = TestProject::new();
proj.add_page(
"index.html",
r##"<loop data="#items#" as="item"><span>#index#-#index1#-#first#-#last#</span></loop>"##,
);
let (_dir, state) = proj.build_state();
state
.store
.set_collection(
"items",
vec![json!({"v": "a"}), json!({"v": "b"}), json!({"v": "c"})],
)
.await
.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("0-1-true-false");
resp.assert_contains("2-3-false-true");
}
#[tokio::test]
async fn loop_empty_array() {
let proj = TestProject::new();
proj.add_page(
"index.html",
r##"<loop data="#items#" as="item"><li>#item.name#</li></loop>"##,
);
let (_dir, state) = proj.build_state();
state.store.set_collection("items", vec![]).await.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_not_contains("<li>");
}
#[tokio::test]
async fn loop_over_object() {
let proj = TestProject::new();
proj.add_page(
"index.html",
r##"<loop data="#settings#" as="val"><span>#key#=#value#</span></loop>"##,
);
let (_dir, state) = proj.build_state();
state
.store
.set("settings", json!({"color": "blue", "size": "large"}))
.await
.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("color=blue");
resp.assert_contains("size=large");
}
#[tokio::test]
async fn if_true_shows_content() {
let proj = TestProject::new();
proj.add_page("index.html", r##"<if cond="#show#">Visible</if>"##);
let (_dir, state) = proj.build_state();
state.store.set("show", json!(true)).await.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Visible");
}
#[tokio::test]
async fn if_false_hides_content() {
let proj = TestProject::new();
proj.add_page(
"index.html",
r##"<if cond="#show#">Hidden</if><p>After</p>"##,
);
let (_dir, state) = proj.build_state();
state.store.set("show", json!(false)).await.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_not_contains("Hidden");
resp.assert_contains("After");
}
#[tokio::test]
async fn if_else_branch() {
let proj = TestProject::new();
proj.add_page("index.html", r##"<if cond="#show#">Yes<else/>No</if>"##);
let (_dir, state) = proj.build_state();
state.store.set("show", json!(false)).await.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("No");
resp.assert_not_contains("Yes");
}
#[tokio::test]
async fn elseif_branch() {
let proj = TestProject::new();
proj.add_page(
"index.html",
r##"<if cond='#status# == "ok"'>OK<elseif cond='#status# == "err"'/>ERR<else/>UNKNOWN</if>"##,
);
let (_dir, state) = proj.build_state();
state.store.set("status", json!("err")).await.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("ERR");
resp.assert_not_contains("OK");
resp.assert_not_contains("UNKNOWN");
}
#[tokio::test]
async fn unless_shows_when_false() {
let proj = TestProject::new();
proj.add_page("index.html", r##"<unless cond="#error#">All OK</unless>"##);
let (_dir, state) = proj.build_state();
state.store.set("error", json!(null)).await.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("All OK");
}
#[tokio::test]
async fn negation_condition() {
let proj = TestProject::new();
proj.add_page("index.html", r##"<if cond="!#hidden#">Shown</if>"##);
let (_dir, state) = proj.build_state();
state.store.set("hidden", json!(false)).await.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Shown");
}
#[tokio::test]
async fn comparison_condition() {
let proj = TestProject::new();
proj.add_page(
"index.html",
r##"<if cond='#role# == "admin"'>Admin Panel</if>"##,
);
let (_dir, state) = proj.build_state();
state.store.set("role", json!("admin")).await.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Admin Panel");
}
#[tokio::test]
async fn include_partial() {
let proj = TestProject::new();
proj.add_page(
"index.html",
r##"<h1>Home</h1><include src="partials/header.html"/>"##,
);
let (_dir, state) = proj.build_state();
std::fs::create_dir_all(_dir.path().join("partials")).unwrap();
std::fs::write(
_dir.path().join("partials/header.html"),
"<nav>Navigation</nav>",
)
.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("<h1>Home</h1>");
resp.assert_contains("<nav>Navigation</nav>");
}
#[tokio::test]
async fn include_with_attributes() {
let proj = TestProject::new();
proj.add_page(
"index.html",
r##"<include src="partials/heading.html" text="Welcome"/>"##,
);
let (_dir, state) = proj.build_state();
std::fs::create_dir_all(_dir.path().join("partials")).unwrap();
std::fs::write(
_dir.path().join("partials/heading.html"),
"<what>\nattribute.text = \"Default\"\n</what>\n<h2>#text#</h2>",
)
.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("<h2>Welcome</h2>");
}
#[tokio::test]
async fn include_missing_file() {
let proj = TestProject::new();
proj.add_page(
"index.html",
r##"<include src="partials/nonexistent.html"/><p>After</p>"##,
);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("<!-- include not found");
resp.assert_contains("<p>After</p>");
}
#[tokio::test]
async fn component_renders() {
let proj = TestProject::new();
proj.add_component(
"card.html",
"<what>\nprops = \"title\"\ndefaults.title = \"Untitled\"\n</what>\n<div class=\"card\"><h3>#title#</h3></div>",
);
proj.add_page("index.html", r##"<what-card title="Test Card"/>"##);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Test Card");
resp.assert_contains("card");
}
#[tokio::test]
async fn component_with_slot() {
let proj = TestProject::new();
proj.add_component(
"box.html",
"<what>\nprops = \"title\"\n</what>\n<div class=\"box\"><h3>#title#</h3><slot/></div>",
);
proj.add_page(
"index.html",
r##"<what-box title="My Box"><p>Inner content</p></what-box>"##,
);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("My Box");
resp.assert_contains("<p>Inner content</p>");
}
#[tokio::test]
async fn component_defaults() {
let proj = TestProject::new();
proj.add_component(
"badge.html",
"<what>\nprops = \"label\"\ndefaults.label = \"Default Label\"\n</what>\n<span class=\"badge\">#label#</span>",
);
proj.add_page("index.html", r##"<what-badge/>"##);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Default Label");
}
#[tokio::test]
async fn component_prop_override() {
let proj = TestProject::new();
proj.add_component(
"badge.html",
"<what>\nprops = \"label\"\ndefaults.label = \"Default Label\"\n</what>\n<span class=\"badge\">#label#</span>",
);
proj.add_page("index.html", r##"<what-badge label="Custom"/>"##);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Custom");
resp.assert_not_contains("Default Label");
}
#[tokio::test]
async fn component_with_loop() {
let proj = TestProject::new();
proj.add_component(
"list.html",
"<what>\nprops = \"items\"\n</what>\n<ul><loop data=\"#items#\" as=\"item\"><li>#item.name#</li></loop></ul>",
);
proj.add_page(
"index.html",
r##"<what-list items='[{"name":"One"},{"name":"Two"}]'/>"##,
);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("<li>One</li>");
resp.assert_contains("<li>Two</li>");
}
#[tokio::test]
async fn include_before_loop() {
let proj = TestProject::new();
proj.add_page(
"index.html",
r##"<include src="partials/user_list.html"/>"##,
);
let (_dir, state) = proj.build_state();
std::fs::create_dir_all(_dir.path().join("partials")).unwrap();
std::fs::write(
_dir.path().join("partials/user_list.html"),
r##"<ul><loop data="#users#" as="u"><li>#u.name#</li></loop></ul>"##,
)
.unwrap();
state
.store
.set_collection(
"users",
vec![json!({"name": "Alice"}), json!({"name": "Bob"})],
)
.await
.unwrap();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("<li>Alice</li>");
resp.assert_contains("<li>Bob</li>");
}
#[tokio::test]
async fn conditionals_after_components() {
let proj = TestProject::new();
proj.add_component(
"toggle.html",
"<what>\nprops = \"active\"\ndefaults.active = \"no\"\n</what>\n<if cond='#active# == \"yes\"'><span>Active</span></if>",
);
proj.add_page("index.html", r##"<what-toggle active="yes"/>"##);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("<span>Active</span>");
}
#[tokio::test]
async fn what_directives_stripped() {
let proj = TestProject::new();
proj.add_page(
"index.html",
"<what>\ntitle: My Page\nauth: all\n</what>\n<h1>#title#</h1>",
);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_not_contains("<what>");
resp.assert_not_contains("</what>");
resp.assert_contains("<h1>My Page</h1>");
}
#[tokio::test]
async fn what_component_tags_preserved() {
let proj = TestProject::new();
proj.add_component(
"card.html",
"<what>\nprops = \"title\"\n</what>\n<div class=\"card\">#title#</div>",
);
proj.add_page("index.html", r##"<what-card title="Hello"/>"##);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Hello");
resp.assert_contains("card");
}
#[tokio::test]
async fn page_title_from_directive() {
let proj = TestProject::new();
proj.add_page(
"index.html",
"<what>\ntitle: Dashboard\n</what>\n<h1>#title#</h1>",
);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("<h1>Dashboard</h1>");
}
#[tokio::test]
async fn partial_route_renders_content() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>Home</h1>");
let dir = proj.root().to_path_buf();
let cd = what_core::server::content_dir_name(proj.root());
let partials = dir.join(cd).join("partials");
std::fs::create_dir_all(&partials).unwrap();
std::fs::write(partials.join("time.html"), "<span>The time is now</span>").unwrap();
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/w-partial/time").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("The time is now");
}
#[tokio::test]
async fn partial_route_returns_noindex_header() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>Home</h1>");
let dir = proj.root().to_path_buf();
let cd = what_core::server::content_dir_name(proj.root());
let partials = dir.join(cd).join("partials");
std::fs::create_dir_all(&partials).unwrap();
std::fs::write(partials.join("hello.html"), "<p>Hello</p>").unwrap();
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/w-partial/hello").await;
assert_eq!(resp.status, StatusCode::OK);
assert_eq!(resp.header("x-robots-tag"), Some("noindex, nofollow"));
}
#[tokio::test]
async fn partial_route_not_found() {
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-partial/nonexistent").await;
assert_eq!(resp.status, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn partial_route_blocks_path_traversal() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>Home</h1>");
let dir = proj.root().to_path_buf();
let cd = what_core::server::content_dir_name(proj.root());
let partials = dir.join(cd).join("partials");
std::fs::create_dir_all(&partials).unwrap();
std::fs::write(partials.join("ok.html"), "<p>Ok</p>").unwrap();
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/w-partial/../index").await;
assert_ne!(resp.status, StatusCode::OK);
}
#[tokio::test]
async fn ssrf_guard_blocks_private_fetch_by_default() {
let proj = TestProject::new();
proj.add_page(
"index.html",
"<what>\nfetch.meta = \"http://169.254.169.254/latest/meta-data/\"\n</what>\n<div id=\"out\">[#meta#]</div>",
);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_not_contains("ami-id");
resp.assert_not_contains("security-credentials");
}