//! The static assets are served out of the binary.
//!
//! The logo and the stylesheet are compiled into the executable rather than
//! read from a `static/` directory next to the process, so they are served no
//! matter where the server was started from. Every test here starts the server
//! in an empty working directory, which is exactly the situation that used to
//! answer `404`: nothing to read from disk means nothing to serve unless the
//! bytes are in the binary.
//!
//! The bodies are compared against the files on disk, so a change to an asset
//! that is not picked up by the build fails here rather than silently shipping
//! a stale copy.
//!
//! The caching tests pin both halves of revalidation: a freshness window, so
//! the browser does not ask at all while it holds a copy, and a validator, so
//! that when it does ask after the window lapses the answer is a body free
//! `304` rather than the whole file again. The bytes are compiled into the
//! binary, so the validator must also be stable - it is the content's hash,
//! not anything about the running process.
mod support;
use support::{Client, Server, free_port, get};
#[tokio::test]
async fn stylesheet_is_served_from_the_binary() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let response = get(port, "/static/style.css");
assert_eq!(response.status, 200, "stylesheet");
assert_eq!(
response.content_type.as_deref(),
Some("text/css"),
"the stylesheet should be sent as CSS"
);
assert_eq!(
response.body,
include_str!("../static/style.css"),
"the stylesheet should be the file that was embedded"
);
Ok(())
}
#[tokio::test]
async fn logo_is_served_from_the_binary() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let response = get(port, "/static/frogue-logo.svg");
assert_eq!(response.status, 200, "logo");
assert_eq!(
response.content_type.as_deref(),
Some("image/svg+xml"),
"the logo should be sent as SVG"
);
assert_eq!(
response.body,
include_str!("../static/frogue-logo.svg"),
"the logo should be the file that was embedded"
);
Ok(())
}
#[tokio::test]
async fn an_unknown_asset_is_not_found() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let response = get(port, "/static/does-not-exist.css");
assert_eq!(response.status, 404, "unknown asset");
Ok(())
}
#[tokio::test]
async fn an_asset_cannot_escape_the_static_tree() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
// The embedded tree has no parent to walk into: anything that is not a
// known asset is simply absent, so traversal is a 404 rather than a read of
// something else on disk.
let response = get(port, "/static/../Cargo.toml");
assert_ne!(
response.status, 200,
"a path outside the static tree must not be served"
);
Ok(())
}
#[tokio::test]
async fn an_asset_is_marked_fresh_for_ten_minutes() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let response = get(port, "/static/style.css");
assert_eq!(response.status, 200, "stylesheet");
assert_eq!(
response.header("cache-control"),
Some("public, max-age=600"),
"the browser should be able to reuse its copy without asking again"
);
let etag = response.header("etag").unwrap_or_else(|| {
panic!("a cached asset needs a validator so it can be revalidated later")
});
assert!(
etag.starts_with('"') && etag.ends_with('"') && etag.len() > 2,
"the etag should be a quoted strong validator, got {etag:?}"
);
Ok(())
}
#[tokio::test]
async fn an_unchanged_asset_revalidates_with_a_body_free_304() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let first = get(port, "/static/style.css");
let etag = first
.header("etag")
.expect("the stylesheet should carry an etag")
.to_string();
let revalidated = Client::new(port).get_with("/static/style.css", &[("If-None-Match", &etag)]);
assert_eq!(
revalidated.status, 304,
"an asset whose etag matches must not be sent again"
);
assert!(
revalidated.body.is_empty(),
"a 304 carries no body, got {:?}",
revalidated.body
);
assert_eq!(
revalidated.header("etag"),
Some(etag.as_str()),
"the 304 should repeat the validator it matched"
);
Ok(())
}
#[tokio::test]
async fn a_stale_validator_gets_the_full_body() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let response =
Client::new(port).get_with("/static/style.css", &[("If-None-Match", "\"stale-etag\"")]);
assert_eq!(
response.status, 200,
"a validator from an older binary must not suppress the body"
);
assert_eq!(
response.body,
include_str!("../static/style.css"),
"the stylesheet should be the file that was embedded"
);
Ok(())
}
#[tokio::test]
async fn the_validator_is_stable_across_requests() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let first = get(port, "/static/style.css");
let second = get(port, "/static/style.css");
let first = first
.header("etag")
.expect("the stylesheet should carry an etag");
let second = second
.header("etag")
.expect("the stylesheet should carry an etag");
assert_eq!(
first, second,
"the embedded bytes cannot change under a running process, so the \
validator must not change either"
);
Ok(())
}