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 serde_json::json;
use what_core::server::create_router;

#[tokio::test]
async fn delete_not_blocked_by_edit_form_validation() {
    // An edit form (w-validate, action /w-action/notes/1) registers that path as
    // requiring validation. A delete button posts to the SAME path
    // (/w-action/notes/1?w-action=delete) with no w-rules — it must NOT be
    // rejected as a validation bypass, since delete has no fields to validate.
    let proj = TestProject::new();
    proj.add_page(
        "edit.html",
        r##"<form w-validate action="/w-action/notes/1?w-redirect=/done"><input name="title" w-required></form>
<form method="post" action="/w-action/notes/1?w-action=delete&w-redirect=/done"><button>del</button></form>"##,
    );
    proj.add_page("done.html", "<h1>done</h1>");
    let config = open_collections_config(&["notes"]);
    let (_dir, state) = proj.build_state_with_config(config);
    state.store.create("notes", json!({ "title": "X" })).await.unwrap();
    let router = create_router(state);

    // Render the edit page → registers /w-action/notes/1 as validated.
    get(&router, "/edit").await;

    // Delete without w-rules must proceed (redirect to /done), not bounce to /edit.
    let resp = post_form_with_headers(
        &router,
        "/w-action/notes/1?w-action=delete&w-redirect=/done",
        "",
        vec![("referer", "/edit")],
    )
    .await;
    assert_eq!(resp.status, StatusCode::SEE_OTHER);
    assert_eq!(
        resp.location(),
        Some("/done"),
        "delete should proceed, not be rejected as a validation bypass"
    );
}

#[tokio::test]
async fn form_w_validate_injects_hidden_field() {
    let proj = TestProject::new();
    proj.add_page(
        "form.html",
        r#"<form w-validate action="/w-action/users?w-redirect=/form">
        <input type="text" name="email" w-required w-type="email">
        <input type="submit" value="Submit">
    </form>"#,
    );
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    let resp = get(&router, "/form").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_contains(r#"name="w-rules""#);
    resp.assert_contains(r#"type="hidden""#);
}

#[tokio::test]
async fn form_without_w_validate_no_hidden_field() {
    let proj = TestProject::new();
    proj.add_page(
        "form.html",
        r#"<form action="/w-action/users?w-redirect=/form">
        <input type="text" name="email" w-required>
        <input type="submit" value="Submit">
    </form>"#,
    );
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    let resp = get(&router, "/form").await;
    assert_eq!(resp.status, StatusCode::OK);
    resp.assert_not_contains(r#"name="w-rules""#);
}

#[tokio::test]
async fn validation_redirect_on_failure() {
    let proj = TestProject::new();
    // Create a form with required field validation
    proj.add_page(
        "form.html",
        r##"<form w-validate action="/w-action/users?w-redirect=/success">
        <input type="text" name="email" w-required w-type="email">
        <input type="submit" value="Submit">
    </form>
    <p>#errors.email|default:"no-error"#</p>"##,
    );
    proj.add_page("success.html", "<h1>Success</h1>");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    // First, render the form to get the w-rules token
    let form_resp = get(&router, "/form").await;
    let body = &form_resp.body;

    // Extract the w-rules token from the hidden field
    let token_start =
        body.find(r#"name="w-rules" value=""#).unwrap() + r#"name="w-rules" value=""#.len();
    let token_end = body[token_start..].find('"').unwrap() + token_start;
    let token = &body[token_start..token_end];

    // Submit with empty email (should fail validation)
    let form_data = format!("email=&w-rules={}", token);
    let resp = post_form_with_headers(
        &router,
        "/w-action/users?w-redirect=/success",
        &form_data,
        vec![("referer", "/form")],
    )
    .await;

    // Should redirect back to form (not to success)
    assert_eq!(resp.status, StatusCode::SEE_OTHER);
    assert_eq!(resp.location(), Some("/form"));
}

#[tokio::test]
async fn validation_passes_on_valid_data() {
    let proj = TestProject::new();
    proj.add_page(
        "form.html",
        r#"<form w-validate action="/w-action/users?w-redirect=/success">
        <input type="text" name="email" w-required w-type="email">
        <input type="submit">
    </form>"#,
    );
    proj.add_page("success.html", "<h1>Success</h1>");
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    // Get form to extract token
    let form_resp = get(&router, "/form").await;
    let body = &form_resp.body;
    let token_start =
        body.find(r#"name="w-rules" value=""#).unwrap() + r#"name="w-rules" value=""#.len();
    let token_end = body[token_start..].find('"').unwrap() + token_start;
    let token = &body[token_start..token_end];

    // Submit with valid email
    let form_data = format!("email=test@example.com&w-rules={}", token);
    let resp = post_form(&router, "/w-action/users?w-redirect=/success", &form_data).await;

    // Should redirect to success (validation passed)
    assert_eq!(resp.status, StatusCode::SEE_OTHER);
    assert_eq!(resp.location(), Some("/success"));
}

#[tokio::test]
async fn validation_errors_in_flash() {
    let proj = TestProject::new();
    proj.add_page(
        "form.html",
        r##"<form w-validate action="/w-action/users?w-redirect=/success">
        <input type="text" name="name" w-required>
        <input type="submit">
    </form>
    <p>error: #errors.name|default:"none"#</p>
    <p>has: #has_errors#</p>"##,
    );
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    // Get token
    let form_resp = get(&router, "/form").await;
    let body = &form_resp.body;
    let token_start =
        body.find(r#"name="w-rules" value=""#).unwrap() + r#"name="w-rules" value=""#.len();
    let token_end = body[token_start..].find('"').unwrap() + token_start;
    let token = &body[token_start..token_end];

    // Submit empty (fails validation)
    let form_data = format!("name=&w-rules={}", token);
    let resp = post_form_with_headers(
        &router,
        "/w-action/users?w-redirect=/success",
        &form_data,
        vec![("referer", "/form")],
    )
    .await;
    let cookie = resp.set_cookie().expect("should set session cookie");

    // Load form again — should see error
    let resp = get_with_headers(&router, "/form", vec![("cookie", cookie)]).await;
    resp.assert_contains("name is required");
    resp.assert_contains("has: true");
}