mod proptest_decode_bundle;
use prikk_object::{BlockKind, ObjectType};
use crate::bundle::{BundleImportOptions, export_bundle, import_bundle};
use crate::layout::LockableContainer;
use crate::lock::acquire_container_locks;
use crate::received::read_received_pointer;
use crate::test_support::{
signed_block, signed_patch_blob_envelope, signed_patch_envelope, signed_ref_state_envelope,
signed_ref_update_envelope, unique_temp_dir,
};
use crate::{FileObjectStore, ObjectWriter, RefPublication, RefStore, RepositoryLayout};
fn seal_two_block_history(
layout: &RepositoryLayout,
) -> prikk_error::Result<prikk_object::ObjectId> {
let mut object_store = FileObjectStore::new(layout.clone());
object_store.write_object(&signed_patch_blob_envelope())?;
let patch = signed_patch_envelope();
let patch_id = object_store.write_object(&patch)?;
let root_block = signed_block(BlockKind::Root, Vec::new(), Vec::new(), None);
let root_block_id = object_store.write_object(&root_block)?;
let child_block = signed_block(BlockKind::Normal, vec![root_block_id], vec![patch_id], None);
let child_block_id = object_store.write_object(&child_block)?;
let ref_store = RefStore::new(layout.clone());
let ref_state = signed_ref_state_envelope("heads/main", None, child_block_id, 1);
let ref_state_id = ref_state.object_id();
let ref_update =
signed_ref_update_envelope("heads/main", None, ref_state_id, child_block_id, 1);
ref_store.publish(&RefPublication {
ref_name: "heads/main".to_string(),
expected_previous_ref_state_id: None,
ref_state,
ref_update,
})?;
Ok(child_block_id)
}
#[test]
fn export_of_missing_ref_fails() {
let root = unique_temp_dir("bundle-export-missing-ref");
let layout = RepositoryLayout::init(root.clone());
assert!(layout.is_ok());
if let Ok(layout) = layout {
assert!(export_bundle(&layout, "heads/main").is_err());
}
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn import_of_malformed_bytes_fails_closed() {
let root = unique_temp_dir("bundle-import-malformed");
let layout = RepositoryLayout::init(root.clone());
assert!(layout.is_ok());
if let Ok(layout) = layout {
let options = BundleImportOptions::default_limits();
assert!(import_bundle(&layout, b"not a bundle", &options).is_err());
assert!(import_bundle(&layout, b"PBNDL001", &options).is_err());
assert!(import_bundle(&layout, &[], &options).is_err());
}
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn export_then_import_carries_the_full_genesis_complete_closure() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-export-source");
let source = RepositoryLayout::init(source_root.clone())?;
let child_block_id = seal_two_block_history(&source)?;
let (report, bytes) = export_bundle(&source, "heads/main")?;
assert_eq!(report.ref_name, "heads/main");
assert_eq!(report.tip_block_id, child_block_id);
assert_eq!(report.object_count, 5);
let target_root = unique_temp_dir("bundle-import-target");
let target = RepositoryLayout::init(target_root.clone())?;
let import_report = import_bundle(&target, &bytes, &BundleImportOptions::default_limits())?;
assert_eq!(import_report.ref_name, "remotes/heads/main");
assert_eq!(import_report.object_count, 5);
assert_eq!(import_report.written_object_count, 5);
let pointer = read_received_pointer(&target, "remotes/heads/main")?;
assert!(pointer.is_some());
if let Some(pointer) = pointer {
assert_eq!(pointer.ref_state_id, import_report.ref_state_id);
}
let target_objects = FileObjectStore::new(target.clone());
assert!(
target_objects
.read_typed(child_block_id, ObjectType::Block)?
.is_some()
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn import_refuses_while_received_index_lock_is_externally_held() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-lock-conflict-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let target_root = unique_temp_dir("bundle-lock-conflict-target");
let target = RepositoryLayout::init(target_root.clone())?;
let held = acquire_container_locks(&target, &[LockableContainer::ReceivedIndex])?;
assert!(import_bundle(&target, &bytes, &BundleImportOptions::default_limits()).is_err());
assert!(read_received_pointer(&target, "remotes/heads/main")?.is_none());
drop(held);
let report = import_bundle(&target, &bytes, &BundleImportOptions::default_limits())?;
assert_eq!(report.ref_name, "remotes/heads/main");
assert!(read_received_pointer(&target, "remotes/heads/main")?.is_some());
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn import_never_writes_a_local_ref_pointer() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-negctrl-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let target_root = unique_temp_dir("bundle-negctrl-target");
let target = RepositoryLayout::init(target_root.clone())?;
let ref_store = RefStore::new(target.clone());
let before = ref_store.list_ref_pointers()?;
assert!(before.is_empty());
import_bundle(&target, &bytes, &BundleImportOptions::default_limits())?;
let after = RefStore::new(target.clone()).list_ref_pointers()?;
assert_eq!(
before, after,
"bundle import must never advance or create a local heads/*-or-tags/* ref pointer"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn reimporting_the_same_bundle_is_idempotent() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-reimport-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let target_root = unique_temp_dir("bundle-reimport-target");
let target = RepositoryLayout::init(target_root.clone())?;
let options = BundleImportOptions::default_limits();
let first = import_bundle(&target, &bytes, &options)?;
assert_eq!(first.written_object_count, 5);
let second = import_bundle(&target, &bytes, &options)?;
assert_eq!(second.written_object_count, 0);
assert_eq!(second.ref_state_id, first.ref_state_id);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn import_object_count_limit_fires_exactly_at_the_boundary() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-limit-count-boundary-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (report, bytes) = export_bundle(&source, "heads/main")?;
assert_eq!(report.object_count, 5);
let under_root = unique_temp_dir("bundle-limit-count-boundary-under");
let under_target = RepositoryLayout::init(under_root.clone())?;
let refused = import_bundle(
&under_target,
&bytes,
&BundleImportOptions::default_limits().with_max_object_count(4),
);
assert!(
refused.is_err(),
"a limit one below the actual count (5) must refuse"
);
let at_root = unique_temp_dir("bundle-limit-count-boundary-at");
let at_target = RepositoryLayout::init(at_root.clone())?;
let accepted = import_bundle(
&at_target,
&bytes,
&BundleImportOptions::default_limits().with_max_object_count(5),
);
assert!(
accepted.is_ok(),
"a limit exactly at the actual count (5) must accept"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(under_root);
let _ = std::fs::remove_dir_all(at_root);
Ok(())
}
#[test]
fn import_total_bytes_limit_fires_exactly_at_the_boundary() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-limit-bytes-boundary-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let under_root = unique_temp_dir("bundle-limit-bytes-boundary-under");
let under_target = RepositoryLayout::init(under_root.clone())?;
let refused = import_bundle(
&under_target,
&bytes,
&BundleImportOptions::default_limits().with_max_total_bytes(bytes.len() - 1),
);
assert!(
refused.is_err(),
"a byte limit one below the bundle's own length must refuse"
);
let at_root = unique_temp_dir("bundle-limit-bytes-boundary-at");
let at_target = RepositoryLayout::init(at_root.clone())?;
let accepted = import_bundle(
&at_target,
&bytes,
&BundleImportOptions::default_limits().with_max_total_bytes(bytes.len()),
);
assert!(
accepted.is_ok(),
"a byte limit exactly at the bundle's own length must accept"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(under_root);
let _ = std::fs::remove_dir_all(at_root);
Ok(())
}
#[test]
fn import_refused_over_the_object_count_limit_writes_nothing() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-limit-writes-nothing-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let target_root = unique_temp_dir("bundle-limit-writes-nothing-target");
let target = RepositoryLayout::init(target_root.clone())?;
let before = crate::verify_repository(&target)?.checked_objects;
let refused = import_bundle(
&target,
&bytes,
&BundleImportOptions::default_limits().with_max_object_count(4),
);
assert!(refused.is_err());
let after = crate::verify_repository(&target)?.checked_objects;
assert_eq!(
before, after,
"a refused import must leave the object store's checked-object count unchanged"
);
assert_eq!(before, Some(0));
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}