use std::sync::Arc;
#[cfg(test)]
use reinhardt_di::SingletonScope;
#[cfg(server)]
use reinhardt_pages::server_fn::ServerFnRouterExt;
use reinhardt_urls::routers::ServerRouter;
use crate::core::AdminSite;
#[cfg(server)]
fn resolve_wasm_dir() -> std::path::PathBuf {
if let Ok(dir) = std::env::var("REINHARDT_ADMIN_WASM_DIR") {
return std::path::PathBuf::from(dir);
}
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("dist-admin")
}
#[cfg(server)]
fn resolve_static_root_admin() -> Option<std::path::PathBuf> {
std::env::var("STATIC_ROOT")
.ok()
.map(|root| std::path::PathBuf::from(root).join("admin"))
.filter(|p| p.is_dir())
}
#[cfg(server)]
fn is_wasm_built() -> bool {
if let Some(admin_dir) = resolve_static_root_admin()
&& admin_dir.join("reinhardt_admin.js").is_file()
{
return true;
}
resolve_wasm_dir().join("reinhardt_admin.js").is_file()
}
#[cfg(server)]
async fn admin_spa_handler(
request: reinhardt_http::Request,
) -> reinhardt_core::exception::Result<reinhardt_http::Response> {
let assets_dir = std::path::PathBuf::from(ADMIN_ASSETS_DIR);
let _ =
reinhardt_utils::staticfiles::vendor::ensure_vendor_assets_for_app("admin", &assets_dir)
.await;
let settings = crate::settings::get_admin_settings();
let security_headers = settings.to_security_headers();
let csrf_token = crate::server::security::generate_csrf_token();
let csrf_cookie = crate::server::security::build_csrf_cookie(&csrf_token, request.is_secure);
let mut response = reinhardt_http::Response::ok()
.with_header("Content-Type", "text/html; charset=utf-8")
.append_header("Set-Cookie", &csrf_cookie);
for (name, value) in security_headers.to_header_map() {
response = response.with_header(name, &value);
}
Ok(response.with_body(admin_spa_html(&settings.site_title)))
}
#[cfg(server)]
fn resolve_admin_static(path: &str) -> String {
let admin_path = format!("admin/{}", path);
reinhardt_pages::static_resolver::resolve_static(&admin_path)
}
#[cfg(server)]
fn admin_spa_html(site_title: &str) -> String {
let css_url = resolve_admin_static("style.css");
let vendor_open_props = resolve_admin_static("vendor/open-props.min.css");
let vendor_animate = resolve_admin_static("vendor/animate.min.css");
let vendor_unocss_runtime = resolve_admin_static("vendor/unocss-runtime.js");
let wasm_built = is_wasm_built();
let js_url = if wasm_built {
resolve_admin_static("reinhardt_admin.js")
} else {
resolve_admin_static("main.js")
};
let script_tag = if wasm_built {
let init_js_url = resolve_admin_static("wasm-init.js");
format!(r#"<script type="module" src="{init_js_url}" data-wasm-entry="{js_url}"></script>"#)
} else {
format!(r#"<script type="module" src="{js_url}"></script>"#)
};
let head = reinhardt_pages::head!(|| {
meta { charset: "utf-8" }
meta { name: "viewport", content: "width=device-width, initial-scale=1.0" }
meta { name: "server-fn-prefix", content: "/admin" }
title { site_title.to_string() }
link { rel: "stylesheet", href: vendor_open_props }
link { rel: "stylesheet", href: vendor_animate }
link { rel: "stylesheet", href: css_url }
script { src: vendor_unocss_runtime }
});
format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
{head_html}
</head>
<body class="bg-slate-50 text-slate-900 antialiased">
<div id="app"></div>
{script_tag}
</body>
</html>"#,
head_html = head.to_html()
)
}
#[cfg(server)]
const ADMIN_ASSETS_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets");
#[cfg(server)]
async fn admin_static_file_handler(
request: reinhardt_http::Request,
) -> reinhardt_core::exception::Result<reinhardt_http::Response> {
match admin_static_file_handler_inner(request).await {
Ok(response) => Ok(response),
Err(e) => {
tracing::error!(error = %e, "Unexpected error in admin static file handler");
Ok(reinhardt_http::Response::internal_server_error()
.with_header("Content-Type", "text/plain; charset=utf-8")
.with_body("Internal Server Error"))
}
}
}
#[cfg(server)]
async fn admin_static_file_handler_inner(
request: reinhardt_http::Request,
) -> reinhardt_core::exception::Result<reinhardt_http::Response> {
use reinhardt_utils::staticfiles::handler::StaticFileHandler;
let path = request
.path_params
.get("path")
.map(|p| p.trim_start_matches('/'))
.unwrap_or("");
if let Some(admin_dir) = resolve_static_root_admin() {
let handler = StaticFileHandler::new(admin_dir);
if let Ok(file) = handler.serve(path).await {
return Ok(reinhardt_http::Response::ok()
.with_header("Content-Type", &file.mime_type)
.with_header("Cache-Control", "public, max-age=3600")
.with_body(file.content));
}
}
let wasm_handler = StaticFileHandler::new(resolve_wasm_dir());
if let Ok(file) = wasm_handler.serve(path).await {
return Ok(reinhardt_http::Response::ok()
.with_header("Content-Type", &file.mime_type)
.with_header("Cache-Control", "public, max-age=3600")
.with_body(file.content));
}
let assets_handler = StaticFileHandler::new(std::path::PathBuf::from(ADMIN_ASSETS_DIR));
match assets_handler.serve(path).await {
Ok(file) => Ok(reinhardt_http::Response::ok()
.with_header("Content-Type", &file.mime_type)
.with_header("Cache-Control", "public, max-age=3600")
.with_body(file.content)),
Err(_) => Ok(reinhardt_http::Response::not_found()),
}
}
pub fn admin_static_routes() -> ServerRouter {
let router = ServerRouter::new();
#[cfg(server)]
let router = router
.function("/{*path}", hyper::Method::GET, admin_static_file_handler)
.function("/{*path}", hyper::Method::HEAD, admin_static_file_handler);
router
}
pub fn admin_csp_exempt_paths() -> Vec<String> {
vec!["/admin".to_string(), "/static/admin".to_string()]
}
fn build_admin_router(
#[cfg(not(target_arch = "wasm32"))] jwt_secret: Option<&[u8]>,
) -> ServerRouter {
let router = ServerRouter::new().with_namespace("admin");
#[cfg(not(target_arch = "wasm32"))]
let router = router.with_middleware(crate::server::origin_guard::AdminOriginGuardMiddleware);
#[cfg(not(target_arch = "wasm32"))]
let router = if let Some(secret) = jwt_secret {
router.with_middleware(crate::server::cookie_auth::AdminCookieAuthMiddleware::new(
secret,
))
} else {
router
};
#[cfg(server)]
let router = {
use crate::server::{
bulk_delete_records, create_record, delete_record, export_data, get_dashboard,
get_detail, get_fields, get_list, import_data, login::admin_login,
logout::admin_logout, update_record,
};
router
.server_fn(get_dashboard::marker)
.server_fn(get_list::marker)
.server_fn(get_detail::marker)
.server_fn(get_fields::marker)
.server_fn(create_record::marker)
.server_fn(update_record::marker)
.server_fn(delete_record::marker)
.server_fn(bulk_delete_records::marker)
.server_fn(export_data::marker)
.server_fn(import_data::marker)
.server_fn(admin_login::marker)
.server_fn(admin_logout::marker)
.function("/", hyper::Method::GET, admin_spa_handler)
.function("/{*tail}", hyper::Method::GET, admin_spa_handler)
};
router
}
pub fn admin_routes_with_di(
site: Arc<AdminSite>,
) -> (ServerRouter, reinhardt_di::DiRegistrationList) {
let mut registrations = reinhardt_di::DiRegistrationList::new();
let loader = site.user_loader().unwrap_or_else(|| {
Arc::new(crate::server::admin_auth::create_admin_user_loader::<
crate::server::user::AdminDefaultUser,
>())
});
registrations.register_arc(loader);
let login_auth = site.login_authenticator().unwrap_or_else(|| {
Arc::new(
crate::server::admin_auth::create_admin_login_authenticator::<
crate::server::user::AdminDefaultUser,
>(),
)
});
registrations.register_arc(login_auth);
let jwt_secret = site.jwt_secret().map(|s| s.to_vec());
registrations.register_arc(site);
(
build_admin_router(
#[cfg(not(target_arch = "wasm32"))]
jwt_secret.as_deref(),
),
registrations,
)
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
const ADMIN_JS: &[u8] = include_bytes!("../../assets/main.js");
fn test_admin_routes() -> ServerRouter {
build_admin_router(
#[cfg(not(target_arch = "wasm32"))]
Some(b"test-jwt-secret"),
)
}
#[rstest]
fn test_admin_routes_creates_router() {
let router = test_admin_routes();
assert_eq!(router.namespace(), Some("admin"));
}
#[rstest]
fn test_admin_routes_with_di_returns_router_and_registrations() {
let site = Arc::new(AdminSite::new("Test Admin"));
let (router, registrations) = admin_routes_with_di(site);
assert_eq!(router.namespace(), Some("admin"));
assert!(!registrations.is_empty());
}
#[rstest]
fn test_admin_routes_with_di_applies_site_to_scope() {
let site = Arc::new(AdminSite::new("Applied Admin"));
let scope = SingletonScope::new();
let (_router, registrations) = admin_routes_with_di(site);
registrations.apply_to(&scope);
let registered = scope.get::<AdminSite>();
assert!(
registered.is_some(),
"AdminSite should be registered after apply_to"
);
assert_eq!(registered.unwrap().name(), "Applied Admin");
}
#[cfg(server)]
#[rstest]
fn test_admin_routes_registers_all_server_functions() {
let expected_paths = [
"/api/server_fn/get_dashboard",
"/api/server_fn/get_list",
"/api/server_fn/get_detail",
"/api/server_fn/get_fields",
"/api/server_fn/create_record",
"/api/server_fn/update_record",
"/api/server_fn/delete_record",
"/api/server_fn/bulk_delete_records",
"/api/server_fn/export_data",
"/api/server_fn/import_data",
"/api/server_fn/admin_login",
"/api/server_fn/admin_logout",
"/",
"/{*tail}",
];
let router = test_admin_routes();
let routes = router.get_all_routes();
let paths: Vec<&str> = routes.iter().map(|(path, _, _, _)| path.as_str()).collect();
assert_eq!(routes.len(), 14);
for expected in &expected_paths {
assert_eq!(
paths.iter().filter(|p| p == &expected).count(),
1,
"expected path {} to be registered exactly once, but found paths: {:?}",
expected,
paths
);
}
}
#[rstest]
fn test_set_favicon_stores_data() {
let site = Arc::new(AdminSite::new("Test Admin"));
let favicon_data = vec![0x89, 0x50, 0x4E, 0x47];
site.set_favicon(favicon_data.clone());
let stored = site.favicon_data();
assert!(stored.is_some());
assert_eq!(stored.unwrap(), favicon_data);
}
#[cfg(server)]
#[rstest]
fn test_admin_routes_includes_html_get_routes() {
let router = test_admin_routes();
let routes = router.get_all_routes();
let get_routes: Vec<_> = routes
.iter()
.filter(|(_, _, _, methods)| methods.contains(&hyper::Method::GET))
.collect();
assert!(get_routes.len() >= 2, "Should have at least 2 GET routes");
let paths: Vec<&str> = get_routes
.iter()
.map(|(path, _, _, _)| path.as_str())
.collect();
assert!(paths.contains(&"/"), "Should have root GET route");
assert!(
paths.contains(&"/{*tail}"),
"Should have catch-all GET route"
);
}
#[cfg(server)]
#[rstest]
fn test_admin_spa_html_contains_mount_point() {
let html = admin_spa_html("Reinhardt Admin");
assert!(
html.contains(r#"id="app""#),
"HTML should contain app mount point"
);
assert!(
html.contains("/static/admin/"),
"HTML should reference admin static files"
);
assert!(
html.contains("<!DOCTYPE html>"),
"HTML should be valid HTML5"
);
assert!(
html.contains(r#"name="server-fn-prefix""#) && html.contains(r#"content="/admin""#),
"HTML should contain server-fn-prefix meta tag for WASM endpoint resolution"
);
}
#[cfg(server)]
#[rstest]
fn test_admin_spa_html_references_css_and_js_entry_point() {
let html = admin_spa_html("Reinhardt Admin");
let wasm_built = is_wasm_built();
assert!(
html.contains("style.css"),
"HTML should reference admin CSS"
);
if wasm_built {
assert!(
html.contains("/static/admin/reinhardt_admin.js"),
"HTML should reference WASM entry point (reinhardt_admin.js) \
when WASM is built. Got:\n{}",
html
);
} else {
assert!(
html.contains("/static/admin/main.js"),
"HTML should reference placeholder (main.js) \
when WASM is not built. Got:\n{}",
html
);
}
}
#[cfg(server)]
#[rstest]
fn test_admin_spa_html_no_external_cdn_urls() {
let html = admin_spa_html("Reinhardt Admin");
assert!(
!html.contains("fonts.googleapis.com"),
"HTML should not reference Google Fonts CDN"
);
assert!(
!html.contains("fonts.gstatic.com"),
"HTML should not reference Google Fonts static CDN"
);
assert!(
!html.contains("cdn.jsdelivr.net"),
"HTML should not reference jsDelivr CDN"
);
}
#[cfg(server)]
#[rstest]
fn test_admin_spa_html_references_vendor_assets() {
let html = admin_spa_html("Reinhardt Admin");
assert!(
html.contains("vendor/open-props"),
"HTML should reference local Open Props CSS"
);
assert!(
html.contains("vendor/animate"),
"HTML should reference local Animate.css"
);
assert!(
html.contains("vendor/unocss-runtime"),
"HTML should reference local UnoCSS runtime JS"
);
}
#[cfg(server)]
#[rstest]
fn test_admin_spa_html_no_inline_script() {
let html = admin_spa_html("Reinhardt Admin");
assert!(
!html.contains("__unocss_runtime"),
"HTML should not contain UnoCSS runtime initialization"
);
}
#[cfg(server)]
#[rstest]
fn test_embedded_admin_js_is_valid_utf8_and_nonempty() {
let js = std::str::from_utf8(ADMIN_JS).expect("JS should be valid UTF-8");
assert!(!js.is_empty(), "Embedded admin JS should not be empty");
assert!(
js.contains("function") || js.contains("init(") || js.contains("wasm_bindgen"),
"Embedded JS should contain executable code. First 200 chars:\n{}",
&js[..js.len().min(200)]
);
}
#[rstest]
fn test_admin_static_routes_creates_router() {
let router = admin_static_routes();
assert_eq!(router.namespace(), None);
}
#[cfg(server)]
#[rstest]
fn test_admin_static_routes_registers_catch_all_route() {
let router = admin_static_routes();
let routes = router.get_all_routes();
let paths: Vec<&str> = routes.iter().map(|(path, _, _, _)| path.as_str()).collect();
assert_eq!(
routes.len(),
2,
"Should have exactly 2 catch-all routes (GET + HEAD)"
);
assert!(
paths.contains(&"/{*path}"),
"Should have catch-all path route, found: {:?}",
paths
);
}
#[cfg(server)]
#[rstest]
#[tokio::test]
async fn test_admin_static_file_handler_serves_css() {
let request = reinhardt_http::Request::builder()
.method(hyper::Method::GET)
.uri("/static/admin/style.css")
.path_params(std::collections::HashMap::from([(
"path".to_string(),
"style.css".to_string(),
)]))
.build()
.unwrap();
let response = admin_static_file_handler(request).await.unwrap();
assert_eq!(response.status, hyper::StatusCode::OK);
let content_type = response
.headers
.get("content-type")
.map(|v| v.to_str().unwrap_or(""))
.unwrap_or("");
assert!(
content_type.contains("text/css"),
"Should return text/css content type, got: {}",
content_type
);
}
#[cfg(server)]
#[rstest]
#[tokio::test]
async fn test_admin_static_file_handler_serves_js() {
let request = reinhardt_http::Request::builder()
.method(hyper::Method::GET)
.uri("/static/admin/main.js")
.path_params(std::collections::HashMap::from([(
"path".to_string(),
"main.js".to_string(),
)]))
.build()
.unwrap();
let response = admin_static_file_handler(request).await.unwrap();
assert_eq!(response.status, hyper::StatusCode::OK);
let content_type = response
.headers
.get("content-type")
.map(|v| v.to_str().unwrap_or(""))
.unwrap_or("");
assert!(
content_type.contains("javascript"),
"Should return application/javascript content type, got: {}",
content_type
);
}
#[cfg(server)]
#[rstest]
#[tokio::test]
async fn test_admin_static_file_handler_returns_404_for_missing_file() {
let request = reinhardt_http::Request::builder()
.method(hyper::Method::GET)
.uri("/static/admin/nonexistent.txt")
.path_params(std::collections::HashMap::from([(
"path".to_string(),
"nonexistent.txt".to_string(),
)]))
.build()
.unwrap();
let response = admin_static_file_handler(request).await.unwrap();
assert_eq!(
response.status,
hyper::StatusCode::NOT_FOUND,
"Should return 404 for nonexistent files"
);
}
#[cfg(server)]
#[rstest]
#[tokio::test]
async fn test_admin_static_file_handler_returns_404_for_wasm_when_not_built() {
let request = reinhardt_http::Request::builder()
.method(hyper::Method::GET)
.uri("/static/admin/reinhardt_admin_bg.wasm")
.path_params(std::collections::HashMap::from([(
"path".to_string(),
"reinhardt_admin_bg.wasm".to_string(),
)]))
.build()
.unwrap();
let response = admin_static_file_handler(request).await.unwrap();
assert_eq!(
response.status,
hyper::StatusCode::NOT_FOUND,
"Should return 404 when WASM is not built"
);
}
#[cfg(server)]
#[rstest]
#[tokio::test]
async fn test_admin_spa_handler_includes_csp_headers() {
let request = reinhardt_http::Request::builder()
.method(hyper::Method::GET)
.uri("/")
.build()
.unwrap();
let response = admin_spa_handler(request).await.unwrap();
let headers = response.headers;
assert!(
headers.contains_key("content-security-policy"),
"Response should include CSP header"
);
assert!(
headers.contains_key("x-frame-options"),
"Response should include X-Frame-Options header"
);
assert!(
headers.contains_key("x-content-type-options"),
"Response should include X-Content-Type-Options header"
);
}
#[cfg(server)]
#[rstest]
fn test_admin_spa_html_fallback_without_wasm() {
let html = admin_spa_html("Reinhardt Admin");
assert!(
html.contains("/static/admin/main.js")
|| html.contains("/static/admin/reinhardt_admin.js"),
"HTML should reference either main.js or reinhardt_admin.js"
);
}
#[cfg(server)]
#[rstest]
fn test_admin_spa_html_uses_configured_site_title() {
let custom_title = "My Custom Admin";
let html = admin_spa_html(custom_title);
assert!(
html.contains("<title>My Custom Admin</title>"),
"HTML <title> should reflect the configured site_title"
);
assert!(
!html.contains("<title>Reinhardt Admin</title>"),
"HTML should not contain the hardcoded default title"
);
}
#[cfg(server)]
#[rstest]
fn test_is_wasm_built_false_when_no_dist_wasm() {
let result = is_wasm_built();
assert_eq!(result, result); }
#[cfg(server)]
#[rstest]
fn test_resolve_wasm_dir_returns_dist_wasm_subdir() {
let dir = resolve_wasm_dir();
assert!(
dir.ends_with("dist-admin"),
"WASM dir should end with 'dist-admin', got: {:?}",
dir
);
}
#[cfg(server)]
#[rstest]
fn test_resolve_admin_static_uses_static_resolver() {
let url = resolve_admin_static("style.css");
assert!(
url.contains("admin/style.css"),
"Should resolve admin static path, got: {}",
url
);
}
#[rstest]
fn test_admin_csp_exempt_paths_returns_expected_paths() {
let paths = admin_csp_exempt_paths();
assert!(paths.contains(&"/admin".to_string()));
assert!(paths.contains(&"/static/admin".to_string()));
assert_eq!(paths.len(), 2);
}
#[cfg(server)]
#[rstest]
#[tokio::test]
async fn test_admin_spa_handler_sets_csrf_cookie() {
let request = reinhardt_http::Request::builder()
.method(hyper::Method::GET)
.uri("/")
.build()
.unwrap();
let response = admin_spa_handler(request).await.unwrap();
let set_cookie = response
.headers
.get("set-cookie")
.expect("Response should include Set-Cookie header")
.to_str()
.unwrap();
assert!(
set_cookie.contains("csrftoken="),
"Cookie should contain CSRF token name, got: {}",
set_cookie
);
assert!(
set_cookie.contains("SameSite=Strict"),
"Cookie should have SameSite=Strict, got: {}",
set_cookie
);
assert!(
set_cookie.contains("Path=/admin"),
"Cookie should be scoped to /admin, got: {}",
set_cookie
);
}
#[cfg(server)]
#[rstest]
fn test_admin_static_routes_serves_wasm_binary() {
let router = admin_static_routes();
let routes = router.get_all_routes();
let paths: Vec<&str> = routes.iter().map(|(path, _, _, _)| path.as_str()).collect();
assert!(
paths.iter().any(|p| p.contains("{*path}")),
"Admin static routes must have a catch-all route to serve WASM files. \
Found routes: {:?}",
paths
);
}
#[cfg(server)]
#[rstest]
fn test_embedded_admin_js_is_not_placeholder() {
if !is_wasm_built() {
return;
}
let js = std::str::from_utf8(ADMIN_JS).expect("JS should be valid UTF-8");
assert!(
!js.contains("placeholder"),
"Embedded admin JS must not be a placeholder. First 200 chars:\n{}",
&js[..js.len().min(200)]
);
assert!(
!js.contains("WASM frontend may not be built yet"),
"Embedded admin JS must not contain 'not built yet' fallback message"
);
}
#[cfg(server)]
#[rstest]
fn test_html_js_reference_matches_static_route() {
let html = admin_spa_html("Reinhardt Admin");
let router = admin_static_routes();
let routes = router.get_all_routes();
let paths: Vec<&str> = routes.iter().map(|(path, _, _, _)| path.as_str()).collect();
let wasm_js_path = if is_wasm_built() {
"/reinhardt_admin.js"
} else {
"/main.js"
};
assert!(
html.contains(&format!("/static/admin{}", wasm_js_path)),
"HTML must reference /static/admin{}, got:\n{}",
wasm_js_path,
html
);
assert!(
paths.contains(&"/{*path}"),
"Static routes must serve files via catch-all pattern, found: {:?}",
paths
);
}
#[cfg(server)]
#[rstest]
#[tokio::test]
async fn test_admin_spa_handler_csrf_cookie_no_secure_for_http() {
let request = reinhardt_http::Request::builder()
.method(hyper::Method::GET)
.uri("/")
.build()
.unwrap();
let response = admin_spa_handler(request).await.unwrap();
let set_cookie = response
.headers
.get("set-cookie")
.expect("Response should include Set-Cookie header")
.to_str()
.unwrap();
assert!(
!set_cookie.contains("Secure"),
"HTTP request should not set Secure flag, got: {}",
set_cookie
);
}
#[cfg(server)]
#[rstest]
#[tokio::test]
async fn test_admin_static_routes_full_stack_css_content_type() {
use reinhardt_http::Handler;
let router = ServerRouter::new().mount("/static/admin/", admin_static_routes());
let request = reinhardt_http::Request::builder()
.method(hyper::Method::GET)
.uri("/static/admin/style.css")
.build()
.unwrap();
let response = router.handle(request).await.unwrap();
assert_eq!(response.status, hyper::StatusCode::OK);
let content_type = response
.headers
.get("content-type")
.map(|v| v.to_str().unwrap_or(""))
.unwrap_or("");
assert!(
content_type.contains("text/css"),
"Full-stack CSS should return text/css, got: {}",
content_type
);
assert!(
!content_type.contains("application/json"),
"Static file must never return application/json (#3135), got: {}",
content_type
);
}
#[cfg(server)]
#[rstest]
#[tokio::test]
async fn test_admin_static_routes_full_stack_js_content_type() {
use reinhardt_http::Handler;
let router = ServerRouter::new().mount("/static/admin/", admin_static_routes());
let request = reinhardt_http::Request::builder()
.method(hyper::Method::GET)
.uri("/static/admin/main.js")
.build()
.unwrap();
let response = router.handle(request).await.unwrap();
assert_eq!(response.status, hyper::StatusCode::OK);
let content_type = response
.headers
.get("content-type")
.map(|v| v.to_str().unwrap_or(""))
.unwrap_or("");
assert!(
content_type.contains("javascript"),
"Full-stack JS should return application/javascript, got: {}",
content_type
);
assert!(
!content_type.contains("application/json"),
"Static file must never return application/json (#3135), got: {}",
content_type
);
}
#[cfg(server)]
#[rstest]
#[tokio::test]
async fn test_admin_static_routes_full_stack_404_not_json() {
use reinhardt_http::Handler;
let router = ServerRouter::new().mount("/static/admin/", admin_static_routes());
let request = reinhardt_http::Request::builder()
.method(hyper::Method::GET)
.uri("/static/admin/nonexistent.wasm")
.build()
.unwrap();
let response = router.handle(request).await.unwrap();
assert_eq!(response.status, hyper::StatusCode::NOT_FOUND);
let content_type = response
.headers
.get("content-type")
.map(|v| v.to_str().unwrap_or(""))
.unwrap_or("");
assert!(
!content_type.contains("application/json"),
"Static file 404 must not return application/json (#3135), got: {}",
content_type
);
}
}