mod common;
use std::fs;
use std::io::Write;
use std::sync::{Arc, Mutex};
use mini_static::Server;
use tempfile::TempDir;
const CAP: usize = 8 * 1024 * 1024;
#[derive(Clone, Default)]
struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
impl SharedBuffer {
fn contents(&self) -> String {
String::from_utf8(self.0.lock().unwrap().clone()).unwrap()
}
}
impl Write for SharedBuffer {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn write_html_of_size(root: &TempDir, name: &str, size: usize) {
let prefix = b"<html><body>Hello<!--";
let suffix = b"--></body></html>";
let padding = size - prefix.len() - suffix.len();
let mut contents = Vec::with_capacity(size);
contents.extend_from_slice(prefix);
contents.resize(prefix.len() + padding, b'x');
contents.extend_from_slice(suffix);
assert_eq!(contents.len(), size);
fs::write(root.path().join(name), contents).unwrap();
}
#[tokio::test]
async fn an_html_page_at_the_cap_is_still_injected() {
let root = TempDir::new().unwrap();
write_html_of_size(&root, "big.html", CAP);
let server = Server::new(root.path()).unwrap().with_spa_mode();
let response = common::get(&server, "/big.html").await;
let body = common::body_bytes(response).await;
assert!(
String::from_utf8_lossy(&body).contains("mini-static:navigate"),
"a page exactly at the cap must still be injected"
);
}
#[tokio::test]
async fn an_html_page_over_the_cap_is_served_unmodified_and_logged() {
let root = TempDir::new().unwrap();
write_html_of_size(&root, "huge.html", CAP + 1);
let log = SharedBuffer::default();
let server = Server::new(root.path())
.unwrap()
.with_spa_mode()
.with_request_logging_to(Box::new(log.clone()));
let response = common::get(&server, "/huge.html").await;
assert_eq!(response.status().as_u16(), 200);
let body = common::body_bytes(response).await;
assert_eq!(
body.len(),
CAP + 1,
"the whole file must still be served, byte for byte"
);
assert!(
!String::from_utf8_lossy(&body).contains("mini-static:navigate"),
"an over-cap page must not be injected"
);
let logged = log.contents();
assert!(
logged.contains("html injection skipped for /huge.html"),
"the skip must be logged, got: {logged:?}"
);
}
#[tokio::test]
async fn the_cap_applies_to_live_reload_injection_too() {
let root = TempDir::new().unwrap();
write_html_of_size(&root, "huge.html", CAP + 1);
write_html_of_size(&root, "small.html", 1024);
let server = Server::new(root.path()).unwrap().with_live_reload();
let (port, handle) = server.run_ephemeral().await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
let small = fetch(port, "/small.html").await;
assert!(
small.contains("EventSource"),
"live-reload injection should be active on this server"
);
let huge = fetch(port, "/huge.html").await;
assert!(
!huge.contains("EventSource"),
"an over-cap page must not get the reload script"
);
handle.shutdown().await;
}
async fn fetch(port: u16, path: &str) -> String {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{port}"))
.await
.unwrap();
stream
.write_all(
format!("GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
.as_bytes(),
)
.await
.unwrap();
let mut response = Vec::new();
stream.read_to_end(&mut response).await.unwrap();
String::from_utf8_lossy(&response).into_owned()
}
#[tokio::test]
async fn an_ordinary_page_is_still_injected() {
let root = TempDir::new().unwrap();
fs::write(
root.path().join("small.html"),
b"<html><body>Hi</body></html>",
)
.unwrap();
let server = Server::new(root.path()).unwrap().with_spa_mode();
let body = common::body_bytes(common::get(&server, "/small.html").await).await;
assert!(String::from_utf8_lossy(&body).contains("mini-static:navigate"));
}
#[tokio::test]
async fn no_skip_is_logged_for_an_injected_page() {
let root = TempDir::new().unwrap();
fs::write(
root.path().join("small.html"),
b"<html><body>Hi</body></html>",
)
.unwrap();
let log = SharedBuffer::default();
let server = Server::new(root.path())
.unwrap()
.with_spa_mode()
.with_request_logging_to(Box::new(log.clone()));
common::get(&server, "/small.html").await;
assert!(
!log.contents().contains("html injection skipped"),
"got: {:?}",
log.contents()
);
}