what-core 1.7.5

Core framework for What - an HTML-first web framework powered by Rust
Documentation
//! Concurrency stress tests — validates thread safety under parallel load.
//!
//! Each test uses `multi_thread` with 4 workers and dispatches many concurrent
//! requests via `tokio::spawn` + `futures::future::join_all`.

mod common;

use axum::http::StatusCode;
use common::{TestProject, get, get_with_headers, post_form_with_headers};
use futures::future::join_all;
use std::collections::HashSet;
use what_core::Config;

/// Helper: config with rate limiting disabled (stress tests exceed default limits)
fn no_rate_limit_config() -> Config {
    let mut config = Config::default();
    config.rate_limit.enabled = false;
    config
}

/// 150 concurrent GET requests to the same page — all must return 200.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_page_renders() {
    let project = TestProject::new();
    project.add_page("index.html", "<h1>Hello</h1><p>World</p>");
    let (_dir, state) = project.build_state();
    let router = what_core::server::create_router(state);

    let tasks: Vec<_> = (0..150)
        .map(|_| {
            let r = router.clone();
            tokio::spawn(async move { get(&r, "/").await })
        })
        .collect();

    let results = join_all(tasks).await;
    for result in results {
        let resp = result.expect("task panicked");
        assert_eq!(resp.status, StatusCode::OK);
        resp.assert_contains("Hello");
    }
}

/// 150 concurrent first-visit requests — each must get a unique session ID.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_session_creation() {
    let project = TestProject::new();
    project.add_page("index.html", "<p>#session.id#</p>");
    let (_dir, state) = project.build_state();
    let router = what_core::server::create_router(state);

    let tasks: Vec<_> = (0..150)
        .map(|_| {
            let r = router.clone();
            tokio::spawn(async move { get(&r, "/").await })
        })
        .collect();

    let results = join_all(tasks).await;
    let mut session_ids = HashSet::new();

    for result in results {
        let resp = result.expect("task panicked");
        assert_eq!(resp.status, StatusCode::OK);

        // Extract session ID from Set-Cookie header
        let cookie = resp.set_cookie().expect("expected Set-Cookie header");
        let id = cookie
            .split('=')
            .nth(1)
            .unwrap()
            .split(';')
            .next()
            .unwrap()
            .to_string();
        session_ids.insert(id);
    }

    assert_eq!(session_ids.len(), 150, "All 150 sessions must be unique");
}

/// 60 concurrent increments on a single session — final counter must equal 60.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_session_increment() {
    let project = TestProject::new();
    project.add_page("index.html", "<p>count=#session.counter#</p>");
    let (_dir, state) = project.build_state_with_config(no_rate_limit_config());
    let router = what_core::server::create_router(state);

    // First request to establish a session and get CSRF token
    let init = get(&router, "/").await;
    assert_eq!(init.status, StatusCode::OK);
    let cookie = init.set_cookie().expect("expected Set-Cookie");
    let session_cookie = cookie.split(';').next().unwrap().to_string();
    let csrf = init.csrf_token().expect("expected CSRF token");

    // 60 concurrent increments on the same session
    let tasks: Vec<_> = (0..60)
        .map(|_| {
            let r = router.clone();
            let c = session_cookie.clone();
            let t = csrf.clone();
            tokio::spawn(async move {
                post_form_with_headers(
                    &r,
                    "/w-set",
                    "expr=session.counter%20%2B%3D%201",
                    vec![
                        ("cookie", &c),
                        ("X-CSRF-Token", &t),
                        ("X-Requested-With", "What"),
                        ("content-type", "application/x-www-form-urlencoded"),
                    ],
                )
                .await
            })
        })
        .collect();

    let results = join_all(tasks).await;
    for result in results {
        let resp = result.expect("task panicked");
        assert_eq!(resp.status, StatusCode::OK, "w-set failed: {}", resp.body);
    }

    // Verify final count
    let final_resp = get_with_headers(&router, "/", vec![("cookie", &session_cookie)]).await;
    assert_eq!(final_resp.status, StatusCode::OK);
    // Session variables are wrapped with reactive spans: <span w-bind="session.counter">60</span>
    final_resp.assert_contains(">60</span>");
}

/// 90 concurrent creates to the same collection — all 90 items must exist.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_crud_creates() {
    let project = TestProject::new();
    project.add_page(
        "index.html",
        r##"<what>
fetch.items = "local:items"
</what>
<loop data="#items#" as="item"><p>#item.title#</p></loop>"##,
    );
    let (_dir, state) = project.build_state_with_config(no_rate_limit_config());
    let router = what_core::server::create_router(state);

    // Get CSRF token
    let init = get(&router, "/").await;
    let cookie = init.set_cookie().unwrap();
    let session_cookie = cookie.split(';').next().unwrap().to_string();
    let csrf = init.csrf_token().expect("expected CSRF token");

    // 90 concurrent creates
    let tasks: Vec<_> = (0..90)
        .map(|i| {
            let r = router.clone();
            let c = session_cookie.clone();
            let t = csrf.clone();
            let body = format!("title=item-{}", i);
            tokio::spawn(async move {
                post_form_with_headers(
                    &r,
                    "/w-action/items?w-action=create&w-redirect=/",
                    &body,
                    vec![("cookie", &c), ("X-CSRF-Token", &t)],
                )
                .await
            })
        })
        .collect();

    let results = join_all(tasks).await;
    for result in results {
        let resp = result.expect("task panicked");
        // Should redirect on success (303 See Other)
        assert_eq!(
            resp.status,
            StatusCode::SEE_OTHER,
            "create failed: {}",
            resp.body
        );
    }

    // Verify all 90 items exist
    let final_resp = get_with_headers(&router, "/", vec![("cookie", &session_cookie)]).await;
    for i in 0..90 {
        final_resp.assert_contains(&format!("item-{}", i));
    }
}

/// Mixed concurrent operations: creates + updates + deletes on the same collection.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_crud_mixed() {
    let project = TestProject::new();
    project.add_page(
        "index.html",
        r##"<what>
fetch.items = "local:items"
</what>
<loop data="#items#" as="item"><p class="item">#item.id#:#item.title#</p></loop>"##,
    );
    let (_dir, state) = project.build_state_with_config(no_rate_limit_config());
    let router = what_core::server::create_router(state);

    // Get CSRF token
    let init = get(&router, "/").await;
    let cookie = init.set_cookie().unwrap();
    let session_cookie = cookie.split(';').next().unwrap().to_string();
    let csrf = init.csrf_token().expect("expected CSRF token");

    // Create 30 items sequentially (so we have known IDs)
    let mut item_ids: Vec<String> = Vec::new();
    for i in 0..30 {
        let body = format!("title=seed-{}", i);
        post_form_with_headers(
            &router,
            "/w-action/items?w-action=create&w-redirect=/",
            &body,
            vec![("cookie", &session_cookie), ("X-CSRF-Token", &csrf)],
        )
        .await;
    }

    // Read back to get IDs
    let page = get_with_headers(&router, "/", vec![("cookie", &session_cookie)]).await;
    for line in page.body.lines() {
        if line.contains("class=\"item\"") {
            // Extract ID from "ID:title" pattern
            if let Some(start) = line.find('>') {
                let content = &line[start + 1..];
                if let Some(end) = content.find(':') {
                    let id = content[..end].trim();
                    if !id.is_empty() {
                        item_ids.push(id.to_string());
                    }
                }
            }
        }
    }
    assert_eq!(
        item_ids.len(),
        30,
        "Expected 30 seed items, got {}",
        item_ids.len()
    );

    // Concurrently: 30 new creates + 15 updates + 15 deletes = 60 parallel ops
    let mut tasks = Vec::new();

    // 30 creates
    for i in 0..30 {
        let r = router.clone();
        let c = session_cookie.clone();
        let t = csrf.clone();
        let body = format!("title=new-{}", i);
        tasks.push(tokio::spawn(async move {
            post_form_with_headers(
                &r,
                "/w-action/items?w-action=create&w-redirect=/",
                &body,
                vec![("cookie", &c), ("X-CSRF-Token", &t)],
            )
            .await
        }));
    }

    // 15 updates (first 15 seed items)
    for id in item_ids.iter().take(15) {
        let r = router.clone();
        let c = session_cookie.clone();
        let t = csrf.clone();
        let url = format!("/w-action/items/{}?w-action=update&w-redirect=/", id);
        tasks.push(tokio::spawn(async move {
            post_form_with_headers(
                &r,
                &url,
                "title=updated",
                vec![("cookie", &c), ("X-CSRF-Token", &t)],
            )
            .await
        }));
    }

    // 15 deletes (last 15 seed items)
    for id in item_ids.iter().skip(15) {
        let r = router.clone();
        let c = session_cookie.clone();
        let t = csrf.clone();
        let url = format!("/w-action/items/{}?w-action=delete&w-redirect=/", id);
        tasks.push(tokio::spawn(async move {
            post_form_with_headers(&r, &url, "", vec![("cookie", &c), ("X-CSRF-Token", &t)]).await
        }));
    }

    let results = join_all(tasks).await;
    for result in results {
        let resp = result.expect("task panicked");
        assert_eq!(
            resp.status,
            StatusCode::SEE_OTHER,
            "action failed: {}",
            resp.body
        );
    }

    // Verify: 15 original (updated) + 30 new = 45 total items
    let final_resp = get_with_headers(&router, "/", vec![("cookie", &session_cookie)]).await;

    let item_count = final_resp.body.matches("class=\"item\"").count();
    assert_eq!(
        item_count, 45,
        "Expected 45 items (15 updated + 30 new), got {}",
        item_count
    );

    // Verify the 15 updates took effect
    assert_eq!(
        final_resp.body.matches(":updated</p>").count(),
        15,
        "Expected 15 items with 'updated' title"
    );
}

/// 60 concurrent GET requests to a partial — all must return 200 with HTML.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_partial_renders() {
    let project = TestProject::new();
    project.add_page("index.html", "<p>main</p>");

    // Create partials directory inside content dir and add a partial
    let partials_dir = project.root().join("site").join("partials");
    std::fs::create_dir_all(&partials_dir).unwrap();
    std::fs::write(
        partials_dir.join("items.html"),
        r##"<what>
fetch.items = "local:items"
</what>
<loop data="#items#" as="item"><li>#item.title#</li></loop>
<unless cond="#items#"><p>No items</p></unless>"##,
    )
    .unwrap();

    let (_dir, state) = project.build_state();
    let router = what_core::server::create_router(state);

    let tasks: Vec<_> = (0..60)
        .map(|_| {
            let r = router.clone();
            tokio::spawn(async move { get(&r, "/w-partial/items").await })
        })
        .collect();

    let results = join_all(tasks).await;
    for result in results {
        let resp = result.expect("task panicked");
        assert_eq!(resp.status, StatusCode::OK);
        resp.assert_contains("No items");
    }
}