test: add functioning tests

This commit is contained in:
Angel Hudgins 2025-03-06 19:22:27 +01:00
parent c459800fca
commit 57f5404fde
11 changed files with 431 additions and 67 deletions

13
Cargo.lock generated
View File

@ -362,18 +362,18 @@ checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.219" version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.219" version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -403,7 +403,12 @@ version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"bytes", "bytes",
"flate2",
"http-body-util",
"static-serve-macro", "static-serve-macro",
"tokio",
"tower",
"zstd",
] ]
[[package]] [[package]]

View File

@ -14,5 +14,12 @@ static-serve-macro = { path = "../static-serve-macro", version = "0.1.0" }
axum = { version = "0.8", default-features = false } axum = { version = "0.8", default-features = false }
bytes = "1.10" bytes = "1.10"
[dev-dependencies]
http-body-util = "0.1"
tokio = { version = "1.44", features = ["rt", "macros"] }
tower = { version = "0.5", features = ["util"] }
zstd = "0.13"
flate2 = "1.1"
[lints] [lints]
workspace = true workspace = true

View File

@ -127,66 +127,3 @@ where
), ),
) )
} }
#[cfg(test)]
mod tests {
#[test]
fn router_created_with_test_routes_lit_str() {
embed_assets!("test_assets/small", compress = false);
let router: Router<()> = static_router();
assert!(router.has_routes());
}
#[test]
fn router_created_not_compressed_because_not_worthwhile() {
embed_assets!("test_assets/small", compress = true);
let router: Router<()> = static_router();
assert!(router.has_routes());
}
#[test]
fn router_created_compressed() {
embed_assets!("test_assets/big", compress = true);
let router: Router<()> = static_router();
assert!(router.has_routes());
}
#[test]
fn router_created_with_test_routes_ident() {
embed_assets!(test_assets, compress = true);
let router: Router<()> = static_router();
assert!(router.has_routes());
}
#[test]
fn router_created_ignore_dirs_one() {
embed_assets!(test_assets, ignore_dirs = ["test_assets/big"]);
let router: Router<()> = static_router();
assert!(router.has_routes());
}
#[test]
fn router_created_ignore_dirs_two() {
embed_assets!(
test_assets,
ignore_dirs = ["test_assets/big", "test_assets/small"]
);
let router: Router<()> = static_router();
// all directories ignored, so router has no routes
assert!(!router.has_routes());
}
#[test]
fn router_created_ignore_dirs_with_defaults() {
// TODO: actually create one of the default ignore directories
// in `test_assets` to make sure this works
embed_assets!(
test_assets,
ignore_dirs = ["test_assets/big"],
use_default_ignore_dirs = true
);
let router: Router<()> = static_router();
assert!(router.has_routes());
}
}

342
static-serve/tests/tests.rs Normal file
View File

@ -0,0 +1,342 @@
//! Integration tests for static-serve and macro
use std::io::Read;
use axum::{
body::Body,
http::{
header::{ACCEPT_ENCODING, CONTENT_ENCODING, IF_NONE_MATCH},
HeaderValue, Request, Response, StatusCode,
},
Router,
};
use http_body_util::BodyExt;
use tower::ServiceExt;
use static_serve_macro::embed_assets;
enum Compression {
Zstd,
Gzip,
Both,
None,
}
async fn get_response(
router: Router<()>,
request: Request<axum::body::Body>,
) -> Response<axum::body::Body> {
router
.into_service()
.oneshot(request)
.await
.expect("sending request")
}
fn create_request(route: &str, compression: &Compression) -> Request<axum::body::Body> {
let accept_encoding_header = match compression {
Compression::Both => Some(HeaderValue::from_static("zstd, gzip")),
Compression::Zstd => Some(HeaderValue::from_static("zstd")),
Compression::Gzip => Some(HeaderValue::from_static("gzip")),
Compression::None => None,
};
match accept_encoding_header {
Some(v) => Request::builder()
.uri(route)
.header(ACCEPT_ENCODING, v)
.body(Body::empty())
.unwrap(),
None => Request::builder().uri(route).body(Body::empty()).unwrap(),
}
}
fn decompress_zstd(compressed_body: &[u8]) -> Vec<u8> {
let mut decoder = zstd::Decoder::new(compressed_body).expect("failed to create decoder");
let mut decompressed_body = Vec::new();
std::io::copy(&mut decoder, &mut decompressed_body).expect("failed to decompress");
decompressed_body
}
fn decompress_gzip(compressed_body: &[u8]) -> Vec<u8> {
let mut decompressed_body = Vec::new();
let mut decoder = flate2::bufread::GzDecoder::new(compressed_body);
decoder
.read_to_end(&mut decompressed_body)
.expect("can't decode body");
decompressed_body
}
#[tokio::test]
async fn router_created_with_lit_str() {
embed_assets!("../static-serve/test_assets/small", compress = false);
let router: Router<()> = static_router();
assert!(router.has_routes());
let request = create_request("/app.js", &Compression::None);
let response = get_response(router, request).await;
let (parts, body) = response.into_parts();
assert!(parts.status.is_success());
assert_eq!(
parts.headers.get("content-type").unwrap(),
"text/javascript"
);
assert!(parts.headers.contains_key("etag"));
let collected_body_bytes = body.into_data_stream().collect().await.unwrap().to_bytes();
let expected_body_bytes = include_bytes!("../../test_assets/small/app.js");
assert_eq!(*collected_body_bytes, *expected_body_bytes);
}
#[tokio::test]
async fn router_created_uncompressed_because_not_worthwhile() {
embed_assets!("../static-serve/test_assets/small", compress = true);
let router: Router<()> = static_router();
assert!(router.has_routes());
let request = create_request("/app.js", &Compression::Zstd);
let response = get_response(router, request).await;
let (parts, body) = response.into_parts();
assert!(parts.status.is_success());
assert_eq!(
parts.headers.get("content-type").unwrap(),
"text/javascript"
);
assert!(parts.headers.contains_key("etag"));
let collected_body_bytes = body.into_data_stream().collect().await.unwrap().to_bytes();
// Response should not be compressed since the benefit is insignificant
let expected_body_bytes = include_bytes!("../../test_assets/small/app.js");
assert_eq!(*collected_body_bytes, *expected_body_bytes);
}
#[tokio::test]
async fn router_created_compressed_zstd_only() {
embed_assets!("../static-serve/test_assets/big", compress = true);
let router: Router<()> = static_router();
assert!(router.has_routes());
let request = create_request("/app.js", &Compression::Zstd);
let response = get_response(router, request).await;
let (parts, body) = response.into_parts();
assert!(parts.status.is_success());
assert_eq!(
parts.headers.get(CONTENT_ENCODING),
Some(&HeaderValue::from_str("zstd").unwrap())
);
assert_eq!(
parts.headers.get("content-type").unwrap(),
"text/javascript"
);
assert!(parts.headers.contains_key("etag"));
let collected_body_bytes = body.into_data_stream().collect().await.unwrap().to_bytes();
// Decompress the response body
let decompressed_body = decompress_zstd(&collected_body_bytes);
assert_eq!(
decompressed_body,
include_bytes!("../../test_assets/big/app.js")
);
// Expect the compressed version
let expected_body_bytes = include_bytes!("../../test_assets/dist/app.js.zst");
assert_eq!(*collected_body_bytes, *expected_body_bytes);
}
#[tokio::test]
async fn router_created_compressed_gzip_only() {
embed_assets!("../static-serve/test_assets/big", compress = true);
let router: Router<()> = static_router();
assert!(router.has_routes());
let request = create_request("/app.js", &Compression::Gzip);
let response = get_response(router, request).await;
let (parts, body) = response.into_parts();
assert!(parts.status.is_success());
assert_eq!(
parts.headers.get(CONTENT_ENCODING),
Some(&HeaderValue::from_str("gzip").unwrap())
);
assert_eq!(
parts.headers.get("content-type").unwrap(),
"text/javascript"
);
assert!(parts.headers.contains_key("etag"));
let collected_body_bytes = body.into_data_stream().collect().await.unwrap().to_bytes();
let decompressed_body = decompress_gzip(&collected_body_bytes);
assert_eq!(
decompressed_body,
include_bytes!("../../test_assets/big/app.js"),
"decompressed body is not as expected"
);
// Expect the compressed version
let expected_body_bytes = include_bytes!("../../test_assets/dist/app.js.gz");
assert_eq!(*collected_body_bytes, *expected_body_bytes);
}
#[tokio::test]
async fn router_created_compressed_zstd_or_gzip_accepted() {
embed_assets!("../static-serve/test_assets/big", compress = true);
let router: Router<()> = static_router();
assert!(router.has_routes());
let request = create_request("/app.js", &Compression::Both);
let response = get_response(router, request).await;
let (parts, body) = response.into_parts();
assert!(parts.status.is_success());
assert_eq!(
parts.headers.get(CONTENT_ENCODING),
Some(&HeaderValue::from_str("zstd").unwrap())
);
assert_eq!(
parts.headers.get("content-type").unwrap(),
"text/javascript"
);
assert!(parts.headers.contains_key("etag"));
let collected_body_bytes = body.into_data_stream().collect().await.unwrap().to_bytes();
let decompressed_body = decompress_zstd(&collected_body_bytes);
assert_eq!(
decompressed_body,
include_bytes!("../../test_assets/big/app.js")
);
// Expect the compressed version
let expected_body_bytes = include_bytes!("../../test_assets/dist/app.js.zst");
assert_eq!(*collected_body_bytes, *expected_body_bytes);
}
#[tokio::test]
async fn router_created_ignore_dirs_one() {
embed_assets!("../static-serve/test_assets", ignore_dirs = ["dist"]);
let router: Router<()> = static_router();
assert!(router.has_routes());
let request = create_request("/small/app.js", &Compression::None);
let response = get_response(router, request).await;
let (parts, body) = response.into_parts();
assert!(parts.status.is_success());
let collected_body_bytes = body.into_data_stream().collect().await.unwrap().to_bytes();
let expected_body_bytes = include_bytes!("../../test_assets/small/app.js");
assert_eq!(
parts.headers.get("content-type").unwrap(),
"text/javascript"
);
assert!(parts.headers.contains_key("etag"));
assert_eq!(*collected_body_bytes, *expected_body_bytes);
}
#[tokio::test]
async fn router_created_ignore_dirs_three() {
embed_assets!(
"../static-serve/test_assets",
ignore_dirs = ["big", "small", "dist"]
);
let router: Router<()> = static_router();
// all directories ignored, so router has no routes
assert!(!router.has_routes());
let request = create_request("/app.js", &Compression::None);
let response = get_response(router, request).await;
let (parts, body) = response.into_parts();
let collected_body_bytes = body.into_data_stream().collect().await.unwrap().to_bytes();
// Expect a 404 Not Found with empty body
assert_eq!(parts.status, StatusCode::NOT_FOUND);
assert!(collected_body_bytes.is_empty());
}
#[tokio::test]
async fn handles_conditional_requests_same_etag() {
embed_assets!("../static-serve/test_assets/big", compress = true);
let router: Router<()> = static_router();
assert!(router.has_routes());
let request = create_request("/app.js", &Compression::Zstd);
let response = get_response(router.clone(), request).await;
let (parts, body) = response.into_parts();
assert!(parts.status.is_success());
assert_eq!(
parts.headers.get(CONTENT_ENCODING),
Some(&HeaderValue::from_str("zstd").unwrap())
);
assert_eq!(
parts.headers.get("content-type").unwrap(),
"text/javascript"
);
let etag = parts
.headers
.get("etag")
.expect("no etag header when there should be one!");
let collected_body_bytes = body.into_data_stream().collect().await.unwrap().to_bytes();
let decompressed_body = decompress_zstd(&collected_body_bytes);
assert_eq!(
decompressed_body,
include_bytes!("../../test_assets/big/app.js")
);
// Expect the compressed version
let expected_body_bytes = include_bytes!("../../test_assets/dist/app.js.zst");
assert_eq!(*collected_body_bytes, *expected_body_bytes);
let request = Request::builder()
.uri("/app.js")
.header(IF_NONE_MATCH, etag)
.header(ACCEPT_ENCODING, "zstd")
.body(Body::empty())
.unwrap();
let response = get_response(router, request).await;
let (parts, body) = response.into_parts();
assert_eq!(parts.status, StatusCode::NOT_MODIFIED);
assert_eq!(
parts
.headers
.get("content-length")
.expect("no content-length header!"),
"0"
);
let collected_body_bytes = body.into_data_stream().collect().await.unwrap().to_bytes();
assert!(collected_body_bytes.is_empty());
}
#[tokio::test]
async fn handles_conditional_requests_different_etag() {
embed_assets!("../static-serve/test_assets/big", compress = true);
let router: Router<()> = static_router();
assert!(router.has_routes());
let request = Request::builder()
.uri("/app.js")
.header(IF_NONE_MATCH, "n0t4r34l3t4g")
.header(ACCEPT_ENCODING, "zstd")
.body(Body::empty())
.unwrap();
let response = get_response(router, request).await;
let (parts, body) = response.into_parts();
assert_eq!(parts.status, StatusCode::OK);
assert_ne!(
parts
.headers
.get("content-length")
.expect("no content-length header!"),
"0",
"content length is unexpectedly zero!"
);
let collected_body_bytes = body.into_data_stream().collect().await.unwrap().to_bytes();
assert!(!collected_body_bytes.is_empty());
let decompressed_body = decompress_zstd(&collected_body_bytes);
assert_eq!(
decompressed_body,
include_bytes!("../../test_assets/big/app.js")
);
// Expect the compressed version
let expected_body_bytes = include_bytes!("../../test_assets/dist/app.js.zst");
assert_eq!(*collected_body_bytes, *expected_body_bytes);
}

20
test_assets/big/app.js Normal file
View File

@ -0,0 +1,20 @@
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");
console.log("Hello, world!");

View File

@ -0,0 +1,50 @@
body {
background: black ;
}
body {
background: black ;
}
body {
background: black ;
}
body {
background: black ;
}
body {
background: black ;
}

BIN
test_assets/dist/app.js.gz vendored Normal file

Binary file not shown.

BIN
test_assets/dist/app.js.zst vendored Normal file

Binary file not shown.

1
test_assets/dist/ignore_me_plz.txt vendored Normal file
View File

@ -0,0 +1 @@
This file should only get included and served when the default ignore list is disabled.

1
test_assets/small/app.js Normal file
View File

@ -0,0 +1 @@
console.log("Hello, world!");

View File

@ -0,0 +1 @@
body { background: black; }