api_spa.rs
⎇
Raw
1//! SPA serving: static assets, client-route fallback, API 404s, method
2//! checks. Uses $FBNG_DIST (the dev-mode asset directory) via a tempdir so
3//! the test is independent of any built frontend.
4
5mod common;
6
7use axum::http::StatusCode;
8use common::*;
9
10#[tokio::test]
11async fn spa_fallback_and_api_guards() {
12 let env = Env::new().await;
13 let c = Client::new(env.app.clone());
14
15 // Unknown /api/ endpoints get a plain 404, not the SPA page.
16 let r = c.get("/api/unknown/endpoint").await;
17 assert_eq!(r.status, StatusCode::NOT_FOUND);
18 assert_eq!(r.text(), "unknown endpoint");
19
20 // Non-GET to a non-API path → 405.
21 let r = c
22 .raw(axum::http::Method::POST, "/some/page", &[], b"x".to_vec())
23 .await;
24 assert_eq!(r.status, StatusCode::METHOD_NOT_ALLOWED);
25
26 // Point the dev asset dir at a controlled tempdir.
27 //
28 // `FBNG_DIST` is process-global; only this test (the sole test in this
29 // binary) touches it, and other test binaries are separate processes.
30 let dist = tempfile::tempdir().unwrap();
31 std::fs::write(dist.path().join("index.html"), "DIST-INDEX").unwrap();
32 std::fs::write(dist.path().join("app.css"), "body{}").unwrap();
33 unsafe { std::env::set_var("FBNG_DIST", dist.path()) };
34
35 // Root serves index.html.
36 let r = c.get("/").await;
37 assert_eq!(r.status, StatusCode::OK);
38 assert_eq!(r.text(), "DIST-INDEX");
39 assert_eq!(r.header("content-type").as_deref(), Some("text/html"));
40 assert_eq!(r.header("cache-control").as_deref(), Some("no-cache"));
41
42 // Hard security headers on every response.
43 let csp = r.header("content-security-policy").unwrap();
44 assert!(csp.starts_with("default-src 'self'"), "CSP: {csp}");
45 assert!(csp.contains("frame-ancestors 'none'"), "CSP: {csp}");
46 assert_eq!(r.header("x-content-type-options").as_deref(), Some("nosniff"));
47 assert_eq!(r.header("x-frame-options").as_deref(), Some("DENY"));
48 assert_eq!(r.header("referrer-policy").as_deref(), Some("no-referrer"));
49
50 // A known asset is served with the right type.
51 let r = c.get("/app.css").await;
52 assert_eq!(r.status, StatusCode::OK);
53 assert_eq!(r.text(), "body{}");
54 assert_eq!(r.header("content-type").as_deref(), Some("text/css"));
55
56 // Unknown paths fall back to index.html (SPA client routes, deep links).
57 let r = c.get("/some/deep/client/route").await;
58 assert_eq!(r.status, StatusCode::OK);
59 assert_eq!(r.text(), "DIST-INDEX");
60 assert_eq!(r.header("cache-control").as_deref(), Some("no-cache"));
61 let r = c.get("/s/abc123token/deeper/path").await;
62 assert_eq!(r.status, StatusCode::OK);
63 assert_eq!(r.text(), "DIST-INDEX");
64
65 // Now with an *empty* dist dir → the friendly "build the frontend" hint.
66 let empty = tempfile::tempdir().unwrap();
67 unsafe { std::env::set_var("FBNG_DIST", empty.path()) };
68 let r = c.get("/").await;
69 assert_eq!(r.status, StatusCode::OK);
70 assert!(r.text().contains("frontend has not been built"));
71
72 unsafe { std::env::remove_var("FBNG_DIST") };
73}
74