use std::io::Write;
use std::path::Path;
use tempfile::tempdir;
use super::safety::{content_type_for_path, references_domi_js};
use super::serve_file;
use super::{ContentType, ServeError};
fn write(p: &Path, s: &str) {
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
let mut f = std::fs::File::create(p).unwrap();
f.write_all(s.as_bytes()).unwrap();
}
#[test]
fn serves_html_with_domi_js_injects_shim_before_script() {
let dir = tempdir().unwrap();
let root = dir.path();
let file = root.join("dashboard.html");
write(
&file,
r#"<!doctype html><html><body><script src="../scripts/runtime/domi.js"></script></body></html>"#,
);
let body = std::fs::read(&file).unwrap();
let requested = Path::new("dashboard.html");
let s = serve_file(root, requested).expect("serve ok");
assert_eq!(s.content_type, ContentType::Html);
let out = std::str::from_utf8(&s.body).unwrap();
let shim_pos = out.find("window.__DOMI_SERVER__").expect("shim present");
let original_script_pos = out.find("domi.js").expect("original ref present");
assert!(shim_pos < original_script_pos, "shim must come before the existing <script>");
assert!(out.contains("domi.js"));
}
#[test]
fn html_without_domi_js_returns_unchanged() {
let dir = tempdir().unwrap();
let root = dir.path();
let file = root.join("plain.html");
write(&file, "<html><body><h1>hi</h1></body></html>");
let original = std::fs::read(&file).unwrap();
let s = serve_file(root, Path::new("plain.html")).unwrap();
assert_eq!(s.body, original);
}
#[test]
fn html_with_domi_audit_only_still_gets_shim() {
let dir = tempdir().unwrap();
let root = dir.path();
let file = root.join("tracker.html");
write(
&file,
r#"<!doctype html><html><body><script src="/scripts/runtime/domi-audit.js" defer></script></body></html>"#,
);
let s = serve_file(root, Path::new("tracker.html")).expect("serve ok");
let out = std::str::from_utf8(&s.body).unwrap();
assert!(out.contains("window.__DOMI_SERVER__"), "shim injected for domi-audit-only page");
assert!(out.contains("domi-audit.js"), "original script tag preserved");
let shim_pos = out.find("window.__DOMI_SERVER__").unwrap();
let audit_pos = out.find("domi-audit.js").unwrap();
assert!(shim_pos < audit_pos, "shim must come before the existing <script>");
}
#[test]
fn html_with_external_script_only_no_inline_still_gets_shim() {
let dir = tempdir().unwrap();
let root = dir.path();
let file = root.join("loader.html");
write(
&file,
r#"<!doctype html><html><body><script src="https://example.com/x.js"></script></body></html>"#,
);
let s = serve_file(root, Path::new("loader.html")).expect("serve ok");
let out = std::str::from_utf8(&s.body).unwrap();
assert!(out.contains("window.__DOMI_SERVER__"));
}
#[test]
fn css_returns_unchanged() {
let dir = tempdir().unwrap();
let root = dir.path();
let file = root.join("style.css");
write(&file, "body { color: red; }");
let s = serve_file(root, Path::new("style.css")).unwrap();
assert_eq!(s.content_type, ContentType::Css);
assert!(std::str::from_utf8(&s.body).unwrap().contains("color: red"));
}
#[test]
fn missing_file_returns_NotFound() {
let dir = tempdir().unwrap();
let s = serve_file(dir.path(), Path::new("nope.html"));
assert!(matches!(s, Err(ServeError::NotFound)));
}
#[test]
fn directory_returns_NotAFile() {
let dir = tempdir().unwrap();
let sub = dir.path().join("subdir");
std::fs::create_dir(&sub).unwrap();
let s = serve_file(dir.path(), Path::new("subdir"));
assert!(matches!(s, Err(ServeError::NotAFile)));
}
#[test]
fn path_escape_returns_EscapedRoot() {
let dir = tempdir().unwrap();
let root = dir.path();
let outside = dir.path().parent().unwrap().join("outside.html");
std::fs::write(&outside, "<html></html>").unwrap();
let s = serve_file(root, Path::new("../outside.html"));
assert!(matches!(s, Err(ServeError::EscapedRoot)));
}
#[test]
fn symlink_under_root_to_outside_file_is_served() {
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let dir = tempdir().unwrap();
let lib = tempdir().unwrap();
std::fs::write(lib.path().join("audit.js"), b"console.log('ok');").unwrap();
let root = dir.path();
std::fs::create_dir(root.join("scripts")).unwrap();
symlink(lib.path().join("audit.js"), root.join("scripts/audit.js")).unwrap();
let s = serve_file(root, Path::new("scripts/audit.js")).expect("serve ok");
let body = std::str::from_utf8(&s.body).unwrap();
assert!(body.contains("console.log('ok')"));
}
}
#[test]
fn symlink_under_root_to_outside_directory_serves_leaf() {
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let dir = tempdir().unwrap();
let lib = tempdir().unwrap();
std::fs::write(lib.path().join("audit.js"), b"console.log('lib');").unwrap();
let root = dir.path();
symlink(lib.path(), root.join("scripts")).unwrap();
let s = serve_file(root, Path::new("scripts/audit.js")).expect("serve ok");
let body = std::str::from_utf8(&s.body).unwrap();
assert!(body.contains("console.log('lib')"));
}
}
#[test]
fn symlink_under_root_to_directory_traversal_still_blocked() {
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let dir = tempdir().unwrap();
let outside = dir.path().parent().unwrap();
std::fs::write(outside.join("secret.html"), "<html>secret</html>").unwrap();
let root = dir.path();
let s = serve_file(root, Path::new("../outside/../escape.html"));
assert!(
matches!(s, Err(ServeError::EscapedRoot) | Err(ServeError::NotFound)),
"expected refusal, got {:?}",
s
);
}
}
#[test]
fn content_type_js_for_js_files() {
let dir = tempdir().unwrap();
let root = dir.path();
write(&root.join("app.js"), "console.log(1);");
let s = serve_file(root, Path::new("app.js")).unwrap();
assert_eq!(s.content_type, ContentType::Js);
}
#[test]
fn content_type_default_is_octet_stream() {
let dir = tempdir().unwrap();
let root = dir.path();
write(&root.join("mystery.xyz"), "data");
let s = serve_file(root, Path::new("mystery.xyz")).unwrap();
assert_eq!(s.content_type, ContentType::OctetStream);
}
#[test]
fn safety_helpers_are_exported() {
let p = std::path::Path::new("/tmp/x.html");
assert_eq!(content_type_for_path(p), ContentType::Html);
let body = b"<script src=\"domi.js\"></script>";
assert!(references_domi_js(body));
let safe = std::path::Path::new("normal/file.css");
assert!(super::safety::ensure_no_parent_dir_components(safe).is_ok());
let bad = std::path::Path::new("../escape.html");
assert!(matches!(
super::safety::ensure_no_parent_dir_components(bad),
Err(ServeError::EscapedRoot)
));
}