haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use super::{Door, Lys, NOW, Response, TestResult, answer, request, site, written};
use flate2::read::GzDecoder;
use std::io::Read;

/// The head as text and the body as bytes, as the door wrote them.
fn written_bytes(response: Response) -> Result<(String, Vec<u8>), Box<dyn std::error::Error>> {
    let mut out = Vec::new();
    response.write(&mut out)?;
    let at = out
        .windows(4)
        .position(|window| window == b"\r\n\r\n")
        .ok_or("no head")?;
    let body = out.split_off(at + 4);
    Ok((String::from_utf8(out)?, body))
}

/// A content-hashed asset, as Vite names one.
const ASSET: &str = "/assets/index-BdF3x9kZ.js";

/// The first bytes of a PNG: compressed already, so never gzipped.
const PNG: [u8; 4] = [137, 80, 78, 71];

fn gunzip(bytes: &[u8]) -> Result<String, std::io::Error> {
    let mut text = String::new();
    GzDecoder::new(bytes).read_to_string(&mut text)?;
    Ok(text)
}

/// Fails while a new build could be hidden behind a cached index, or a
/// content-hashed asset is fetched again on every load: the hashed asset is
/// kept for good, and the index, the app's addresses and any other file are
/// `no-store`.
#[test]
fn a_hashed_asset_is_kept_for_good_and_the_index_is_never_stored() -> TestResult {
    let files = tempfile::tempdir()?;
    std::fs::create_dir(files.path().join("assets"))?;
    std::fs::write(files.path().join("index.html"), "<html>studio</html>")?;
    std::fs::write(files.path().join("app.js"), "let a = 1;")?;
    std::fs::write(files.path().join("assets/index-BdF3x9kZ.js"), "let b = 2;")?;
    let site = site(files.path());
    let door = Door::new(Lys);
    let hashed = written(answer(
        &door,
        &site,
        &request("GET /assets/index-BdF3x9kZ.js HTTP/1.1\r\n\r\n")?,
        NOW,
    ))?;
    assert!(
        hashed.contains("\r\nCache-Control: public, max-age=31536000, immutable\r\n"),
        "{hashed}"
    );
    assert!(!hashed.contains("no-store"), "{hashed}");
    for address in ["/", "/index.html", "/app.js", "/assets/absent-BdF3x9kZ.js"] {
        let answered = written(answer(
            &door,
            &site,
            &request(&format!("GET {address} HTTP/1.1\r\n\r\n"))?,
            NOW,
        ))?;
        assert!(
            answered.contains("\r\nCache-Control: no-store\r\n"),
            "{address}: {answered}"
        );
        assert!(!answered.contains("immutable"), "{address}: {answered}");
    }
    Ok(())
}

/// Fails while gzip reaches a browser that did not ask for it, a compressed
/// body does not decode to the file, or the index is answered from an older
/// build: gzip only when asked, decoding to the file, and the index read and
/// compressed again on every request.
#[test]
fn gzip_is_sent_only_when_asked_and_decodes_to_the_file() -> TestResult {
    let files = tempfile::tempdir()?;
    std::fs::create_dir(files.path().join("assets"))?;
    let script = "export const studio = 1;\n".repeat(100);
    std::fs::write(files.path().join(ASSET.trim_start_matches('/')), &script)?;
    std::fs::write(files.path().join("index.html"), "<html>first</html>")?;
    std::fs::write(files.path().join("logo.png"), PNG)?;
    let site = site(files.path());
    let door = Door::new(Lys);
    let get = |address: &str, accept: Option<&str>| {
        let accept = accept
            .map(|accept| format!("Accept-Encoding: {accept}\r\n"))
            .unwrap_or_default();
        request(&format!("GET {address} HTTP/1.1\r\n{accept}\r\n"))
            .map(|asked| answer(&door, &site, &asked, NOW))
    };
    let (plain, body) = written_bytes(get(ASSET, None)?)?;
    assert!(!plain.contains("Content-Encoding"), "{plain}");
    assert!(plain.contains("\r\nVary: Accept-Encoding\r\n"), "{plain}");
    assert_eq!(body, script.as_bytes());
    let (refused, body) = written_bytes(get(ASSET, Some("br, gzip;q=0"))?)?;
    assert!(!refused.contains("Content-Encoding"), "{refused}");
    assert_eq!(body, script.as_bytes());
    let (packed, body) = written_bytes(get(ASSET, Some("gzip, deflate, br"))?)?;
    assert!(
        packed.contains("\r\nContent-Encoding: gzip\r\n"),
        "{packed}"
    );
    assert!(packed.contains("\r\nVary: Accept-Encoding\r\n"), "{packed}");
    assert!(
        packed.contains(&format!("\r\nContent-Length: {}\r\n", body.len())),
        "{packed}"
    );
    assert!(body.len() < script.len(), "gzip made it no smaller");
    assert_eq!(gunzip(&body)?, script);
    let (first, body) = written_bytes(get("/", Some("gzip"))?)?;
    assert!(first.contains("Content-Encoding: gzip"), "{first}");
    assert_eq!(gunzip(&body)?, "<html>first</html>");
    std::fs::write(files.path().join("index.html"), "<html>second</html>")?;
    let (_, body) = written_bytes(get("/", Some("gzip"))?)?;
    assert_eq!(gunzip(&body)?, "<html>second</html>");
    let (image, body) = written_bytes(get("/logo.png", Some("gzip"))?)?;
    assert!(!image.contains("Content-Encoding"), "{image}");
    assert!(!image.contains("Vary"), "{image}");
    assert_eq!(body, PNG);
    Ok(())
}