use crate::{CaseResult, TestCase};
use zerodds_hpack::{Decoder, Encoder, HeaderField};
use zerodds_http2::frame::DEFAULT_MAX_FRAME_SIZE;
use zerodds_http2::stream::StreamEvent;
use zerodds_http2::{
ErrorCode, Flags, FrameHeader, FrameType, StreamState, decode_frame, encode_frame,
};
fn case_1_1_frame_header_round_trip() -> CaseResult {
let h = FrameHeader {
length: 5,
frame_type: FrameType::Data,
flags: Flags(Flags::END_STREAM),
stream_id: 7,
};
let payload = alloc::vec![1u8, 2, 3, 4, 5];
let mut buf = alloc::vec![0u8; 32];
let n = match encode_frame(&h, &payload, &mut buf, DEFAULT_MAX_FRAME_SIZE) {
Ok(n) => n,
Err(e) => return CaseResult::Fail(alloc::format!("encode: {e}")),
};
match decode_frame(&buf[..n], DEFAULT_MAX_FRAME_SIZE) {
Ok((f, _)) if f.header == h && f.payload == payload => CaseResult::Pass,
Ok(_) => CaseResult::Fail("§4.1 frame round-trip mismatch".into()),
Err(e) => CaseResult::Fail(alloc::format!("decode: {e}")),
}
}
fn case_1_2_r_bit_stripped() -> CaseResult {
let mut buf = alloc::vec![0u8; 9];
buf[3] = FrameType::Settings as u8;
buf[5] = 0x80; match decode_frame(&buf, DEFAULT_MAX_FRAME_SIZE) {
Ok((f, _)) if f.header.stream_id == 0 => CaseResult::Pass,
_ => CaseResult::Fail("§4.1 R-bit must be stripped".into()),
}
}
fn case_1_3_state_idle_to_open() -> CaseResult {
use zerodds_http2::stream::transition;
match transition(StreamState::Idle, StreamEvent::RecvHeaders) {
Ok(StreamState::Open) => CaseResult::Pass,
_ => CaseResult::Fail("§5.1 idle→open via RecvHeaders".into()),
}
}
fn case_1_4_stream_reset_terminates() -> CaseResult {
use zerodds_http2::stream::transition;
for s in [
StreamState::Open,
StreamState::HalfClosedLocal,
StreamState::HalfClosedRemote,
] {
match transition(s, StreamEvent::Reset) {
Ok(StreamState::Closed) => {}
_ => return CaseResult::Fail(alloc::format!("§5.1: reset from {s:?}")),
}
}
CaseResult::Pass
}
fn case_1_5_error_code_table_complete() -> CaseResult {
for v in 0x0..=0xdu32 {
if (ErrorCode::from_u32(v) as u32) != v {
return CaseResult::Fail(alloc::format!("§7: error code {v:#x}"));
}
}
if (ErrorCode::from_u32(0xfff0) as u32) >= 0xd {
if ErrorCode::from_u32(0xfff0) != ErrorCode::InternalError {
return CaseResult::Fail("§7: unknown should map to InternalError".into());
}
}
CaseResult::Pass
}
fn case_2_1_integer_c1_test_vectors() -> CaseResult {
let buf = zerodds_hpack::encode_integer(10, 5, 0);
if buf != alloc::vec![0x0au8] {
return CaseResult::Fail("§C.1.1".into());
}
let buf = zerodds_hpack::encode_integer(1337, 5, 0);
if buf != alloc::vec![0x1fu8, 0x9a, 0x0a] {
return CaseResult::Fail("§C.1.2".into());
}
CaseResult::Pass
}
fn case_2_2_static_table_61_entries() -> CaseResult {
use zerodds_hpack::STATIC_TABLE;
if STATIC_TABLE.len() == 61
&& STATIC_TABLE[0].name == ":authority"
&& STATIC_TABLE[1].name == ":method"
&& STATIC_TABLE[1].value == "GET"
&& STATIC_TABLE[60].name == "www-authenticate"
{
CaseResult::Pass
} else {
CaseResult::Fail("Appendix A static table layout".into())
}
}
fn case_2_3_encode_decode_round_trip() -> CaseResult {
let mut e = Encoder::new();
let mut d = Decoder::new();
let headers = alloc::vec![
HeaderField {
name: ":method".into(),
value: "GET".into(),
},
HeaderField {
name: ":scheme".into(),
value: "https".into(),
},
HeaderField {
name: "custom-key".into(),
value: "custom-value".into(),
},
];
let buf = e.encode(&headers);
match d.decode(&buf) {
Ok(decoded) if decoded == headers => CaseResult::Pass,
_ => CaseResult::Fail("HPACK encode/decode round-trip".into()),
}
}
fn case_2_4_huffman_c4_1_www_example_com() -> CaseResult {
let enc = zerodds_hpack::huffman::encode(b"www.example.com");
let expected = [
0xf1u8, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff,
];
if enc == expected {
CaseResult::Pass
} else {
CaseResult::Fail("§C.4.1 huffman test vector".into())
}
}
fn case_3_1_lpm_round_trip() -> CaseResult {
use zerodds_grpc_bridge::{decode_message, encode_message};
let payload = b"hello world";
let lpm = match encode_message(payload, false) {
Ok(b) => b,
Err(e) => return CaseResult::Fail(alloc::format!("LPM encode: {e:?}")),
};
match decode_message(&lpm) {
Ok((_compressed, msg, _consumed)) if msg == payload => CaseResult::Pass,
_ => CaseResult::Fail("LPM round-trip".into()),
}
}
fn case_3_2_path_parse() -> CaseResult {
use zerodds_grpc_bridge::parse_path;
match parse_path("/dds.demo.Trader/PlaceOrder") {
Ok((s, m)) if s == "dds.demo.Trader" && m == "PlaceOrder" => CaseResult::Pass,
_ => CaseResult::Fail("path parse `/<service>/<method>`".into()),
}
}
pub const SUITE: &[TestCase] = &[
TestCase {
name: "rfc7540-4.1-frame-roundtrip",
run: case_1_1_frame_header_round_trip,
},
TestCase {
name: "rfc7540-4.1-r-bit-stripped",
run: case_1_2_r_bit_stripped,
},
TestCase {
name: "rfc7540-5.1-idle-to-open",
run: case_1_3_state_idle_to_open,
},
TestCase {
name: "rfc7540-5.1-stream-reset",
run: case_1_4_stream_reset_terminates,
},
TestCase {
name: "rfc7540-7-error-codes",
run: case_1_5_error_code_table_complete,
},
TestCase {
name: "rfc7541-c1-integer-vectors",
run: case_2_1_integer_c1_test_vectors,
},
TestCase {
name: "rfc7541-appendix-a-static-table",
run: case_2_2_static_table_61_entries,
},
TestCase {
name: "rfc7541-encode-decode-roundtrip",
run: case_2_3_encode_decode_round_trip,
},
TestCase {
name: "rfc7541-c4.1-huffman-www-example",
run: case_2_4_huffman_c4_1_www_example_com,
},
TestCase {
name: "grpc-lpm-roundtrip",
run: case_3_1_lpm_round_trip,
},
TestCase {
name: "grpc-path-parse",
run: case_3_2_path_parse,
},
];
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn full_suite_passes() {
let (p, s, f) = crate::run_suite(SUITE);
assert_eq!(f, 0, "no h2spec/gRPC cases must fail");
assert_eq!(p + s, SUITE.len());
assert!(p >= 10);
}
}