#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)]
use prikk_error::Result;
use prikk_object::ObjectId;
use super::{
DEFAULT_HAVE_LIST_MAX_PATCH_COUNT, DEFAULT_HAVE_LIST_MAX_TOTAL_BYTES, build_have_list,
decode_have_list,
};
use crate::patch_set_digest::compute_patch_set_digest;
use crate::sync_negotiation::sync_test_support::{cleanup, fresh_repo, publish_branch};
const PATCH_COUNT: usize = DEFAULT_HAVE_LIST_MAX_PATCH_COUNT;
const TOTAL_BYTES: usize = DEFAULT_HAVE_LIST_MAX_TOTAL_BYTES;
#[test]
fn a_have_list_round_trips_a_branchs_patch_ids_and_digest() -> Result<()> {
let layout = fresh_repo("have-list-round-trip")?;
let p1 = ObjectId::from_bytes([0x31; 32]);
let p2 = ObjectId::from_bytes([0x32; 32]);
publish_branch(&layout, "heads/main", vec![p1, p2])?;
let bytes = build_have_list(&layout, "heads/main")?;
let have_list = decode_have_list(&bytes, TOTAL_BYTES, PATCH_COUNT)?;
assert_eq!(have_list.ref_name, "heads/main");
let mut expected = vec![p1, p2];
expected.sort_unstable();
assert_eq!(have_list.patch_ids, expected);
assert_eq!(have_list.digest, compute_patch_set_digest(&expected)?);
cleanup(&layout);
Ok(())
}
#[test]
fn a_have_list_for_an_absent_ref_is_empty_not_refused() -> Result<()> {
let layout = fresh_repo("have-list-absent-ref")?;
let bytes = build_have_list(&layout, "heads/never-existed")?;
let have_list = decode_have_list(&bytes, TOTAL_BYTES, PATCH_COUNT)?;
assert_eq!(have_list.ref_name, "heads/never-existed");
assert!(have_list.patch_ids.is_empty());
assert_eq!(have_list.digest, compute_patch_set_digest(&[])?);
cleanup(&layout);
Ok(())
}
#[test]
fn a_patch_count_over_the_limit_is_rejected_before_allocating() {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"PSYNCHV1");
bytes.extend_from_slice(&0_u16.to_be_bytes()); bytes.extend_from_slice(&[0_u8; 32]); bytes.extend_from_slice(&(DEFAULT_HAVE_LIST_MAX_PATCH_COUNT as u64 + 1).to_be_bytes());
let result = decode_have_list(&bytes, TOTAL_BYTES, DEFAULT_HAVE_LIST_MAX_PATCH_COUNT);
let message = match result {
Ok(_) => panic!("an over-limit declared patch count must be rejected"),
Err(error) => error.to_string(),
};
assert!(
message.contains("over the configured limit"),
"expected the declared-count refusal, got: {message}"
);
}
#[test]
fn oversized_total_bytes_is_refused_before_parsing() -> Result<()> {
let layout = fresh_repo("have-list-oversized")?;
publish_branch(
&layout,
"heads/main",
vec![ObjectId::from_bytes([0x33; 32])],
)?;
let bytes = build_have_list(&layout, "heads/main")?;
let result = decode_have_list(&bytes, bytes.len() - 1, PATCH_COUNT);
let message = match result {
Ok(_) => panic!("an oversized have-list must be refused before decoding"),
Err(error) => error.to_string(),
};
assert!(
message.contains("over the configured limit"),
"expected the total-byte-length refusal, got: {message}"
);
cleanup(&layout);
Ok(())
}
#[test]
fn a_truncated_list_under_an_unchanged_digest_is_refused() -> Result<()> {
let p1 = ObjectId::from_bytes([0x41; 32]);
let p2 = ObjectId::from_bytes([0x42; 32]);
let full_digest = compute_patch_set_digest(&[p1, p2])?;
let mut bytes = Vec::new();
bytes.extend_from_slice(b"PSYNCHV1");
let ref_name = b"heads/main";
bytes.extend_from_slice(&(ref_name.len() as u16).to_be_bytes());
bytes.extend_from_slice(ref_name);
bytes.extend_from_slice(&full_digest.0);
bytes.extend_from_slice(&1_u64.to_be_bytes()); bytes.extend_from_slice(p1.as_bytes());
let result = decode_have_list(&bytes, TOTAL_BYTES, PATCH_COUNT);
assert!(
result.is_err(),
"a truncated list under an unchanged digest must be refused"
);
Ok(())
}