embedded.rs
⎇
Raw
1//! End-to-end check for the production (embedded) build.
2//!
3//! With `--features embedded` the frontend is baked into the binary by
4//! rust-embed at *compile* time from `server/dist` (a copy of `web/dist`),
5//! so this test only makes sense after the frontend has been built and
6//! staged. Run it via `just e2e`, which does exactly that.
7#![cfg(feature = "embedded")]
8
9use crate::common::*;
10use axum::http::StatusCode;
11
12/// Absolute (`/...`) `src`/`href` asset references in the built index.html.
13fn asset_refs(html: &str) -> Vec<String> {
14 let mut refs = Vec::new();
15 let mut rest = html;
16 while let Some(pos) = rest.find('"') {
17 let value_start = pos + 1;
18 let Some(end) = rest[value_start..].find('"') else {
19 break;
20 };
21 let before = &rest[..pos];
22 let value = &rest[value_start..value_start + end];
23 if before.ends_with("src=") || before.ends_with("href=") {
24 if let Some(p) = value.strip_prefix('/') {
25 // Only real files (skip routes like `#`-anchors or bare paths).
26 if p.contains('.') && !p.contains(' ') {
27 refs.push(p.to_string());
28 }
29 }
30 }
31 rest = &rest[value_start + end + 1..]; // skip the closing quote
32 }
33 refs
34}
35
36#[tokio::test]
37async fn embedded_frontend_is_complete() {
38 let env = Env::new().await;
39 let c = Client::new(env.app.clone());
40
41 // index.html is the real built frontend, served no-cache.
42 let r = c.get("/").await;
43 assert_eq!(r.status, StatusCode::OK);
44 assert_eq!(r.header("cache-control").as_deref(), Some("no-cache"));
45 let html = r.text();
46 assert!(
47 html.contains("dovenest"),
48 "index.html does not look like the built frontend"
49 );
50
51 // Every asset index.html references must be embedded and servable.
52 let refs = asset_refs(&html);
53 assert!(
54 refs.iter().any(|r| r.ends_with(".wasm")),
55 "index.html must reference the wasm module; found: {refs:?}"
56 );
57 for ref_ in &refs {
58 let r = c.get(&format!("/{ref_}")).await;
59 assert_eq!(r.status, StatusCode::OK, "missing embedded asset: {ref_}");
60 assert!(!r.body.is_empty(), "empty embedded asset: {ref_}");
61
62 let ct = r.header("content-type").unwrap_or_default();
63 if ref_.ends_with(".js") {
64 assert!(
65 ct == "text/javascript" || ct == "application/javascript",
66 "unexpected JS content-type for {ref_}: {ct}"
67 );
68 } else {
69 let expected = match ref_.rsplit('.').next().unwrap_or("") {
70 "css" => "text/css",
71 "wasm" => "application/wasm",
72 "svg" => "image/svg+xml",
73 other => panic!("unexpected asset type in index.html: {other}"),
74 };
75 assert_eq!(ct, expected, "content-type for {ref_}");
76 }
77 // Hashed asset names are safe to cache forever (from disk: no).
78 assert_eq!(
79 r.header("cache-control").as_deref(),
80 Some("public, max-age=31536000, immutable"),
81 "cache-control for {ref_}"
82 );
83 // The ETag lets a repeat visit revalidate instead of downloading.
84 let etag = r
85 .header("etag")
86 .unwrap_or_else(|| panic!("etag for {ref_}"));
87 let r = c
88 .raw(
89 axum::http::Method::GET,
90 &format!("/{ref_}"),
91 &[("if-none-match", etag.as_str())],
92 Vec::new(),
93 )
94 .await;
95 assert_eq!(r.status, StatusCode::NOT_MODIFIED, "304 for {ref_}");
96 assert!(r.body.is_empty(), "304 body for {ref_}");
97 }
98
99 // Client routes fall back to index.html; unknown API paths stay 404.
100 let r = c.get("/some/client/route").await;
101 assert_eq!(r.status, StatusCode::OK);
102 assert!(r.text().contains("dovenest"));
103 let r = c.get("/api/unknown/endpoint").await;
104 assert_eq!(r.status, StatusCode::NOT_FOUND);
105}
106