what-core 1.7.0

Core framework for What - an HTML-first web framework powered by Rust
Documentation
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);

    // Unauthenticated request should redirect to login
    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() {
    // Page-level auth can make access MORE restrictive than app config.
    // App config says auth="user" (any authenticated), page says auth="admin" (role required).
    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);

    // A user with role "editor" should be denied because the page requires "admin"
    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);

    // A user with role "admin" should be allowed
    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\">");
}