what-core 1.7.5

Core framework for What - an HTML-first web framework powered by Rust
Documentation
mod common;

use axum::http::StatusCode;
use common::*;
use what_core::server::create_router;

#[tokio::test]
async fn root_index_returns_200() {
    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, "/").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_contains("<h1>Home</h1>");
}

#[tokio::test]
async fn exact_page_match() {
    let proj = TestProject::new();
    proj.add_page("about.html", "<h1>About Us</h1>");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    let resp = get(&router, "/about").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_contains("<h1>About Us</h1>");
}

#[tokio::test]
async fn nested_page_match() {
    let proj = TestProject::new();
    proj.add_page("admin/users.html", "<h1>User List</h1>");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    let resp = get(&router, "/admin/users").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_contains("<h1>User List</h1>");
}

#[tokio::test]
async fn directory_index_fallback() {
    let proj = TestProject::new();
    proj.add_page("admin/index.html", "<h1>Admin Dashboard</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("<h1>Admin Dashboard</h1>");
}

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

    let resp = get(&router, "/blog/2024").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_contains("<h1>Blog 2024</h1>");
}

#[tokio::test]
async fn dynamic_route_resolves() {
    let proj = TestProject::new();
    proj.add_page("post/[id].html", "<h1>Post Detail</h1>");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    let resp = get(&router, "/post/123").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_contains("<h1>Post Detail</h1>");
}

#[tokio::test]
async fn dynamic_route_injects_id() {
    let proj = TestProject::new();
    proj.add_page("post/[id].html", "<h1>Post #id#</h1>");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    let resp = get(&router, "/post/123").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_contains("123");
}

#[tokio::test]
async fn dynamic_route_url_encoded_param() {
    let proj = TestProject::new();
    proj.add_page("post/[id].html", "<p>#id#</p>");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    // Route params are extracted from the raw URL path, so percent-encoding is preserved
    let resp = get(&router, "/post/hello%20world").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_contains("hello%20world");
}

#[tokio::test]
async fn nonexistent_page_returns_404() {
    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, "/nothing").await;
    assert_eq!(resp.status, StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn custom_404_page() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page("404.html", "<h1>Oops! Not Found</h1>");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    let resp = get(&router, "/nothing").await;
    assert_eq!(resp.status, StatusCode::NOT_FOUND);
    resp.assert_contains("Oops! Not Found");
}

#[tokio::test]
async fn default_404_without_custom_page() {
    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, "/nothing").await;
    assert_eq!(resp.status, StatusCode::NOT_FOUND);
    resp.assert_contains("Page Not Found");
}

#[tokio::test]
async fn html_content_type_header() {
    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, "/").await;
    assert_eq!(resp.status, StatusCode::OK);
    assert_eq!(resp.content_type(), Some("text/html"));
}

#[tokio::test]
async fn partial_enforces_auth_directive() {
    // SECURITY: a partial declaring `auth: user` must not be served to an
    // anonymous visitor via GET /w-partial/<name> (broken access control).
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "partials/secret.html",
        "<what>\nauth: user\n</what>\n<p>TOP SECRET</p>",
    );
    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, "/w-partial/secret").await;
    assert_eq!(resp.status, StatusCode::FORBIDDEN);
    resp.assert_not_contains("TOP SECRET");
}

#[tokio::test]
async fn public_partial_still_served() {
    // A partial with no auth directive remains publicly fetchable.
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page("partials/promo.html", "<p>Public promo</p>");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

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

#[tokio::test]
async fn partial_inherits_application_what_auth() {
    // Gating the whole app with a root site/application.what `auth: user` must
    // also protect partials that have no inline <what> auth block — otherwise
    // /w-partial/<name> leaks them to anonymous visitors.
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_app_config("", "auth = \"user\"\n"); // root site/application.what
    proj.add_page("partials/widget.html", "<p>MEMBER WIDGET</p>"); // no inline auth
    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, "/w-partial/widget").await;
    assert_eq!(resp.status, StatusCode::FORBIDDEN);
    resp.assert_not_contains("MEMBER WIDGET");
}

#[tokio::test]
async fn action_partial_inherits_application_what_auth() {
    // Same inherited-auth protection on the action render path.
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_app_config("", "auth = \"user\"\n");
    proj.add_page("partials/widget.html", "<p>MEMBER WIDGET</p>");
    let config = auth_config_with_secret("test-secret");
    let (_dir, state) = proj.build_state_with_config(config);
    let router = create_router(state);

    let resp = post_form_with_headers(
        &router,
        "/w-action/notes?w-redirect=/",
        "title=x&w-partial=widget",
        vec![("x-requested-with", "What")],
    )
    .await;
    resp.assert_not_contains("MEMBER WIDGET");
}

#[tokio::test]
async fn action_partial_render_enforces_auth_directive() {
    // SECURITY: the action partial-render path (X-Requested-With: What + a
    // w-partial field) must also enforce the partial's auth directive — an anon
    // create must not return an `auth: user` partial's content in the response.
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "partials/secret.html",
        "<what>\nauth: user\n</what>\n<p>TOP SECRET PANEL</p>",
    );
    let config = auth_config_with_secret("test-secret");
    let (_dir, state) = proj.build_state_with_config(config);
    let router = create_router(state);

    // Anonymous create (create = all) requesting the protected partial back.
    let resp = post_form_with_headers(
        &router,
        "/w-action/notes?w-redirect=/",
        "title=x&w-partial=secret",
        vec![("x-requested-with", "What")],
    )
    .await;

    // The protected partial's content must never appear in the response.
    resp.assert_not_contains("TOP SECRET PANEL");
}