mod common;
use hyper::Method;
use mini_static::{Server, TrailingSlash};
use std::fs;
use tempfile::TempDir;
fn root_with_index(dir: &str) -> TempDir {
let root = TempDir::new().unwrap();
fs::create_dir_all(root.path().join(dir)).unwrap();
fs::write(
root.path().join(dir).join("index.html"),
b"<html>Index</html>",
)
.unwrap();
root
}
fn location(response: &hyper::Response<mini_static::ResponseBody>) -> Option<&str> {
response
.headers()
.get("Location")
.and_then(|v| v.to_str().ok())
}
#[tokio::test]
async fn directory_without_a_trailing_slash_redirects_to_one() {
let root = root_with_index("docs");
let server = Server::new(root.path()).unwrap();
let response = common::get(&server, "/docs").await;
assert_eq!(response.status().as_u16(), 301);
assert_eq!(location(&response), Some("/docs/"));
assert_eq!(
response
.headers()
.get("X-Content-Type-Options")
.and_then(|v| v.to_str().ok()),
Some("nosniff"),
"the redirect must still carry the nosniff header"
);
}
#[tokio::test]
async fn nested_directory_redirects_to_its_full_path_with_a_slash() {
let root = root_with_index("api/v1/users");
let server = Server::new(root.path()).unwrap();
let response = common::get(&server, "/api/v1/users").await;
assert_eq!(response.status().as_u16(), 301);
assert_eq!(location(&response), Some("/api/v1/users/"));
}
#[tokio::test]
async fn head_gets_the_same_redirect_as_get() {
let root = root_with_index("docs");
let server = Server::new(root.path()).unwrap();
let response = common::request(&server, &Method::HEAD, "/docs").await;
assert_eq!(response.status().as_u16(), 301);
assert_eq!(location(&response), Some("/docs/"));
}
#[tokio::test]
async fn redirect_location_preserves_percent_encoding() {
let root = root_with_index("my docs");
let server = Server::new(root.path()).unwrap();
let response = common::get(&server, "/my%20docs").await;
assert_eq!(response.status().as_u16(), 301);
assert_eq!(location(&response), Some("/my%20docs/"));
}
#[tokio::test]
async fn a_path_that_already_addresses_the_index_is_served_without_a_redirect() {
let root = root_with_index("docs");
let server = Server::new(root.path()).unwrap();
for path in ["/docs/", "/docs/index.html", "/docs/index.htm%6c"] {
let response = common::get(&server, path).await;
assert_eq!(
response.status().as_u16(),
200,
"{path} should be served directly"
);
assert!(location(&response).is_none(), "{path} should not redirect");
}
}
#[tokio::test]
async fn directory_without_an_index_html_returns_404() {
let root = TempDir::new().unwrap();
fs::create_dir(root.path().join("empty")).unwrap();
let server = Server::new(root.path()).unwrap();
let response = common::get(&server, "/empty").await;
assert_eq!(response.status().as_u16(), 404);
}
#[tokio::test]
async fn serve_mode_answers_the_unslashed_directory_directly() {
let root = root_with_index("docs");
let server = Server::new(root.path())
.unwrap()
.with_trailing_slash(TrailingSlash::Serve);
let response = common::get(&server, "/docs").await;
assert_eq!(response.status().as_u16(), 200);
assert_eq!(
location(&response),
None,
"serve mode must not redirect at all"
);
}
#[tokio::test]
async fn serve_mode_still_answers_the_slashed_directory() {
let root = root_with_index("docs");
let server = Server::new(root.path())
.unwrap()
.with_trailing_slash(TrailingSlash::Serve);
let response = common::get(&server, "/docs/").await;
assert_eq!(response.status().as_u16(), 200);
}
#[tokio::test]
async fn serve_mode_returns_the_same_bytes_at_both_urls() {
let root = root_with_index("docs");
let server = Server::new(root.path())
.unwrap()
.with_trailing_slash(TrailingSlash::Serve);
let slashless = common::body_bytes(common::get(&server, "/docs").await).await;
let slashed = common::body_bytes(common::get(&server, "/docs/").await).await;
assert_eq!(slashless, slashed);
assert_eq!(slashless.as_ref(), b"<html>Index</html>");
}
#[tokio::test]
async fn serve_mode_does_not_invent_pages_for_missing_directories() {
let root = root_with_index("docs");
let server = Server::new(root.path())
.unwrap()
.with_trailing_slash(TrailingSlash::Serve);
let response = common::get(&server, "/nope").await;
assert_eq!(
response.status().as_u16(),
404,
"relaxing the trailing slash must not relax what exists"
);
}
#[tokio::test]
async fn redirect_is_the_default_when_trailing_slash_is_not_configured() {
let root = root_with_index("docs");
let server = Server::new(root.path()).unwrap();
let response = common::get(&server, "/docs").await;
assert_eq!(
response.status().as_u16(),
301,
"the safe behaviour must be the one you get without asking"
);
}