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