mockforge_ui/handlers/
assets.rs

1//! Static asset serving handlers
2//!
3//! This module handles serving static assets like HTML, CSS, and JavaScript
4//! files for the admin UI.
5
6use axum::{
7    extract::Path,
8    http::{self, StatusCode},
9    response::{Html, IntoResponse, Redirect},
10};
11use std::collections::HashMap;
12
13// Include the generated asset map from build.rs
14include!(concat!(env!("OUT_DIR"), "/asset_paths.rs"));
15
16/// Serve the main admin HTML page
17pub async fn serve_admin_html() -> Html<&'static str> {
18    Html(include_str!("../../ui/dist/index.html"))
19}
20
21/// Serve the admin CSS with proper content type
22pub async fn serve_admin_css() -> ([(http::HeaderName, &'static str); 1], &'static str) {
23    (
24        [(http::header::CONTENT_TYPE, "text/css")],
25        include_str!("../../ui/dist/assets/index.css"),
26    )
27}
28
29/// Serve the admin JavaScript with proper content type
30pub async fn serve_admin_js() -> ([(http::HeaderName, &'static str); 1], &'static str) {
31    (
32        [(http::header::CONTENT_TYPE, "application/javascript")],
33        include_str!("../../ui/dist/assets/index.js"),
34    )
35}
36
37/// Serve vendor JavaScript files dynamically
38/// This handler uses a build-time generated asset map that includes all files
39/// from the assets directory, so it automatically handles files with changing hashes
40pub async fn serve_vendor_asset(Path(filename): Path<String>) -> impl IntoResponse {
41    // Determine content type based on file extension
42    let content_type = if filename.ends_with(".js") {
43        "application/javascript"
44    } else if filename.ends_with(".css") {
45        "text/css"
46    } else if filename.ends_with(".png") {
47        "image/png"
48    } else if filename.ends_with(".svg") {
49        "image/svg+xml"
50    } else if filename.ends_with(".woff") || filename.ends_with(".woff2") {
51        "font/woff2"
52    } else {
53        "application/octet-stream"
54    };
55
56    // Look up the asset in the dynamically generated map (built at compile time)
57    let asset_map = get_asset_map();
58    if let Some(content) = asset_map.get(filename.as_str()) {
59        ([(http::header::CONTENT_TYPE, content_type)], *content).into_response()
60    } else {
61        // Return 404 for unknown files
62        (
63            StatusCode::NOT_FOUND,
64            [(http::header::CONTENT_TYPE, "text/plain")],
65            "Asset not found",
66        )
67            .into_response()
68    }
69}
70
71/// Serve icon files
72pub async fn serve_icon() -> impl IntoResponse {
73    // Return a simple SVG icon or placeholder
74    let icon_svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\"><rect width=\"32\" height=\"32\" fill=\"#4f46e5\"/><text x=\"16\" y=\"20\" text-anchor=\"middle\" fill=\"white\" font-family=\"Arial\" font-size=\"14\">MF</text></svg>";
75    ([(http::header::CONTENT_TYPE, "image/svg+xml")], icon_svg)
76}
77
78/// Serve 32x32 icon
79pub async fn serve_icon_32() -> impl IntoResponse {
80    serve_icon().await
81}
82
83/// Serve 48x48 icon
84pub async fn serve_icon_48() -> impl IntoResponse {
85    serve_icon().await
86}
87
88/// Serve logo files
89pub async fn serve_logo() -> impl IntoResponse {
90    serve_icon().await
91}
92
93/// Serve 40x40 logo
94pub async fn serve_logo_40() -> impl IntoResponse {
95    serve_icon().await
96}
97
98/// Serve 80x80 logo
99pub async fn serve_logo_80() -> impl IntoResponse {
100    serve_icon().await
101}
102
103/// Serve the API documentation - redirects to the book
104pub async fn serve_api_docs() -> impl IntoResponse {
105    // Redirect to the comprehensive documentation in the book
106    Redirect::permanent("https://docs.mockforge.dev/api/admin-ui-rest.html")
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[tokio::test]
114    async fn test_serve_admin_html() {
115        let html = serve_admin_html().await;
116        let html_str = html.0;
117        assert!(!html_str.is_empty());
118        assert!(html_str.contains("<!DOCTYPE html>") || html_str.contains("<html"));
119    }
120
121    #[tokio::test]
122    async fn test_serve_admin_css() {
123        let (headers, css) = serve_admin_css().await;
124        assert_eq!(headers[0].0, http::header::CONTENT_TYPE);
125        assert_eq!(headers[0].1, "text/css");
126        assert!(!css.is_empty());
127    }
128
129    #[tokio::test]
130    async fn test_serve_admin_js() {
131        let (headers, js) = serve_admin_js().await;
132        assert_eq!(headers[0].0, http::header::CONTENT_TYPE);
133        assert_eq!(headers[0].1, "application/javascript");
134        assert!(!js.is_empty());
135    }
136
137    #[tokio::test]
138    async fn test_serve_icon() {
139        let response = serve_icon().await;
140        // Icon returns SVG content - we can't easily check headers in impl IntoResponse
141        // but we can verify it returns successfully
142        let _ = response;
143    }
144
145    #[tokio::test]
146    async fn test_serve_icon_32() {
147        let _ = serve_icon_32().await;
148    }
149
150    #[tokio::test]
151    async fn test_serve_icon_48() {
152        let _ = serve_icon_48().await;
153    }
154
155    #[tokio::test]
156    async fn test_serve_logo() {
157        let _ = serve_logo().await;
158    }
159
160    #[tokio::test]
161    async fn test_serve_logo_40() {
162        let _ = serve_logo_40().await;
163    }
164
165    #[tokio::test]
166    async fn test_serve_logo_80() {
167        let _ = serve_logo_80().await;
168    }
169
170    #[tokio::test]
171    async fn test_serve_api_docs() {
172        let _ = serve_api_docs().await;
173        // Redirect can't be easily tested without request context
174        // but we verify it compiles and runs
175    }
176}