brewlog/tests/server/stats_api.rs
Jon Seager 5820c162f6
fix: address static assets code review findings
- Remove dead CSS color property in .tab-mobile
- Fix design token violations (text-red-500, fallback colors)
- Add disconnectedCallback to chip-scroll, image-upload, searchable-select
- Replace innerHTML with safe DOM APIs in world-map screen reader table
- Add credentials: same-origin to WebAuthn fetch calls
- Move page-specific scripts (donut-chart, location) out of base.html
- Add client-side image resizing (1920px max dimension)
- Resize app-icon-512.png from 2048x2048 to 512x512
- Document Datastar unsafe-eval CSP requirement
- Add static asset serving tests (16 routes)
- Add e2e tests for world-map, donut-chart, chip-scroll presence
- Add cache-busting query params to all static asset URLs
2026-02-13 17:13:02 +00:00

259 lines
7.2 KiB
Rust

use reqwest::Client;
use crate::helpers::{
assert_datastar_headers_with_mode, assert_full_page, assert_html_fragment, create_default_brew,
create_default_cafe, create_default_roast, create_default_roaster, spawn_app,
spawn_app_with_auth,
};
#[tokio::test]
async fn stats_page_returns_200_with_empty_database() {
let app = spawn_app().await;
let client = Client::new();
let response = client
.get(app.page_url("/stats"))
.send()
.await
.expect("Failed to execute request");
assert_eq!(response.status(), 200);
let body = response.text().await.expect("Failed to read body");
assert_full_page(&body);
assert!(body.contains("Stats"), "Page should contain title");
}
#[tokio::test]
async fn stats_page_returns_200_with_data() {
let app = spawn_app_with_auth().await;
let roaster = create_default_roaster(&app).await;
let _roast = create_default_roast(&app, roaster.id).await;
let client = Client::new();
let response = client
.get(app.page_url("/stats"))
.send()
.await
.expect("Failed to execute request");
assert_eq!(response.status(), 200);
let body = response.text().await.expect("Failed to read body");
assert_full_page(&body);
assert!(
body.contains("Ethiopia"),
"Stats page should contain roast origin"
);
}
#[tokio::test]
async fn stats_page_datastar_tab_switch_returns_fragment() {
let app = spawn_app().await;
let client = Client::new();
let response = client
.get(app.page_url("/stats?type=roasts"))
.header("datastar-request", "true")
.send()
.await
.expect("Failed to execute request");
assert_eq!(response.status(), 200);
assert_datastar_headers_with_mode(&response, "#stats-content", "inner");
let body = response.text().await.expect("Failed to read body");
assert_html_fragment(&body);
}
#[tokio::test]
async fn recompute_stats_requires_authentication() {
let app = spawn_app().await;
let client = Client::new();
let response = client
.post(app.api_url("/stats/recompute"))
.send()
.await
.expect("Failed to execute request");
assert_eq!(response.status(), 401);
}
#[tokio::test]
async fn recompute_stats_returns_json() {
let app = spawn_app_with_auth().await;
let client = Client::new();
let response = client
.post(app.api_url("/stats/recompute"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.send()
.await
.expect("Failed to execute request");
assert_eq!(response.status(), 200);
let body: serde_json::Value = response.json().await.expect("Failed to parse JSON");
assert!(
body.get("computed_at").is_some(),
"Response should contain computed_at"
);
assert!(
body.get("roast_summary").is_some(),
"Response should contain roast_summary"
);
assert!(
body.get("consumption").is_some(),
"Response should contain consumption"
);
assert!(
body.get("brewing_summary").is_some(),
"Response should contain brewing_summary"
);
}
#[tokio::test]
async fn recompute_stats_reflects_created_data() {
let app = spawn_app_with_auth().await;
let roaster = create_default_roaster(&app).await;
let _roast = create_default_roast(&app, roaster.id).await;
let _cafe = create_default_cafe(&app).await;
let client = Client::new();
let response = client
.post(app.api_url("/stats/recompute"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.send()
.await
.expect("Failed to execute request");
assert_eq!(response.status(), 200);
let body: serde_json::Value = response.json().await.expect("Failed to parse JSON");
// Roaster is in UK
let geo_roasters = &body["geo_roasters"]["entries"];
assert!(
geo_roasters
.as_array()
.unwrap()
.iter()
.any(|e| e["country_name"] == "UK"),
"geo_roasters should contain UK: {geo_roasters}"
);
// Roast origin is Ethiopia
let geo_roasts = &body["geo_roasts"]["entries"];
assert!(
geo_roasts
.as_array()
.unwrap()
.iter()
.any(|e| e["country_name"] == "Ethiopia"),
"geo_roasts should contain Ethiopia: {geo_roasts}"
);
// Cafe is in US
let geo_cafes = &body["geo_cafes"]["entries"];
assert!(
geo_cafes
.as_array()
.unwrap()
.iter()
.any(|e| e["country_name"] == "US"),
"geo_cafes should contain US: {geo_cafes}"
);
}
#[tokio::test]
async fn stats_page_contains_world_map_with_geo_data() {
let app = spawn_app_with_auth().await;
let client = Client::new();
let roaster = create_default_roaster(&app).await;
let _roast = create_default_roast(&app, roaster.id).await;
// Populate the cache so the stats page has data
let recompute = client
.post(app.api_url("/stats/recompute"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.send()
.await
.expect("Failed to recompute");
assert_eq!(recompute.status(), 200);
// Fetch the roasts tab fragment which contains the world-map
let response = client
.get(app.page_url("/stats?type=roasts"))
.header("datastar-request", "true")
.send()
.await
.expect("Failed to execute request");
assert_eq!(response.status(), 200);
let body = response.text().await.expect("Failed to read body");
assert!(
body.contains("<world-map"),
"Roasts tab should contain world-map component"
);
}
#[tokio::test]
async fn stats_page_contains_donut_chart_with_brew_data() {
let app = spawn_app_with_auth().await;
let client = Client::new();
let _brew = create_default_brew(&app).await;
// Populate the cache
let recompute = client
.post(app.api_url("/stats/recompute"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.send()
.await
.expect("Failed to recompute");
assert_eq!(recompute.status(), 200);
let response = client
.get(app.page_url("/stats"))
.send()
.await
.expect("Failed to execute request");
assert_eq!(response.status(), 200);
let body = response.text().await.expect("Failed to read body");
assert!(
body.contains("<donut-chart"),
"Stats page should contain donut-chart component when brew data exists"
);
}
#[tokio::test]
async fn stats_page_loads_after_recompute() {
let app = spawn_app_with_auth().await;
let client = Client::new();
// Populate the cache
let recompute_response = client
.post(app.api_url("/stats/recompute"))
.bearer_auth(app.auth_token.as_ref().unwrap())
.send()
.await
.expect("Failed to recompute");
assert_eq!(recompute_response.status(), 200);
// Load the page — should read from cache
let response = client
.get(app.page_url("/stats"))
.send()
.await
.expect("Failed to execute request");
assert_eq!(response.status(), 200);
let body = response.text().await.expect("Failed to read body");
assert_full_page(&body);
}