spikard-core 0.16.1

Shared transport-agnostic primitives for Spikard runtimes
Documentation
use brotli::Decompressor;
use flate2::read::GzDecoder;
use spikard_core::{CompressionConfig, RawResponse, StaticAsset};
use std::collections::HashMap;
use std::io::Read;

fn decode_gzip(payload: &[u8]) -> Vec<u8> {
    let mut decoder = GzDecoder::new(payload);
    let mut buf = Vec::new();
    decoder.read_to_end(&mut buf).expect("gzip decode failed");
    buf
}

fn decode_brotli(payload: &[u8]) -> Vec<u8> {
    let mut decoder = Decompressor::new(payload, 4096);
    let mut buf = Vec::new();
    decoder.read_to_end(&mut buf).expect("brotli decode failed");
    buf
}

#[test]
fn raw_response_does_not_compress_empty_or_partial_content() {
    let mut headers = HashMap::new();
    headers.insert("content-type".to_string(), "application/json".to_string());

    let mut empty = RawResponse::new(200, headers.clone(), Vec::new());
    let cfg = CompressionConfig {
        min_size: 1,
        ..Default::default()
    };
    empty.apply_compression(&HashMap::new(), &cfg);
    assert!(!empty.headers.contains_key("content-encoding"));
    assert!(empty.body.is_empty());

    let mut partial = RawResponse::new(206, headers, b"{\"ok\":true}".to_vec());
    partial.apply_compression(&HashMap::new(), &cfg);
    assert!(!partial.headers.contains_key("content-encoding"));
}

#[test]
fn raw_response_respects_existing_content_encoding_and_min_size() {
    let mut headers = HashMap::new();
    headers.insert("content-encoding".to_string(), "gzip".to_string());

    let mut response = RawResponse::new(200, headers, b"x".repeat(4096));
    let cfg = CompressionConfig::default();
    response.apply_compression(&HashMap::new(), &cfg);
    assert_eq!(
        response.headers.get("content-encoding").map(String::as_str),
        Some("gzip")
    );

    let mut small = RawResponse::new(200, HashMap::new(), b"x".repeat(10));
    let cfg = CompressionConfig {
        min_size: 1024,
        ..Default::default()
    };
    small.apply_compression(&HashMap::new(), &cfg);
    assert!(!small.headers.contains_key("content-encoding"));
}

#[test]
fn raw_response_prefers_brotli_when_accepted() {
    let original = b"hello world ".repeat(256);
    let mut response = RawResponse::new(200, HashMap::new(), original.clone());

    let mut request_headers = HashMap::new();
    request_headers.insert("Accept-Encoding".to_string(), "br, gzip".to_string());

    let cfg = CompressionConfig {
        min_size: 1,
        quality: 6,
        gzip: true,
        brotli: true,
    };

    response.apply_compression(&request_headers, &cfg);

    assert_eq!(response.headers.get("content-encoding").map(String::as_str), Some("br"));
    assert_eq!(
        response.headers.get("vary").map(String::as_str),
        Some("Accept-Encoding")
    );
    assert_eq!(decode_brotli(&response.body), original);
}

#[test]
fn raw_response_falls_back_to_gzip() {
    let original = b"hello world ".repeat(256);
    let mut response = RawResponse::new(200, HashMap::new(), original.clone());

    let mut request_headers = HashMap::new();
    request_headers.insert("Accept-Encoding".to_string(), "gzip".to_string());

    let cfg = CompressionConfig {
        min_size: 1,
        quality: 6,
        gzip: true,
        brotli: true,
    };

    response.apply_compression(&request_headers, &cfg);

    assert_eq!(
        response.headers.get("content-encoding").map(String::as_str),
        Some("gzip")
    );
    assert_eq!(decode_gzip(&response.body), original);
}

#[test]
fn static_asset_serves_get_and_head_only() {
    let mut headers = HashMap::new();
    headers.insert("content-type".to_string(), "text/plain".to_string());

    let asset = StaticAsset {
        route: "/static/hello.txt".to_string(),
        headers,
        body: b"hello".to_vec(),
    };

    let get = asset.serve("GET", "/static/hello.txt").expect("expected GET response");
    assert_eq!(get.status, 200);
    assert_eq!(get.body, b"hello");
    assert_eq!(get.headers.get("content-length").map(String::as_str), Some("5"));

    let head = asset
        .serve("HEAD", "/static/hello.txt")
        .expect("expected HEAD response");
    assert_eq!(head.status, 200);
    assert!(head.body.is_empty());
    assert_eq!(head.headers.get("content-length").map(String::as_str), Some("5"));

    assert!(asset.serve("POST", "/static/hello.txt").is_none());
    assert!(asset.serve("GET", "/static/other.txt").is_none());
}