Skip to main content

allowthem_server/
static_routes.rs

1//! Static-asset routes for AllowThem-owned browser JS.
2//!
3//! Mounted at `/__allowthem/static/` in both embedded and standalone modes.
4//! All assets are embedded in the binary via `include_bytes!` so integrators
5//! don't need to vendor anything themselves.
6
7use axum::Router;
8use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
9use axum::response::IntoResponse;
10use axum::routing::get;
11
12const MODE_TOGGLE_JS: &[u8] = include_bytes!("assets/static/js/mode-toggle.js");
13const SHADER_ASCII_JS: &[u8] = include_bytes!("assets/static/js/shader-ascii.js");
14
15/// Cache-Control value for all static assets.
16///
17/// Files are served with fixed filenames, so we keep the TTL modest.
18/// A future milestone can switch to content-hashed filenames + immutable.
19const CACHE_CONTROL: &str = "public, max-age=1";
20
21/// Build the static-asset router. Mount it at `/__allowthem/static/`.
22pub fn router() -> Router {
23    Router::new()
24        .route(
25            "/__allowthem/static/js/mode-toggle.js",
26            get(|| asset(MODE_TOGGLE_JS, "application/javascript; charset=utf-8")),
27        )
28        .route(
29            "/__allowthem/static/js/shader-ascii.js",
30            get(|| asset(SHADER_ASCII_JS, "application/javascript; charset=utf-8")),
31        )
32}
33
34async fn asset(bytes: &'static [u8], content_type: &'static str) -> impl IntoResponse {
35    let mut headers = HeaderMap::new();
36    headers.insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));
37    headers.insert(
38        header::CACHE_CONTROL,
39        HeaderValue::from_static(CACHE_CONTROL),
40    );
41    (StatusCode::OK, headers, bytes)
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use axum::body::Body;
48    use axum::http::{Request, StatusCode};
49    use tower::ServiceExt;
50
51    #[tokio::test]
52    async fn unknown_asset_returns_404() {
53        let app = router();
54        let res = app
55            .oneshot(
56                Request::builder()
57                    .uri("/__allowthem/static/css/nonexistent.css")
58                    .body(Body::empty())
59                    .unwrap(),
60            )
61            .await
62            .unwrap();
63        assert_eq!(res.status(), StatusCode::NOT_FOUND);
64    }
65
66    #[tokio::test]
67    async fn copied_wavefunk_assets_are_not_served() {
68        let paths = [
69            "/__allowthem/static/css/01-tokens.css",
70            "/__allowthem/static/css/03-layout.css",
71            "/__allowthem/static/fonts/MartianGrotesk-VF.woff2",
72            "/__allowthem/static/js/echo.js",
73        ];
74        for path in paths {
75            let app = router();
76            let res = app
77                .oneshot(Request::builder().uri(path).body(Body::empty()).unwrap())
78                .await
79                .unwrap();
80            assert_eq!(res.status(), StatusCode::NOT_FOUND, "{path} should be gone");
81        }
82    }
83
84    #[tokio::test]
85    async fn serves_mode_toggle_js() {
86        let app = router();
87        let res = app
88            .oneshot(
89                Request::builder()
90                    .uri("/__allowthem/static/js/mode-toggle.js")
91                    .body(Body::empty())
92                    .unwrap(),
93            )
94            .await
95            .unwrap();
96        assert_eq!(res.status(), StatusCode::OK);
97        let ct = res.headers().get("content-type").unwrap();
98        assert_eq!(ct, "application/javascript; charset=utf-8");
99    }
100
101    #[tokio::test]
102    async fn serves_shader_ascii_js() {
103        let app = router();
104        let res = app
105            .oneshot(
106                Request::builder()
107                    .uri("/__allowthem/static/js/shader-ascii.js")
108                    .body(Body::empty())
109                    .unwrap(),
110            )
111            .await
112            .unwrap();
113        assert_eq!(res.status(), StatusCode::OK);
114        let ct = res.headers().get("content-type").unwrap();
115        assert_eq!(ct, "application/javascript; charset=utf-8");
116    }
117}