sark-core 0.7.0

the L7 layer for dope
Documentation
use http::StatusCode;
use o3::buffer::{Owned, Shared};

use super::*;
use crate::http::request::LocalFrameBytes;

#[test]
fn response_ok_has_200_status() {
    let resp = Response::ok();
    assert_eq!(resp.status(), StatusCode::OK);
}

#[test]
fn response_not_found_has_404_status() {
    let resp = Response::not_found();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[test]
fn response_text_sets_content_type_and_body() {
    let resp = Response::text("hello");
    assert_eq!(resp.status(), StatusCode::OK);
    assert_eq!(resp.headers().get("content-type").unwrap(), "text/plain");
    assert_eq!(resp.body_str(), Some("hello"));
}

#[test]
fn response_text_with_status_sets_status_and_body() {
    let resp = Response::text_with_status(StatusCode::CREATED, "ok");
    assert_eq!(resp.status(), StatusCode::CREATED);
    assert_eq!(resp.body_str(), Some("ok"));
}

#[test]
fn response_json_sets_content_type_and_serialized_body() {
    let value = serde_json::json!({ "k": "v" });
    let resp = Response::json(&value).unwrap();

    assert_eq!(resp.status(), StatusCode::OK);
    assert_eq!(
        resp.headers().get("content-type").unwrap(),
        "application/json"
    );
    assert_eq!(resp.body_str(), Some("{\"k\":\"v\"}"));
}

#[test]
fn response_json_with_status_sets_custom_status() {
    let value = serde_json::json!({ "ok": true });
    let resp = Response::json_with_status(StatusCode::ACCEPTED, &value).unwrap();

    assert_eq!(resp.status(), StatusCode::ACCEPTED);
    assert_eq!(resp.body_str(), Some("{\"ok\":true}"));
}

#[test]
fn insert_header_adds_header_retrievable_via_headers() {
    let mut resp = Response::ok();
    resp.insert_header(
        http::header::HeaderName::from_static("x-test"),
        http::HeaderValue::from_static("1"),
    );

    assert_eq!(resp.headers().get("x-test").unwrap(), "1");
}

#[test]
fn content_type_sets_content_type_header() {
    let mut resp = Response::ok();
    resp.content_type("text/html");
    assert_eq!(resp.headers().get("content-type").unwrap(), "text/html");
}

#[test]
fn set_body_str_and_body_str_round_trip() {
    let mut resp = Response::ok();
    resp.set_body_str("hello");
    assert_eq!(resp.body_str(), Some("hello"));
}

#[test]
fn into_body_returns_owned_bytes_mut() {
    let mut resp = Response::ok();
    resp.set_body_str("owned");
    let body = resp.into_body();
    assert_eq!(body, Owned::from(&b"owned"[..]));
}

#[test]
fn body_str_returns_none_for_non_utf8() {
    let mut resp = Response::ok();
    resp.set_body(Owned::from(&[0xff_u8, 0xfe_u8][..]));
    assert_eq!(resp.body_str(), None);
}

#[test]
fn is_chunked_false_by_default() {
    let resp = Response::ok();
    assert!(!resp.is_chunked());
}

#[test]
fn push_chunk_makes_response_chunked() {
    let mut resp = Response::ok();
    resp.push_chunk(Shared::from_static(b"first"));
    assert!(resp.is_chunked());
}

#[test]
fn chunked_parts_returns_chunks_in_order() {
    let mut resp = Response::ok();
    resp.push_chunk(Shared::from_static(b"a"));
    resp.push_chunk(Shared::from_static(b"b"));
    let parts = resp.chunked_parts().unwrap();
    assert_eq!(
        parts,
        &[Shared::from_static(b"a"), Shared::from_static(b"b")]
    );
}

#[test]
fn wire_headers_append_and_expose_encoded_lines() {
    let mut resp = Response::ok();
    resp.append_wire_header_static("cache-control", "no-cache");
    resp.append_wire_header("x-bench-key", "k01");
    assert!(resp.has_wire_headers());
    assert_eq!(
        resp.wire_headers(),
        b"cache-control: no-cache\r\nx-bench-key: k01\r\n"
    );
}

#[test]
#[should_panic(expected = "managed headers")]
fn wire_headers_reject_managed_header_name() {
    let mut resp = Response::ok();
    let _ = resp.append_wire_header_static("content-length", "10");
}

#[test]
fn response_accepts_local_frame_body_without_eager_copy() {
    let mut resp = Response::ok();
    resp.set_body(LocalFrameBytes::from_slice(b"xxpayloadyy").slice(2..9));
    assert_eq!(resp.body(), b"payload");
    assert!(resp.body_is_shared());
}

#[test]
fn response_accepts_text_body_via_shared_bytes() {
    let mut body = TextBody::new();
    body.push_static(b"ok:");
    body.push_local(LocalFrameBytes::from_slice(b"xxk01yy").slice(2..5));
    body.push_static(b":1");

    let mut resp = Response::ok();
    resp.set_body(body);

    assert_eq!(resp.body(), b"ok:k01:1");
    assert!(resp.body_is_shared());
}

#[test]
fn text_body_from_items_materializes_shared_bytes() {
    let body = TextBody::from_items([
        TextItem::Static(b"ok:"),
        TextItem::Local(LocalFrameBytes::from_slice(b"xxk01yy").slice(2..5)),
        TextItem::Static(b":1"),
    ]);

    let mut resp = Response::ok();
    resp.set_body(body);

    assert_eq!(resp.body(), b"ok:k01:1");
    assert!(resp.body_is_shared());
}

#[test]
fn direct_response_plan_push_token_writes_header_line() {
    let mut plan = ResponsePlan::ok();
    let name = HeaderNameToken::new("x-bench-key");
    let _ = plan.push_token(name, "k01");
    assert_eq!(plan.wire_headers().as_ref(), b"x-bench-key: k01\r\n");
}

#[test]
fn direct_response_plan_push_token_static_writes_header_line() {
    let mut plan = ResponsePlan::ok();
    let name = HeaderNameToken::new("cache-control");
    let value = HeaderStaticValueToken::new("no-cache");
    let _ = plan.push_token_static(name, value);
    assert_eq!(plan.wire_headers().as_ref(), b"cache-control: no-cache\r\n");
}

#[test]
fn direct_response_plan_push_token_small_keeps_header_line() {
    let mut plan = ResponsePlan::ok();
    let name = HeaderNameToken::new("x-bench-nonce");
    let _ = plan.push_token(name, "100008");
    assert_eq!(plan.wire_headers().as_ref(), b"x-bench-nonce: 100008\r\n");
}

#[test]
fn direct_response_plan_respond_mono_keeps_wire_parts() {
    let mut plan = ResponsePlan::ok();
    let _ = plan.push("x-bench-key", "k01");
    let mut body = TextBody::new();
    body.push_static(b"o");
    body.push_static(b"k");
    let response = plan.respond_mono(body);
    let head = &response.head;
    match head {
        HotHeadInner::Wire(_) => panic!("direct mono response must preserve direct headers"),
        HotHeadInner::Direct(head) => {
            assert_eq!(head.static_headers(), b"");
            assert_eq!(head.headers().wire_len(), b"x-bench-key: k01\r\n".len());
        }
    }
    let resp = Response::from(response);

    assert_eq!(resp.status(), StatusCode::OK);
    assert!(resp.headers().is_empty());
    assert_eq!(resp.wire_headers(), b"x-bench-key: k01\r\n");
    assert_eq!(resp.body(), b"ok");
}

#[test]
fn response_plan_respond_text_keeps_text_body() {
    let plan = ResponsePlan::ok();
    let body = TextBody::from_items([
        TextItem::Static(b"ok:"),
        TextItem::Static(b"k01"),
        TextItem::Static(b":"),
        TextItem::Static(b"1"),
    ]);
    let response = plan.respond_text(body);

    assert_eq!(response.status(), StatusCode::OK);
    let mut buf = vec![0u8; response.body.body_len()];
    response.body.write_to(&mut buf);
    assert_eq!(&buf[..], b"ok:k01:1");
    assert!(response.headers().is_empty());
}

#[test]
fn direct_response_plan_respond_text_keeps_direct_parts() {
    let mut plan = ResponsePlan::ok();
    let _ = plan.push("x-bench-key", "k01");
    let body = TextBody::from_items([TextItem::Static(b"ok:"), TextItem::Static(b"k01")]);
    let response = plan.respond_text(body);

    assert_eq!(response.status(), StatusCode::OK);
    let mut buf = vec![0u8; response.body.body_len()];
    response.body.write_to(&mut buf);
    assert_eq!(&buf[..], b"ok:k01");
    let resp = Response::from(response);
    assert_eq!(resp.wire_headers(), b"x-bench-key: k01\r\n");
}

#[test]
fn custom_into_serve_response_builds_direct_text_late() {
    #[derive(Clone)]
    struct Reply {
        key: LocalFrameBytes,
        nonce: LocalFrameBytes,
    }

    impl IntoServeResponse<'static> for Reply {
        fn into_serve_response(self) -> Serve {
            let mut plan = ResponsePlan::from_static(StatusCode::OK, b"");
            let key_name = HeaderNameToken::new("x-bench-key");
            let nonce_name = HeaderNameToken::new("x-bench-nonce");
            let _ = plan.push_local_token(key_name, self.key.clone());
            let _ = plan.push_local_token(nonce_name, self.nonce.clone());
            let mut body = TextBody::new();
            let _ = body.push_static(b"ok:");
            let _ = body.push_local(self.key);
            let _ = body.push_static(b":");
            let _ = body.push_local(self.nonce);
            plan.respond_text(body).into_serve_response()
        }
    }

    let reply = Reply {
        key: LocalFrameBytes::from_slice(b"xxk01yy").slice(2..5),
        nonce: LocalFrameBytes::from_slice(b"00100008").slice(2..8),
    };
    let response = reply.into_serve_response();
    let resp = response.into_response();
    assert_eq!(resp.body(), b"ok:k01:100008");
    assert_eq!(
        resp.wire_headers(),
        b"x-bench-key: k01\r\nx-bench-nonce: 100008\r\n"
    );
}

#[test]
fn fixed_response_write_into_slice_round_trips_plaintext() {
    let resp = FixedResponse::direct(
        StatusCode::OK,
        b"Content-Type: text/plain\r\n",
        Headers::new(),
        Shared::from_static(b"Hello, World!"),
    );
    let date: [u8; 29] = *b"Sun, 06 Nov 1994 08:49:37 GMT";
    let mut out = [0u8; 256];
    let n = resp.write_into_slice(&mut out, &date).expect("fits");
    let wire = std::str::from_utf8(&out[..n]).expect("ascii");
    assert!(
        wire.starts_with("HTTP/1.1 200 OK\r\n"),
        "status line: {wire:?}"
    );
    assert!(wire.contains("Content-Type: text/plain\r\n"));
    assert!(wire.contains("Content-Length: 13\r\n"));
    assert!(wire.contains("Server: sark\r\n"));
    assert!(wire.contains("Date: Sun, 06 Nov 1994 08:49:37 GMT\r\n"));
    assert!(wire.ends_with("\r\n\r\nHello, World!"));
}

#[test]
fn fixed_response_write_into_slice_returns_none_when_too_small() {
    let resp = FixedResponse::direct(
        StatusCode::OK,
        b"",
        Headers::new(),
        Shared::from_static(b"body"),
    );
    let date = *b"Sun, 06 Nov 1994 08:49:37 GMT";
    let mut tiny = [0u8; 32];
    assert!(resp.write_into_slice(&mut tiny, &date).is_none());
}

#[test]
fn mono_write_head_only_emits_head_then_returns_static_body() {
    static BODY: &[u8] = b"hello world body";
    let resp = MonoResponseInner::from_static_slice_body(
        StatusCode::OK,
        b"cache-control: no-store\r\n",
        Headers::new(),
        BODY,
    );
    let date = *b"Sun, 06 Nov 1994 08:49:37 GMT";
    let mut out = vec![0u8; 4096];
    let (n, body_out) = resp.write_head_only(&mut out, &date).expect("emit ok");
    let head = std::str::from_utf8(&out[..n]).expect("ascii head");
    assert!(
        head.starts_with("HTTP/1.1 200 OK\r\n"),
        "wire prefix: {head:?}"
    );
    assert!(
        head.contains("Content-Length: 16\r\n"),
        "no Content-Length match: {head:?}"
    );
    assert!(
        head.contains("cache-control: no-store\r\n"),
        "no static header: {head:?}"
    );
    assert!(
        head.contains("Server: sark\r\n"),
        "no server line: {head:?}"
    );
    assert!(
        head.contains("Date: Sun, 06 Nov 1994 08:49:37 GMT\r\n"),
        "no date line: {head:?}"
    );
    assert!(head.ends_with("\r\n\r\n"), "no head terminator: {head:?}");
    assert_eq!(body_out.as_ptr(), BODY.as_ptr());
    assert_eq!(body_out.len(), BODY.len());
}

#[test]
fn mono_write_into_slice_with_static_body_emits_head_and_body() {
    static BODY: &[u8] = b"abcd";
    let resp = MonoResponseInner::from_static_slice_body(StatusCode::OK, b"", Headers::new(), BODY);
    let date = *b"Sun, 06 Nov 1994 08:49:37 GMT";
    let mut out = vec![0u8; 1024];
    let n = resp.write_into_slice(&mut out, &date).expect("emit ok");
    let wire = &out[..n];
    let body_off = wire.windows(4).position(|w| w == b"\r\n\r\n").unwrap() + 4;
    assert_eq!(&wire[body_off..n], BODY);
}

#[test]
fn dyn_write_head_only_forwards_to_mono() {
    static BODY: &[u8] = b"forwarded";
    let mono = MonoResponseInner::from_static_slice_body(StatusCode::OK, b"", Headers::new(), BODY);
    let dyn_resp = ServeInner::Mono(mono);
    let date = *b"Sun, 06 Nov 1994 08:49:37 GMT";
    let mut out = vec![0u8; 1024];
    let (n, body_out) = dyn_resp
        .write_head_only(&mut out, &date)
        .expect("Mono forward");
    assert!(n > 0);
    assert_eq!(body_out.as_ptr(), BODY.as_ptr());
}