mod common;
use axum::http::StatusCode;
use common::*;
use serde_json::json;
use what_core::server::create_router;
fn future_exp() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
+ 3600
}
fn auth_config_with_roles(secret: &str) -> what_core::Config {
let mut config = auth_config_with_secret(secret);
config.auth.jwt_claims.push("roles".to_string());
config
}
#[tokio::test]
async fn root_config_applies_to_page() {
let proj = TestProject::new();
proj.add_app_config("", "title = \"My Site\"");
proj.add_page("index.html", "<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("My Site");
}
#[tokio::test]
async fn child_overrides_parent() {
let proj = TestProject::new();
proj.add_app_config("", "title = \"Root\"");
proj.add_app_config("admin", "title = \"Admin\"");
proj.add_page("admin/index.html", "<h1>#title#</h1>");
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/admin").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Admin");
resp.assert_not_contains("Root");
}
#[tokio::test]
async fn parent_values_inherited() {
let proj = TestProject::new();
proj.add_app_config("", "theme = \"light\"");
proj.add_app_config("admin", "title = \"Admin\"");
proj.add_page("admin/index.html", "<p>#theme#</p>");
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/admin").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("light");
}
#[tokio::test]
async fn three_level_deep_inheritance() {
let proj = TestProject::new();
proj.add_app_config("", "theme = \"light\"\ntitle = \"Root\"");
proj.add_app_config("admin", "title = \"Admin\"\nsidebar = \"narrow\"");
proj.add_app_config("admin/settings", "title = \"Settings\"");
proj.add_page(
"admin/settings/index.html",
"<p>#title# #theme# #sidebar#</p>",
);
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/admin/settings").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Settings");
resp.assert_contains("light");
resp.assert_contains("narrow");
}
#[tokio::test]
async fn auth_cascades_to_children() {
let proj = TestProject::new();
proj.add_app_config("", "auth = \"all\"");
proj.add_app_config("admin", "auth = \"admin\"");
proj.add_page("admin/dashboard.html", "<h1>Dashboard</h1>");
let config = auth_config_with_secret("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let resp = get(&router, "/admin/dashboard").await;
assert_eq!(resp.status, StatusCode::SEE_OTHER);
let location = resp.location().expect("should have Location header");
assert!(
location.contains("/login"),
"should redirect to login, got: {}",
location
);
}
#[tokio::test]
async fn page_level_auth_overrides_app_config() {
let proj = TestProject::new();
proj.add_app_config("section", "auth = \"user\"");
proj.add_page(
"section/admin-only.html",
"<what auth=\"admin\" />\n<h1>Admin Only</h1>",
);
let config = auth_config_with_roles("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let token = create_test_jwt(
&json!({ "email": "editor@test.com", "roles": ["editor"], "exp": future_exp() }),
"test-secret",
);
let resp = get_with_headers(
&router,
"/section/admin-only",
vec![("cookie", &format!("w_token={}", token))],
)
.await;
assert_eq!(resp.status, StatusCode::FORBIDDEN);
let admin_token = create_test_jwt(
&json!({ "email": "admin@test.com", "roles": ["admin"], "exp": future_exp() }),
"test-secret",
);
let resp = get_with_headers(
&router,
"/section/admin-only",
vec![("cookie", &format!("w_token={}", admin_token))],
)
.await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Admin Only");
}
#[tokio::test]
async fn layout_wrapping_from_app_config() {
let proj = TestProject::new();
proj.add_app_config("", "layout = \"layouts/main.html\"");
proj.add_layout(
"layouts/main.html",
"<header>Nav</header><slot/><footer>Foot</footer>",
);
proj.add_page("index.html", "<main>Content</main>");
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("Nav");
resp.assert_contains("Content");
resp.assert_contains("Foot");
}
#[tokio::test]
async fn layout_slot_replacement() {
let proj = TestProject::new();
proj.add_app_config("", "layout = \"layouts/base.html\"");
proj.add_layout("layouts/base.html", "<div class=\"wrapper\"><slot/></div>");
proj.add_page("index.html", "<p>Inner Content</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("<div class=\"wrapper\"><p>Inner Content</p></div>");
}
#[tokio::test]
async fn layout_none_disables() {
let proj = TestProject::new();
proj.add_app_config("", "layout = \"layouts/main.html\"");
proj.add_layout(
"layouts/main.html",
"<header>Nav</header><slot/><footer>Foot</footer>",
);
proj.add_page("bare.html", "<what layout=\"none\" />\n<p>No Layout</p>");
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/bare").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("No Layout");
resp.assert_not_contains("Nav");
resp.assert_not_contains("Foot");
}
#[tokio::test]
async fn custom_values_in_templates() {
let proj = TestProject::new();
proj.add_app_config("", "brand = \"Acme\"");
proj.add_page("index.html", "<p>#brand#</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("Acme");
}
#[tokio::test]
async fn layout_from_child_config() {
let proj = TestProject::new();
proj.add_app_config("", "layout = \"layouts/base.html\"");
proj.add_app_config("admin", "layout = \"layouts/admin.html\"");
proj.add_layout("layouts/base.html", "<div class=\"base\"><slot/></div>");
proj.add_layout("layouts/admin.html", "<div class=\"admin\"><slot/></div>");
proj.add_page("admin/index.html", "<p>Admin Content</p>");
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/admin").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("<div class=\"admin\">");
resp.assert_contains("Admin Content");
resp.assert_not_contains("<div class=\"base\">");
}