//! End-to-end check for the production (embedded) build. //! //! With `--features embedded` the frontend is baked into the binary by //! rust-embed at *compile* time from `server/dist` (a copy of `web/dist`), //! so this test only makes sense after the frontend has been built and //! staged. Run it via `just e2e`, which does exactly that. #![cfg(feature = "embedded")] use crate::common::*; use axum::http::StatusCode; /// Absolute (`/...`) `src`/`href` asset references in the built index.html. fn asset_refs(html: &str) -> Vec { let mut refs = Vec::new(); let mut rest = html; while let Some(pos) = rest.find('"') { let value_start = pos + 1; let Some(end) = rest[value_start..].find('"') else { break; }; let before = &rest[..pos]; let value = &rest[value_start..value_start + end]; if before.ends_with("src=") || before.ends_with("href=") { if let Some(p) = value.strip_prefix('/') { // Only real files (skip routes like `#`-anchors or bare paths). if p.contains('.') && !p.contains(' ') { refs.push(p.to_string()); } } } rest = &rest[value_start + end + 1..]; // skip the closing quote } refs } #[tokio::test] async fn embedded_frontend_is_complete() { let env = Env::new().await; let c = Client::new(env.app.clone()); // index.html is the real built frontend, served no-cache. let r = c.get("/").await; assert_eq!(r.status, StatusCode::OK); assert_eq!(r.header("cache-control").as_deref(), Some("no-cache")); let html = r.text(); assert!( html.contains("dovenest"), "index.html does not look like the built frontend" ); // Every asset index.html references must be embedded and servable. let refs = asset_refs(&html); assert!( refs.iter().any(|r| r.ends_with(".wasm")), "index.html must reference the wasm module; found: {refs:?}" ); for ref_ in &refs { let r = c.get(&format!("/{ref_}")).await; assert_eq!(r.status, StatusCode::OK, "missing embedded asset: {ref_}"); assert!(!r.body.is_empty(), "empty embedded asset: {ref_}"); let ct = r.header("content-type").unwrap_or_default(); if ref_.ends_with(".js") { assert!( ct == "text/javascript" || ct == "application/javascript", "unexpected JS content-type for {ref_}: {ct}" ); } else { let expected = match ref_.rsplit('.').next().unwrap_or("") { "css" => "text/css", "wasm" => "application/wasm", "svg" => "image/svg+xml", other => panic!("unexpected asset type in index.html: {other}"), }; assert_eq!(ct, expected, "content-type for {ref_}"); } // Hashed asset names are safe to cache forever (from disk: no). assert_eq!( r.header("cache-control").as_deref(), Some("public, max-age=31536000, immutable"), "cache-control for {ref_}" ); // The ETag lets a repeat visit revalidate instead of downloading. let etag = r .header("etag") .unwrap_or_else(|| panic!("etag for {ref_}")); let r = c .raw( axum::http::Method::GET, &format!("/{ref_}"), &[("if-none-match", etag.as_str())], Vec::new(), ) .await; assert_eq!(r.status, StatusCode::NOT_MODIFIED, "304 for {ref_}"); assert!(r.body.is_empty(), "304 body for {ref_}"); } // Client routes fall back to index.html; unknown API paths stay 404. let r = c.get("/some/client/route").await; assert_eq!(r.status, StatusCode::OK); assert!(r.text().contains("dovenest")); let r = c.get("/api/unknown/endpoint").await; assert_eq!(r.status, StatusCode::NOT_FOUND); }