use mini_static::resolve;
use std::fs;
use tempfile::TempDir;
fn canon_eq(a: &std::path::Path, b: &std::path::Path) {
let a_canon = a.canonicalize().unwrap();
let b_canon = b.canonicalize().unwrap();
assert_eq!(a_canon, b_canon);
}
#[test]
fn resolve_legitimate_filename_with_double_dot_substring() {
let root = TempDir::new().unwrap();
let test_file = root.path().join("jquery..min.js");
fs::write(&test_file, b"content").unwrap();
let result = resolve(root.path(), "/jquery..min.js");
assert!(result.is_ok(), "should allow legitimate filename with .. substring");
canon_eq(&result.unwrap(), &test_file);
}
#[test]
fn resolve_traversal_attempt_rejected() {
let root = TempDir::new().unwrap();
let result = resolve(root.path(), "/../../etc/passwd");
assert!(result.is_err(), "should reject traversal attempt");
assert!(matches!(result, Err(mini_static::StaticError::Traversal(_))));
}
#[test]
fn resolve_traversal_single_segment_rejected() {
let root = TempDir::new().unwrap();
let result = resolve(root.path(), "/../etc/passwd");
assert!(result.is_err(), "should reject single .. segment");
}
#[test]
fn resolve_traversal_middle_segment_rejected() {
let root = TempDir::new().unwrap();
let result = resolve(root.path(), "/foo/../../../etc/passwd");
assert!(result.is_err(), "should reject .. segment in middle of path");
}
#[test]
fn resolve_regular_file() {
let root = TempDir::new().unwrap();
let test_file = root.path().join("test.txt");
fs::write(&test_file, b"hello").unwrap();
let result = resolve(root.path(), "/test.txt");
assert!(result.is_ok());
canon_eq(&result.unwrap(), &test_file);
}
#[test]
fn resolve_missing_file() {
let root = TempDir::new().unwrap();
let result = resolve(root.path(), "/nonexistent.txt");
assert!(result.is_err(), "should reject missing file");
assert!(matches!(result, Err(mini_static::StaticError::NotFound(_))));
}
#[test]
fn resolve_directory_with_index_html() {
let root = TempDir::new().unwrap();
fs::create_dir(root.path().join("docs")).unwrap();
let index = root.path().join("docs/index.html");
fs::write(&index, b"<html></html>").unwrap();
let result = resolve(root.path(), "/docs");
assert!(result.is_ok());
canon_eq(&result.unwrap(), &index);
}
#[test]
fn resolve_directory_without_index_html() {
let root = TempDir::new().unwrap();
fs::create_dir(root.path().join("empty")).unwrap();
let result = resolve(root.path(), "/empty");
assert!(result.is_err(), "should reject directory without index.html");
assert!(matches!(result, Err(mini_static::StaticError::NotFound(_))));
}
#[test]
fn resolve_null_byte_rejected() {
let root = TempDir::new().unwrap();
let result = resolve(root.path(), "/file\x00.txt");
assert!(result.is_err(), "should reject null bytes");
assert!(matches!(result, Err(mini_static::StaticError::Traversal(_))));
}
#[test]
fn resolve_percent_encoded_path() {
let root = TempDir::new().unwrap();
let test_file = root.path().join("hello world.txt");
fs::write(&test_file, b"content").unwrap();
let result = resolve(root.path(), "/hello%20world.txt");
assert!(result.is_ok());
canon_eq(&result.unwrap(), &test_file);
}
#[test]
fn resolve_leading_slash() {
let root = TempDir::new().unwrap();
let test_file = root.path().join("test.txt");
fs::write(&test_file, b"content").unwrap();
let result = resolve(root.path(), "test.txt");
assert!(result.is_ok(), "should work with or without leading slash");
canon_eq(&result.unwrap(), &test_file);
}
#[test]
fn resolve_ensures_boundary() {
let root = TempDir::new().unwrap();
let result = resolve(root.path(), "/");
assert!(result.is_err(), "root itself has no index.html");
}
#[test]
fn resolve_rejects_symlink_escaping_root() {
let outside = TempDir::new().unwrap();
let secret = outside.path().join("secret.txt");
fs::write(&secret, b"secret").unwrap();
let root = TempDir::new().unwrap();
let link = root.path().join("escape.txt");
std::os::unix::fs::symlink(&secret, &link).unwrap();
let result = resolve(root.path(), "/escape.txt");
assert!(
result.is_err(),
"a symlink resolving outside root should be rejected"
);
assert!(matches!(result, Err(mini_static::StaticError::Traversal(_))));
}
#[test]
fn resolve_rejects_symlinked_index_html_escaping_root() {
let outside = TempDir::new().unwrap();
let secret = outside.path().join("secret.txt");
fs::write(&secret, b"secret").unwrap();
let root = TempDir::new().unwrap();
fs::create_dir(root.path().join("docs")).unwrap();
let index_link = root.path().join("docs/index.html");
std::os::unix::fs::symlink(&secret, &index_link).unwrap();
let result = resolve(root.path(), "/docs/");
assert!(
result.is_err(),
"a symlinked index.html resolving outside root should be rejected"
);
assert!(matches!(result, Err(mini_static::StaticError::Traversal(_))));
}
#[test]
fn resolve_allows_symlink_within_root() {
let root = TempDir::new().unwrap();
let real_file = root.path().join("real.txt");
fs::write(&real_file, b"content").unwrap();
let link = root.path().join("link.txt");
std::os::unix::fs::symlink(&real_file, &link).unwrap();
let result = resolve(root.path(), "/link.txt");
assert!(result.is_ok(), "a symlink resolving within root should be allowed");
canon_eq(&result.unwrap(), &real_file);
}