use poem::{ Response, handler, http::{StatusCode, header}, web::Path, }; use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "../frontend/dist"] struct EmbeddedAssets; fn serve_embedded(path: &str) -> Response { let normalized = if path.is_empty() { "index.html" } else { path.trim_start_matches('/') }; let is_asset_request = normalized.starts_with("assets/"); let asset = if is_asset_request { EmbeddedAssets::get(normalized) } else { EmbeddedAssets::get(normalized).or_else(|| { if normalized == "index.html" { None } else { EmbeddedAssets::get("index.html") } }) }; match asset { Some(content) => { let body = content.data.into_owned(); let mime = mime_guess::from_path(normalized) .first_or_octet_stream() .to_string(); Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, mime) .body(body) } None => Response::builder() .status(StatusCode::NOT_FOUND) .body("Not Found"), } } /// Serve a single embedded asset from the `assets/` folder. #[handler] pub fn embedded_asset(Path(path): Path) -> Response { let asset_path = format!("assets/{path}"); serve_embedded(&asset_path) } /// Serve an embedded file by path (falls back to `index.html` for SPA routing). #[handler] pub fn embedded_file(Path(path): Path) -> Response { serve_embedded(&path) } /// Serve the embedded SPA entrypoint. #[handler] pub fn embedded_index() -> Response { serve_embedded("index.html") } #[cfg(test)] mod tests { use super::*; use poem::http::StatusCode; #[test] fn non_asset_path_spa_fallback_or_not_found() { // Non-asset paths fall back to index.html for SPA client-side routing. // In release builds (with embedded dist/) this returns 200. // In debug builds without a built frontend dist/ it returns 404. let response = serve_embedded("__nonexistent_spa_route__.html"); let status = response.status(); assert!( status == StatusCode::OK || status == StatusCode::NOT_FOUND, "unexpected status: {status}", ); } #[test] fn missing_asset_path_prefix_returns_not_found() { // assets/ prefix: no SPA fallback – returns 404 if the file does not exist let response = serve_embedded("assets/__nonexistent__.js"); assert_eq!(response.status(), StatusCode::NOT_FOUND); } #[test] fn serve_embedded_does_not_panic_on_empty_path() { // Empty path normalises to index.html; OK in release, 404 in debug without dist/ let response = serve_embedded(""); let status = response.status(); assert!( status == StatusCode::OK || status == StatusCode::NOT_FOUND, "unexpected status: {status}", ); } #[test] fn embedded_assets_struct_is_iterable() { // Verifies that rust-embed compiled the EmbeddedAssets struct correctly. // In debug builds without a built frontend dist/ directory the iterator is empty; that is // expected. In release builds it will contain all bundled frontend files. let _files: Vec<_> = EmbeddedAssets::iter().collect(); // No assertion needed – the test passes as long as it compiles and does not panic. } #[tokio::test] async fn embedded_index_handler_returns_ok_or_not_found() { // Route the handler through TestClient; index.html is the SPA entry point. let app = poem::Route::new().at("/", poem::get(embedded_index)); let cli = poem::test::TestClient::new(app); let resp = cli.get("/").send().await; let status = resp.0.status(); assert!( status == StatusCode::OK || status == StatusCode::NOT_FOUND, "unexpected status: {status}", ); } #[tokio::test] async fn embedded_file_handler_with_path_returns_ok_or_not_found() { // Non-asset paths fall back to index.html (SPA routing) or 404. let app = poem::Route::new().at("/*path", poem::get(embedded_file)); let cli = poem::test::TestClient::new(app); let resp = cli.get("/__spa_route__").send().await; let status = resp.0.status(); assert!( status == StatusCode::OK || status == StatusCode::NOT_FOUND, "unexpected status: {status}", ); } #[tokio::test] async fn embedded_asset_handler_missing_file_returns_not_found() { // The assets/ prefix disables SPA fallback; missing files must return 404. let app = poem::Route::new().at("/assets/*path", poem::get(embedded_asset)); let cli = poem::test::TestClient::new(app); let resp = cli.get("/assets/__nonexistent__.js").send().await; assert_eq!(resp.0.status(), StatusCode::NOT_FOUND); } }