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

#[tokio::test]
async fn create_item_via_post_form() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "todos.html",
        "<loop data=\"#todos#\" as=\"t\"><li>#t.title#</li></loop>",
    );
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    // Create item
    let resp = post_form(
        &router,
        "/w-action/todos?w-redirect=/todos",
        "title=Buy+milk",
    )
    .await;
    assert_eq!(resp.status, StatusCode::SEE_OTHER);
    assert_eq!(resp.location(), Some("/todos"));

    // Verify it exists
    let resp2 = get(&router, "/todos").await;
    assert_eq!(resp2.status, StatusCode::OK);
    resp2.assert_contains("Buy milk");
}

#[tokio::test]
async fn auto_generated_ids() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "todos.html",
        "<loop data=\"#todos#\" as=\"t\"><span>#t.id#</span></loop>",
    );
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    // Create item without explicit id
    post_form(&router, "/w-action/todos?w-redirect=/", "title=First").await;

    // Check that it got id=1
    let resp = get(&router, "/todos").await;
    resp.assert_contains(">1<");
}

#[tokio::test]
async fn sequential_ids() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "todos.html",
        "<loop data=\"#todos#\" as=\"t\"><span>#t.id#</span></loop>",
    );
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    post_form(&router, "/w-action/todos?w-redirect=/", "title=First").await;
    post_form(&router, "/w-action/todos?w-redirect=/", "title=Second").await;

    let resp = get(&router, "/todos").await;
    resp.assert_contains(">1<");
    resp.assert_contains(">2<");
}

#[tokio::test]
async fn collection_available_in_template_loop() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "todos.html",
        "<loop data=\"#todos#\" as=\"t\"><li>#t.title#</li></loop>",
    );
    let (_dir, state) = proj.build_state();
    let router = create_router(state);

    post_form(&router, "/w-action/todos?w-redirect=/", "title=Alpha").await;
    post_form(&router, "/w-action/todos?w-redirect=/", "title=Beta").await;

    let resp = get(&router, "/todos").await;
    resp.assert_contains("Alpha");
    resp.assert_contains("Beta");
}

#[tokio::test]
async fn update_merges_fields() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "todos.html",
        "<loop data=\"#todos#\" as=\"t\"><li>#t.title# - #t.status#</li></loop>",
    );
    let (_dir, state) = proj.build_state_with_config(open_collections_config(&["todos"]));
    let router = create_router(state);

    // Create item with title
    post_form(&router, "/w-action/todos?w-redirect=/", "title=Task").await;

    // Update with status field
    post_form(&router, "/w-action/todos/1?w-redirect=/", "status=done").await;

    let resp = get(&router, "/todos").await;
    resp.assert_contains("Task");
    resp.assert_contains("done");
}

#[tokio::test]
async fn update_preserves_unmodified_fields() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "todos.html",
        "<loop data=\"#todos#\" as=\"t\"><li>#t.title# by #t.author#</li></loop>",
    );
    let (_dir, state) = proj.build_state_with_config(open_collections_config(&["todos"]));
    let router = create_router(state);

    // Create with title + author
    post_form(
        &router,
        "/w-action/todos?w-redirect=/",
        "title=Original&author=Alice",
    )
    .await;

    // Update only title
    post_form(&router, "/w-action/todos/1?w-redirect=/", "title=Updated").await;

    let resp = get(&router, "/todos").await;
    resp.assert_contains("Updated");
    resp.assert_contains("Alice"); // author preserved
}

#[tokio::test]
async fn delete_removes_item() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "todos.html",
        "<loop data=\"#todos#\" as=\"t\"><li>#t.title#</li></loop>",
    );
    let (_dir, state) = proj.build_state_with_config(open_collections_config(&["todos"]));
    let router = create_router(state);

    // Create and then delete
    post_form(&router, "/w-action/todos?w-redirect=/", "title=Remove+me").await;
    post_form(
        &router,
        "/w-action/todos/1?w-action=delete&w-redirect=/",
        "",
    )
    .await;

    let resp = get(&router, "/todos").await;
    resp.assert_not_contains("Remove me");
}

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

    // Delete with non-existent id — should not crash, just redirect
    let resp = post_form(
        &router,
        "/w-action/todos/999?w-action=delete&w-redirect=/",
        "",
    )
    .await;
    assert_eq!(resp.status, StatusCode::SEE_OTHER);
}

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

    let resp = post_form(&router, "/w-action/todos?w-redirect=/success", "title=Test").await;
    assert_eq!(resp.status, StatusCode::SEE_OTHER);
    assert_eq!(resp.location(), Some("/success"));
}

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

    // No w-redirect param → should default to /
    let resp = post_form(&router, "/w-action/todos", "title=Test").await;
    assert_eq!(resp.status, StatusCode::SEE_OTHER);
    assert_eq!(resp.location(), Some("/"));
}

#[tokio::test]
async fn id_after_delete_no_collision() {
    let proj = TestProject::new();
    proj.add_page("index.html", "<h1>Home</h1>");
    proj.add_page(
        "todos.html",
        "<loop data=\"#todos#\" as=\"t\"><span>#t.id#</span></loop>",
    );
    let (_dir, state) = proj.build_state_with_config(open_collections_config(&["todos"]));
    let router = create_router(state);

    // Create 1, 2, 3
    post_form(&router, "/w-action/todos?w-redirect=/", "title=A").await;
    post_form(&router, "/w-action/todos?w-redirect=/", "title=B").await;
    post_form(&router, "/w-action/todos?w-redirect=/", "title=C").await;

    // Delete id=2
    post_form(
        &router,
        "/w-action/todos/2?w-action=delete&w-redirect=/",
        "",
    )
    .await;

    // Create new item — should get id=4 (not reuse id=2)
    post_form(&router, "/w-action/todos?w-redirect=/", "title=D").await;

    let resp = get(&router, "/todos").await;
    resp.assert_contains(">4<");
    resp.assert_not_contains(">2<");
}