mockforge-ui 0.3.135

Admin UI for MockForge - web-based interface for managing mock servers
//! Static asset serving handlers
//!
//! This module handles serving static assets like HTML, CSS, and JavaScript
//! files for the admin UI.

use axum::{
    extract::Path,
    http::{self, StatusCode},
    response::{Html, IntoResponse, Redirect},
};

// Include the generated asset map from build.rs
include!(concat!(env!("OUT_DIR"), "/asset_paths.rs"));

/// Serve the main admin HTML page
pub async fn serve_admin_html() -> Html<&'static str> {
    Html(crate::get_admin_html())
}

/// Serve the admin CSS with proper content type
pub async fn serve_admin_css() -> ([(http::HeaderName, &'static str); 1], &'static str) {
    ([(http::header::CONTENT_TYPE, "text/css")], crate::get_admin_css())
}

/// Serve the admin JavaScript with proper content type
pub async fn serve_admin_js() -> ([(http::HeaderName, &'static str); 1], &'static str) {
    ([(http::header::CONTENT_TYPE, "application/javascript")], crate::get_admin_js())
}

/// Serve vendor JavaScript files dynamically
/// This handler uses a build-time generated asset map that includes all files
/// from the assets directory, so it automatically handles files with changing hashes
pub async fn serve_vendor_asset(Path(filename): Path<String>) -> impl IntoResponse {
    // Determine content type based on file extension
    let content_type = if filename.ends_with(".js") {
        "application/javascript"
    } else if filename.ends_with(".css") {
        "text/css"
    } else if filename.ends_with(".png") {
        "image/png"
    } else if filename.ends_with(".svg") {
        "image/svg+xml"
    } else if filename.ends_with(".woff") || filename.ends_with(".woff2") {
        "font/woff2"
    } else {
        "application/octet-stream"
    };

    // Look up the asset in the dynamically generated map (built at compile time)
    let asset_map = get_asset_map();
    if let Some(content) = asset_map.get(filename.as_str()) {
        ([(http::header::CONTENT_TYPE, content_type)], *content).into_response()
    } else {
        // Return 404 for unknown files
        (
            StatusCode::NOT_FOUND,
            [(http::header::CONTENT_TYPE, "text/plain")],
            "Asset not found",
        )
            .into_response()
    }
}

// Embedded icon/logo assets to avoid 404s/fallbacks in the admin UI
// These are generated by build.rs to handle both development and published crate scenarios
include!(concat!(env!("OUT_DIR"), "/icon_assets.rs"));

/// Serve icon files
pub async fn serve_icon() -> impl IntoResponse {
    ([(http::header::CONTENT_TYPE, "image/png")], ICON_DEFAULT)
}

/// Serve 32x32 icon
pub async fn serve_icon_32() -> impl IntoResponse {
    ([(http::header::CONTENT_TYPE, "image/png")], ICON_32)
}

/// Serve 48x48 icon
pub async fn serve_icon_48() -> impl IntoResponse {
    ([(http::header::CONTENT_TYPE, "image/png")], ICON_48)
}

/// Serve logo files
pub async fn serve_logo() -> impl IntoResponse {
    ([(http::header::CONTENT_TYPE, "image/png")], ICON_DEFAULT)
}

/// Serve 40x40 logo
pub async fn serve_logo_40() -> impl IntoResponse {
    ([(http::header::CONTENT_TYPE, "image/png")], LOGO_40)
}

/// Serve 80x80 logo
pub async fn serve_logo_80() -> impl IntoResponse {
    ([(http::header::CONTENT_TYPE, "image/png")], LOGO_80)
}

/// Serve the API documentation - redirects to the book
pub async fn serve_api_docs() -> impl IntoResponse {
    // Redirect to the comprehensive documentation in the book
    Redirect::permanent("https://docs.mockforge.dev/api/admin-ui-rest.html")
}

/// Serve the PWA manifest.json file
/// Note: Vite generates its own manifest.json, so we use pwa-manifest.json
/// which is copied from ui/public/manifest.json by the build script
pub async fn serve_manifest() -> impl IntoResponse {
    // Serve the source manifest directly so compilation does not depend on dist artifacts.
    (
        [(http::header::CONTENT_TYPE, "application/manifest+json")],
        include_str!("../../ui/public/manifest.json"),
    )
}

/// Serve the service worker JavaScript file
pub async fn serve_service_worker() -> impl IntoResponse {
    (
        [(http::header::CONTENT_TYPE, "application/javascript")],
        include_str!("../../ui/public/sw.js"),
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_serve_admin_html() {
        let html = serve_admin_html().await;
        let html_str = html.0;
        assert!(!html_str.is_empty());
        assert!(html_str.contains("<!DOCTYPE html>") || html_str.contains("<html"));
    }

    #[tokio::test]
    async fn test_serve_admin_css() {
        let (headers, _css) = serve_admin_css().await;
        assert_eq!(headers[0].0, http::header::CONTENT_TYPE);
        assert_eq!(headers[0].1, "text/css");
        // Content may be a placeholder when UI is not built
    }

    #[tokio::test]
    async fn test_serve_admin_js() {
        let (headers, _js) = serve_admin_js().await;
        assert_eq!(headers[0].0, http::header::CONTENT_TYPE);
        assert_eq!(headers[0].1, "application/javascript");
        // Content may be a placeholder when UI is not built
    }

    #[tokio::test]
    async fn test_serve_icon() {
        let response = serve_icon().await;
        // Icon returns SVG content - we can't easily check headers in impl IntoResponse
        // but we can verify it returns successfully
        let _ = response;
    }

    #[tokio::test]
    async fn test_serve_icon_32() {
        let _ = serve_icon_32().await;
    }

    #[tokio::test]
    async fn test_serve_icon_48() {
        let _ = serve_icon_48().await;
    }

    #[tokio::test]
    async fn test_serve_logo() {
        let _ = serve_logo().await;
    }

    #[tokio::test]
    async fn test_serve_logo_40() {
        let _ = serve_logo_40().await;
    }

    #[tokio::test]
    async fn test_serve_logo_80() {
        let _ = serve_logo_80().await;
    }

    #[tokio::test]
    async fn test_serve_api_docs() {
        let _ = serve_api_docs().await;
        // Redirect can't be easily tested without request context
        // but we verify it compiles and runs
    }
}