mod common;
use hyper::header::HeaderMap;
use hyper::Method;
use mini_static::Server;
use std::fs;
use tempfile::TempDir;
#[tokio::test]
async fn traversal_and_missing_file_are_indistinguishable() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("exists.txt"), b"content").unwrap();
let server = Server::new(root.path()).unwrap();
let traversal = common::get(&server, "/../../../etc/passwd").await;
let missing = common::get(&server, "/definitely-not-a-real-file-xyz.txt").await;
assert_eq!(traversal.status().as_u16(), 404);
assert_eq!(missing.status().as_u16(), 404);
let traversal_body = common::body_bytes(traversal).await;
let missing_body = common::body_bytes(missing).await;
assert_eq!(
traversal_body, missing_body,
"the two bodies must be byte-identical, or the response leaks which case occurred"
);
}
#[tokio::test]
async fn every_response_carries_nosniff_and_success_returns_the_real_file_content() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("test.txt"), b"content").unwrap();
let server = Server::new(root.path()).unwrap();
for path in ["/../etc/passwd", "/nonexistent.txt", "/test.txt"] {
let response = common::get(&server, path).await;
assert_eq!(
response
.headers()
.get("X-Content-Type-Options")
.map(|v| v.to_str().unwrap()),
Some("nosniff"),
"{path} should carry nosniff"
);
}
let success = common::get(&server, "/test.txt").await;
assert_eq!(success.status().as_u16(), 200);
let body = common::body_bytes(success).await;
assert_eq!(
&body[..],
b"content",
"success response body should be the file's real content"
);
}
#[tokio::test]
async fn a_same_second_rewrite_of_equal_length_is_not_answered_304() {
let root = TempDir::new().unwrap();
let path = root.path().join("asset.js");
fs::write(&path, b"console.log(1)").unwrap();
let server = Server::new(root.path()).unwrap();
let first = common::get(&server, "/asset.js").await;
let stale_etag = first.headers().get("ETag").unwrap().clone();
fs::write(&path, b"console.log(2)").unwrap();
let mut headers = HeaderMap::new();
headers.insert("if-none-match", stale_etag);
let revalidated = server
.handle_request(&Method::GET, "/asset.js", &headers)
.await;
assert_eq!(
revalidated.status().as_u16(),
200,
"a changed file must not revalidate as 304"
);
assert_eq!(
common::body_bytes(revalidated).await.as_ref(),
b"console.log(2)",
"the client must receive the new bytes, not the cached ones"
);
}
#[tokio::test]
async fn a_304_carries_nosniff_like_every_other_response() {
let root = TempDir::new().unwrap();
fs::write(root.path().join("test.txt"), b"content").unwrap();
let server = Server::new(root.path()).unwrap();
let first = common::get(&server, "/test.txt").await;
let etag = first
.headers()
.get("ETag")
.expect("a 200 must carry an ETag to revalidate against")
.clone();
let mut headers = HeaderMap::new();
headers.insert("if-none-match", etag);
let revalidated = server
.handle_request(&Method::GET, "/test.txt", &headers)
.await;
assert_eq!(
revalidated.status().as_u16(),
304,
"a matching ETag must revalidate, or this test is asserting nothing"
);
assert_eq!(
revalidated
.headers()
.get("X-Content-Type-Options")
.map(|v| v.to_str().unwrap()),
Some("nosniff"),
);
}