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;
fn no_rate_limit_config() -> Config {
let mut config = Config::default();
config.rate_limit.enabled = false;
config
}
#[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");
}
}
#[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);
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");
}
#[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);
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");
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);
}
let final_resp = get_with_headers(&router, "/", vec![("cookie", &session_cookie)]).await;
assert_eq!(final_resp.status, StatusCode::OK);
final_resp.assert_contains(">60</span>");
}
#[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);
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");
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");
assert_eq!(
resp.status,
StatusCode::SEE_OTHER,
"create failed: {}",
resp.body
);
}
let final_resp = get_with_headers(&router, "/", vec![("cookie", &session_cookie)]).await;
for i in 0..90 {
final_resp.assert_contains(&format!("item-{}", i));
}
}
#[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);
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");
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;
}
let page = get_with_headers(&router, "/", vec![("cookie", &session_cookie)]).await;
for line in page.body.lines() {
if line.contains("class=\"item\"") {
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()
);
let mut tasks = Vec::new();
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
}));
}
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
}));
}
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
);
}
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
);
assert_eq!(
final_resp.body.matches(":updated</p>").count(),
15,
"Expected 15 items with 'updated' title"
);
}
#[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>");
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");
}
}