use super::receipt_fragments::{
Assembler, EncodedReceipt, FragmentEnvelope, FragmentPart, ReceiptCodecError, minimum_cap,
reserve_bytes,
};
use super::receipt_wire::WireReceipt;
use super::receipt_wire_tests::fixture;
use std::error::Error;
type TestResult = Result<(), Box<dyn Error>>;
fn encoded() -> Result<EncodedReceipt, Box<dyn Error>> {
let encoded = EncodedReceipt::project(&fixture()?, None)?;
Ok(encoded)
}
#[test]
fn complete_receipt_roundtrips_through_real_bounded_frames() -> TestResult {
let encoded = encoded()?;
let cap = u32::try_from(minimum_cap()?)?;
assert!(encoded.bytes().len() > usize::try_from(cap)?);
let frames = encoded.frames("haem-call", cap)?;
assert!(frames.len() > 2);
let limit = std::num::NonZeroU32::new(cap).ok_or("receipt cap is zero")?;
let mut transport = Vec::new();
for body in &frames {
super::frame::write(&mut transport, body, limit)?;
}
let mut input = std::io::Cursor::new(transport);
let mut assembler = Assembler::new("haem-call", cap)?;
let mut result = None;
while let Some(body) = super::frame::read(&mut input, limit)? {
let next = assembler.accept(&body)?;
if next.is_some() {
assert!(result.is_none());
result = next;
}
}
let complete = result.ok_or("assembler never completed the receipt")?;
let receipt = complete.receipt().ok_or("packet has no receipt")?;
assert_eq!(receipt, &WireReceipt::project(&fixture()?)?);
Ok(())
}
#[test]
fn low_cap_names_both_actual_numbers() -> TestResult {
let minimum = minimum_cap()?;
let offered = u32::try_from(minimum - 1)?;
let Err(error) = encoded()?.frames("haem-call", offered) else {
return Err("frames accepted a cap below the minimum".into());
};
assert!(
matches!(error, ReceiptCodecError::FrameTooSmall { offered: n, minimum: m } if n == offered && m == minimum)
);
let message = error.to_string();
assert!(message.contains("receipt_frame_too_small"));
assert!(message.contains(&offered.to_string()));
assert!(message.contains(&minimum.to_string()));
Ok(())
}
fn change(
frame: &[u8],
mutate: impl FnOnce(&mut FragmentEnvelope) -> TestResult,
) -> Result<Vec<u8>, Box<dyn Error>> {
let mut envelope: FragmentEnvelope = serde_json::from_slice(frame)?;
mutate(&mut envelope)?;
let changed = serde_json::to_vec(&envelope)?;
Ok(changed)
}
#[test]
fn dropped_duplicate_and_out_of_order_fragments_never_complete() -> TestResult {
let cap = u32::try_from(minimum_cap()?)?;
let frames = encoded()?.frames("id", cap)?;
let mut missing = Assembler::new("id", cap)?;
missing.accept(&frames[0])?;
assert!(matches!(
missing.accept(&frames[2]),
Err(ReceiptCodecError::Sequence { .. })
));
assert!(matches!(
missing.connection_closed(),
Err(ReceiptCodecError::ConnectionClosed)
));
let mut duplicate = Assembler::new("id", cap)?;
duplicate.accept(&frames[0])?;
assert!(matches!(
duplicate.accept(&frames[0]),
Err(ReceiptCodecError::Sequence { .. })
));
let mut order = Assembler::new("id", cap)?;
assert!(matches!(
order.accept(&frames[1]),
Err(ReceiptCodecError::Sequence { .. })
));
Ok(())
}
#[test]
fn terminal_digest_count_and_extent_are_verified() -> TestResult {
let cap = u32::try_from(minimum_cap()?)?;
let frames = encoded()?.frames("id", cap)?;
let terminal = frames.last().ok_or("encoded receipt has no frames")?;
for fault in 0..3 {
let last = change(terminal, |e| {
let FragmentPart::End {
digest,
outcomes,
bytes,
..
} = &mut e.part
else {
return Err("terminal missing".into());
};
match fault {
0 => *digest = "00".repeat(32),
1 => *outcomes += 1,
2 => *bytes += 1,
_ => unreachable!(),
}
Ok(())
})?;
let mut assembler = Assembler::new("id", cap)?;
for frame in &frames[..frames.len() - 1] {
assert!(assembler.accept(frame)?.is_none());
}
let Err(error) = assembler.accept(&last) else {
return Err("a tampered terminal fragment was accepted".into());
};
assert!(matches!(
(fault, error),
(0, ReceiptCodecError::Digest)
| (1, ReceiptCodecError::Count { .. })
| (2, ReceiptCodecError::Extent { .. })
));
assert!(matches!(
assembler.connection_closed(),
Err(ReceiptCodecError::ConnectionClosed)
));
}
Ok(())
}
#[test]
fn allocation_refusal_uses_actual_checked_reservation() {
let mut bytes = Vec::new();
assert!(matches!(
reserve_bytes(&mut bytes, usize::MAX),
Err(ReceiptCodecError::Allocation { .. })
));
assert!(bytes.is_empty());
}
#[test]
fn connection_close_id_hex_decode_and_missing_terminal_are_named() -> TestResult {
let cap = u32::try_from(minimum_cap()?)?;
let frames = encoded()?.frames("id", cap)?;
let mut closed = Assembler::new("id", cap)?;
closed.accept(&frames[0])?;
assert!(matches!(
closed.connection_closed(),
Err(ReceiptCodecError::ConnectionClosed)
));
let mut wrong_id = Assembler::new("other", cap)?;
assert!(matches!(
wrong_id.accept(&frames[0]),
Err(ReceiptCodecError::Identity)
));
let invalid = change(&frames[0], |e| {
let FragmentPart::Chunk { hex, .. } = &mut e.part else {
return Err("chunk missing".into());
};
*hex = "gg".into();
Ok(())
})?;
assert!(matches!(
Assembler::new("id", cap)?.accept(&invalid),
Err(ReceiptCodecError::Hex)
));
assert!(matches!(
Assembler::new("id", cap)?.accept(b"not json"),
Err(ReceiptCodecError::Decode { .. })
));
Ok(())
}
#[test]
fn worst_valid_identity_fits_the_derived_floor() -> TestResult {
let id = "\0".repeat(512);
let cap = u32::try_from(minimum_cap()?)?;
let frames = encoded()?.frames(&id, cap)?;
let mut assembler = Assembler::new(&id, cap)?;
let mut result = None;
for frame in frames {
assert!(frame.len() <= usize::try_from(cap)?);
if let Some(receipt) = assembler.accept(&frame)? {
result = Some(receipt);
}
}
assert!(result.is_some());
assert!(assembler.connection_closed().is_ok());
assert!(matches!(
assembler.accept(b"{}"),
Err(ReceiptCodecError::Finished)
));
Ok(())
}
#[test]
fn incomplete_terminal_invalid_identity_and_frame_limit_are_refused() -> TestResult {
let cap = u32::try_from(minimum_cap()?)?;
assert!(matches!(
Assembler::new("", cap),
Err(ReceiptCodecError::InvalidId)
));
assert!(matches!(
Assembler::new(&"x".repeat(513), cap),
Err(ReceiptCodecError::InvalidId)
));
let terminal = serde_json::to_vec(&FragmentEnvelope {
id: "id".into(),
part: FragmentPart::End {
fragments: 0,
outcomes: 0,
bytes: 0,
digest: "00".repeat(32),
},
})?;
assert!(matches!(
Assembler::new("id", cap)?.accept(&terminal),
Err(ReceiptCodecError::Incomplete)
));
let oversized = vec![b' '; usize::try_from(cap)? + 1];
assert!(matches!(
Assembler::new("id", cap)?.accept(&oversized),
Err(ReceiptCodecError::FrameLimit { .. })
));
let mut nonempty = vec![1];
assert!(matches!(
reserve_bytes(&mut nonempty, usize::MAX),
Err(ReceiptCodecError::ExtentOverflow)
));
Ok(())
}
#[test]
fn verified_bytes_must_decode_as_a_complete_typed_receipt() -> TestResult {
use sha2::{Digest, Sha256};
let bytes = b"{";
let digest = super::receipt_fragments::encode_hex(&Sha256::digest(bytes))?;
let cap = u32::try_from(minimum_cap()?)?;
let chunk = serde_json::to_vec(&FragmentEnvelope {
id: "id".into(),
part: FragmentPart::Chunk {
sequence: 0,
hex: "7b".into(),
},
})?;
let terminal = serde_json::to_vec(&FragmentEnvelope {
id: "id".into(),
part: FragmentPart::End {
fragments: 1,
outcomes: 0,
bytes: 1,
digest,
},
})?;
let mut assembler = Assembler::new("id", cap)?;
assert!(assembler.accept(&chunk)?.is_none());
assert!(matches!(
assembler.accept(&terminal),
Err(ReceiptCodecError::Decode { .. })
));
Ok(())
}
#[test]
fn verified_receipt_retains_the_exact_once_serialized_bytes() -> TestResult {
let encoded = encoded()?;
let cap = u32::try_from(minimum_cap()?)?;
let mut assembler = Assembler::new("sealed", cap)?;
let mut complete = None;
for frame in encoded.frames("sealed", cap)? {
if let Some(receipt) = assembler.accept(&frame)? {
assert!(complete.is_none());
complete = Some(receipt);
}
}
let complete = complete.ok_or("assembler never completed the receipt")?;
assert_eq!(complete.bytes(), encoded.bytes());
let receipt = complete.receipt().ok_or("packet has no receipt")?;
assert_eq!(receipt, &WireReceipt::project(&fixture()?)?);
assert!(assembler.connection_closed().is_ok());
assert_eq!(complete.bytes(), encoded.bytes());
Ok(())
}
#[test]
fn verified_receipt_retains_lossless_noncanonical_json_bytes() -> TestResult {
use sha2::{Digest, Sha256};
let bytes = b"{ \"kind\": \"receipt\", \"receipt\": { \"outcomes\": [], \"submit_error\": null, \"first_error\": null }, \"refusal\": null }\n";
let digest = super::receipt_fragments::encode_hex(&Sha256::digest(bytes))?;
let cap = u32::try_from(minimum_cap()?)?;
let mut assembler = Assembler::new("exact", cap)?;
let chunk = serde_json::to_vec(&FragmentEnvelope {
id: "exact".into(),
part: FragmentPart::Chunk {
sequence: 0,
hex: super::receipt_fragments::encode_hex(bytes)?,
},
})?;
let end = serde_json::to_vec(&FragmentEnvelope {
id: "exact".into(),
part: FragmentPart::End {
fragments: 1,
outcomes: 0,
bytes: u64::try_from(bytes.len())?,
digest,
},
})?;
assert!(assembler.accept(&chunk)?.is_none());
let complete = assembler.accept(&end)?.ok_or("terminal did not complete")?;
assert_eq!(complete.bytes(), bytes);
let receipt = complete.receipt().ok_or("packet has no receipt")?;
assert!(receipt.outcomes.is_empty());
assert_ne!(serde_json::to_vec(complete.packet())?, bytes);
Ok(())
}