mod common;
use common::{body_bytes, get, request_with_headers};
use hyper::{Method, StatusCode};
use mini_static::Server;
use std::fs;
use tempfile::TempDir;
const PLAIN: &[u8] = b"body{color:red}\n";
const BROTLI: &[u8] = b"pretend-brotli-bytes";
fn root_with_sidecar() -> TempDir {
let root = TempDir::new().unwrap();
fs::write(root.path().join("styles.css"), PLAIN).unwrap();
fs::write(root.path().join("styles.css.br"), BROTLI).unwrap();
root
}
async fn br_request(server: &Server, path: &str) -> hyper::Response<mini_static::ResponseBody> {
request_with_headers(server, &Method::GET, path, &[("accept-encoding", "br")]).await
}
#[tokio::test]
async fn a_sidecar_is_served_with_the_originals_content_type() {
let root = root_with_sidecar();
let server = Server::new(root.path()).unwrap();
let response = br_request(&server, "/styles.css").await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get("content-encoding").unwrap(),
"br",
"the sidecar must be selected"
);
assert_eq!(
response.headers().get("content-type").unwrap(),
"text/css; charset=utf-8",
"content type comes from the original's extension, not the sidecar's"
);
assert_eq!(body_bytes(response).await.as_ref(), BROTLI);
}
#[tokio::test]
async fn a_sidecar_without_its_original_is_not_served() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("styles.css.br"), BROTLI).unwrap();
let server = Server::new(root.path()).unwrap();
let response = br_request(&server, "/styles.css").await;
assert_eq!(
response.status(),
StatusCode::NOT_FOUND,
"no original means 404, whichever order the probe runs in"
);
}
#[tokio::test]
async fn a_slashless_directory_still_redirects() {
let root = TempDir::new().unwrap();
fs::create_dir(root.path().join("assets")).unwrap();
fs::write(root.path().join("assets").join("index.html"), b"<p>in</p>").unwrap();
let server = Server::new(root.path()).unwrap();
let response = get(&server, "/assets").await;
assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY);
assert_eq!(response.headers().get("location").unwrap(), "/assets/");
}
#[tokio::test]
async fn a_directory_index_serves_its_sidecar() {
let root = TempDir::new().unwrap();
let assets = root.path().join("assets");
fs::create_dir(&assets).unwrap();
fs::write(assets.join("index.html"), b"<p>plain</p>").unwrap();
fs::write(assets.join("index.html.br"), BROTLI).unwrap();
let server = Server::new(root.path()).unwrap();
for path in ["/assets/", "/"] {
let response = br_request(&server, path).await;
if path == "/" {
assert_eq!(response.status(), StatusCode::NOT_FOUND);
continue;
}
assert_eq!(
response.headers().get("content-encoding").map(|v| v.to_str().unwrap()),
Some("br"),
"a trailing slash must find index.html's sidecar"
);
assert_eq!(body_bytes(response).await.as_ref(), BROTLI);
}
}
#[tokio::test]
async fn a_range_request_gets_the_original() {
let root = root_with_sidecar();
let server = Server::new(root.path()).unwrap();
let response = request_with_headers(
&server,
&Method::GET,
"/styles.css",
&[("accept-encoding", "br"), ("range", "bytes=0-3")],
)
.await;
assert!(
!response.headers().contains_key("content-encoding"),
"a ranged response must come from the original, not a sidecar"
);
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
}
#[tokio::test]
async fn spa_mode_keeps_the_old_order_and_still_serves_sidecars() {
let root = root_with_sidecar();
let server = Server::new(root.path()).unwrap().with_spa_mode();
let response = br_request(&server, "/styles.css").await;
assert_eq!(
response.headers().get("content-encoding").map(|v| v.to_str().unwrap()),
Some("br"),
"a non-HTML asset still gets its sidecar with SPA mode enabled"
);
assert_eq!(body_bytes(response).await.as_ref(), BROTLI);
}
#[tokio::test]
async fn a_symlinked_original_still_serves() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("real.css"), PLAIN).unwrap();
std::os::unix::fs::symlink(root.path().join("real.css"), root.path().join("styles.css"))
.unwrap();
let server = Server::new(root.path()).unwrap();
let response = br_request(&server, "/styles.css").await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(body_bytes(response).await.as_ref(), PLAIN);
}
#[tokio::test]
async fn the_pre_open_path_still_refuses_hidden_files() {
let root = TempDir::new().unwrap();
fs::write(root.path().join(".env"), b"SECRET=1").unwrap();
fs::write(root.path().join(".env.br"), BROTLI).unwrap();
let server = Server::new(root.path()).unwrap();
let response = br_request(&server, "/.env").await;
assert_eq!(
response.status(),
StatusCode::NOT_FOUND,
"a dot-prefixed segment is refused before any sidecar is considered"
);
}
#[tokio::test]
async fn the_pre_open_path_still_refuses_an_encoded_separator() {
let root = TempDir::new().unwrap();
let admin = root.path().join("admin");
fs::create_dir(&admin).unwrap();
fs::write(admin.join("config"), b"nested").unwrap();
fs::write(admin.join("config.br"), BROTLI).unwrap();
let server = Server::new(root.path()).unwrap();
let response = br_request(&server, "/admin%2Fconfig").await;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn spa_mode_injects_html_rather_than_serving_its_sidecar() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("index.html"), b"<html><body>hi</body></html>").unwrap();
fs::write(root.path().join("index.html.br"), BROTLI).unwrap();
let server = Server::new(root.path()).unwrap().with_spa_mode();
let response = br_request(&server, "/index.html").await;
assert!(
!response.headers().contains_key("content-encoding"),
"an injectable page must not be replaced by a precompressed sidecar"
);
let body = body_bytes(response).await;
assert_ne!(
body.as_ref(),
BROTLI,
"the sidecar was served instead of the page that needed injecting"
);
}
#[tokio::test]
async fn a_symlinked_parent_directory_falls_back_to_the_open() {
let root = TempDir::new().unwrap();
let real = root.path().join("real");
fs::create_dir(&real).unwrap();
fs::write(real.join("styles.css"), PLAIN).unwrap();
fs::write(real.join("styles.css.br"), BROTLI).unwrap();
std::os::unix::fs::symlink(&real, root.path().join("link")).unwrap();
let server = Server::new(root.path())
.unwrap()
.with_immutable_assets(|path| path.to_string_lossy().contains("/real/"));
let response = br_request(&server, "/link/styles.css").await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get("cache-control").map(|v| v.to_str().unwrap()),
Some("public, max-age=31536000, immutable"),
"the predicate must see the resolved path, as it did before the reordering"
);
}
#[tokio::test]
async fn a_directory_beside_a_sidecar_named_file_still_redirects() {
let root = TempDir::new().unwrap();
let assets = root.path().join("assets");
fs::create_dir(&assets).unwrap();
fs::write(assets.join("index.html"), b"<p>in</p>").unwrap();
fs::write(root.path().join("assets.br"), BROTLI).unwrap();
let server = Server::new(root.path()).unwrap();
let response = br_request(&server, "/assets").await;
assert_eq!(
response.status(),
StatusCode::MOVED_PERMANENTLY,
"a directory must redirect, not be answered from a same-named sidecar"
);
assert_eq!(response.headers().get("location").unwrap(), "/assets/");
}