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 std::time::{SystemTime, UNIX_EPOCH};
use what_core::server::create_router;

#[tokio::test]
async fn directory_without_index_returns_404() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    // Create blog directory but no index.html inside it
    proj.add_page("blog/post.html", "<h1>A Post</h1>");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    let resp = get(&router, "/blog").await;
    assert_eq!(resp.status, StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn deep_nonexistent_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, "/a/b/c/d").await;
    assert_eq!(resp.status, StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn exclude_directive_returns_404() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page("hidden.html", "<what exclude />\n<h1>Secret</h1>");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    let resp = get(&router, "/hidden").await;
    assert_eq!(resp.status, StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn redirect_directive_returns_3xx() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page("old.html", "<what redirect=\"/new-page\" />\n<h1>Old</h1>");
    proj.add_page("new-page.html", "<h1>New</h1>");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    let resp = get(&router, "/old").await;
    assert!(
        resp.status.is_redirection(),
        "expected redirect, got {}",
        resp.status
    );
    assert_eq!(resp.location(), Some("/new-page"));
}

#[tokio::test]
async fn static_file_serving() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_static("style.css", b"body { color: red; }");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    let resp = get(&router, "/static/style.css").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_contains("body { color: red; }");
}

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

    let resp = get(&router, "/w-sessions/list").await;
    assert_eq!(resp.status, StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn dev_endpoint_200_in_dev() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    let (_dir, state) = proj.build_state_dev(); // dev mode
    let router = create_router(state);

    let resp = get(&router, "/w-sessions/list").await;
    assert_eq!(resp.status, StatusCode::OK);
}

#[tokio::test]
async fn cache_header_production() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_static("app.css", b"h1 { font-size: 2em; }");
    let (_dir, state) = proj.build_state(); // production mode
    let router = create_router(state);

    let resp = get(&router, "/static/app.css").await;
    assert_eq!(resp.status, StatusCode::OK);
    let cc = resp
        .header("cache-control")
        .expect("should have cache-control");
    assert!(
        cc.contains("max-age=3600"),
        "production should have max-age=3600, got: {}",
        cc
    );
}

#[tokio::test]
async fn cache_header_dev() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_static("app.css", b"h1 { font-size: 2em; }");
    let (_dir, state) = proj.build_state_dev(); // dev mode
    let router = create_router(state);

    let resp = get(&router, "/static/app.css").await;
    assert_eq!(resp.status, StatusCode::OK);
    let cc = resp
        .header("cache-control")
        .expect("should have cache-control");
    assert!(
        cc.contains("no-cache"),
        "dev should have no-cache, got: {}",
        cc
    );
}

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

    let resp = get(&router, "/nonexistent-page").await;
    assert_eq!(resp.status, StatusCode::NOT_FOUND);
    resp.assert_contains("Custom 404 - Page Not Found");
}

#[tokio::test]
async fn custom_404_with_error_context() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "404.html",
        "<h1>Error #error.status#</h1><p>Path: #error.path#</p>",
    );
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    let resp = get(&router, "/does-not-exist").await;
    assert_eq!(resp.status, StatusCode::NOT_FOUND);
    resp.assert_contains("Error 404");
    resp.assert_contains("Path: /does-not-exist");
}

#[tokio::test]
async fn custom_403_page_used() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    // auth: admin requires admin role — a regular authenticated user will get 403
    proj.add_page("secret.html", "<what auth=\"admin\" />\n<h1>Secret</h1>");
    proj.add_page(
        "403.html",
        "<h1>Custom 403 - Access Denied</h1><p>#error.path#</p>",
    );
    // Build with auth enabled and a known JWT secret
    let mut config = what_core::Config::default();
    config.auth.enabled = true;
    config.auth.jwt_secret = Some("test-secret".to_string());
    let (_dir, state) = proj.build_state_with_config(config);
    let router = create_router(state);

    // Create a JWT for a regular user (no admin role)
    let exp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs()
        + 3600;
    let claims = serde_json::json!({
        "sub": "user1",
        "id": "user1",
        "email": "user@test.com",
        "full_name": "Test User",
        "exp": exp
    });
    let token = create_test_jwt(&claims, "test-secret");

    // Access with auth but no admin role should get 403
    let resp = get_with_headers(
        &router,
        "/secret",
        vec![("cookie", &format!("w_token={}", token))],
    )
    .await;
    assert_eq!(resp.status, StatusCode::FORBIDDEN);
    resp.assert_contains("Custom 403 - Access Denied");
    resp.assert_contains("/secret");
}

#[tokio::test]
async fn error_message_hidden_in_production() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "404.html",
        "<h1>#error.status#</h1><p>Detail: #error.message#</p>",
    );
    let (_dir, state) = proj.build_state(); // production mode
    let router = create_router(state);

    let resp = get(&router, "/missing").await;
    assert_eq!(resp.status, StatusCode::NOT_FOUND);
    resp.assert_contains("Detail: </p>"); // message should be empty in production
}

#[tokio::test]
async fn error_message_shown_in_dev() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page("404.html", "<h1>#error.status#</h1><p>#error.message#</p>");
    let (_dir, state) = proj.build_state_dev(); // dev mode
    let router = create_router(state);

    let resp = get(&router, "/missing").await;
    assert_eq!(resp.status, StatusCode::NOT_FOUND);
    resp.assert_contains("Page not found");
}