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