538 lines
24 KiB
Rust
538 lines
24 KiB
Rust
// Payload-delta archive loading and validation tests.
|
|
|
|
use super::*;
|
|
|
|
fn build_delta_fixture() -> (tempfile::TempDir, PathBuf, PathBuf, String, String) {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let archive_root = temp.path().join("payload-delta-archive");
|
|
let capture = "delta-cap";
|
|
let base_capture = "base-cap";
|
|
let base_sha = "deadbeef";
|
|
let capture_root = archive_root.join("v1").join("captures").join(capture);
|
|
std::fs::create_dir_all(&capture_root).expect("mkdir capture root");
|
|
std::fs::write(
|
|
capture_root.join("capture.json"),
|
|
format!(
|
|
r#"{{"version":1,"captureId":"{capture}","createdAt":"2026-03-15T00:00:00Z","notes":""}}"#
|
|
),
|
|
)
|
|
.expect("write capture json");
|
|
std::fs::write(
|
|
capture_root.join("base.json"),
|
|
format!(
|
|
r#"{{"version":1,"baseCapture":"{base_capture}","baseLocksSha256":"{base_sha}","createdAt":"2026-03-15T00:00:00Z"}}"#
|
|
),
|
|
)
|
|
.expect("write base json");
|
|
|
|
let notify_uri = "https://rrdp.example.test/notification.xml".to_string();
|
|
let session = "11111111-1111-1111-1111-111111111111".to_string();
|
|
let _target_serial = 12u64;
|
|
let repo_hash = sha256_hex(notify_uri.as_bytes());
|
|
let session_dir = capture_root
|
|
.join("rrdp/repos")
|
|
.join(&repo_hash)
|
|
.join(&session);
|
|
let deltas_dir = session_dir.join("deltas");
|
|
std::fs::create_dir_all(&deltas_dir).expect("mkdir deltas");
|
|
std::fs::write(
|
|
session_dir.parent().unwrap().join("meta.json"),
|
|
format!(
|
|
r#"{{"version":1,"rpkiNotify":"{notify_uri}","createdAt":"2026-03-15T00:00:00Z","lastSeenAt":"2026-03-15T00:00:01Z"}}"#
|
|
),
|
|
)
|
|
.expect("write meta");
|
|
std::fs::write(
|
|
session_dir.parent().unwrap().join("transition.json"),
|
|
format!(
|
|
r#"{{"kind":"delta","base":{{"transport":"rrdp","session":"{session}","serial":10}},"target":{{"transport":"rrdp","session":"{session}","serial":12}},"delta_count":2,"deltas":[11,12]}}"#
|
|
),
|
|
)
|
|
.expect("write transition");
|
|
std::fs::write(
|
|
session_dir.join("notification-target-12.xml"),
|
|
b"<notification/>",
|
|
)
|
|
.expect("write notification");
|
|
std::fs::write(
|
|
deltas_dir.join("delta-11-aaaa.xml"),
|
|
b"<delta serial='11'/>",
|
|
)
|
|
.expect("write delta 11");
|
|
std::fs::write(
|
|
deltas_dir.join("delta-12-bbbb.xml"),
|
|
b"<delta serial='12'/>",
|
|
)
|
|
.expect("write delta 12");
|
|
std::fs::write(
|
|
session_dir.parent().unwrap().join("target-archive-12.bin"),
|
|
b"bin",
|
|
)
|
|
.expect("write target archive");
|
|
|
|
let module_uri = "rsync://rsync.example.test/repo/".to_string();
|
|
let module_hash = sha256_hex(module_uri.as_bytes());
|
|
let module_bucket = capture_root.join("rsync/modules").join(&module_hash);
|
|
let tree_root = module_bucket
|
|
.join("tree")
|
|
.join("rsync.example.test")
|
|
.join("repo");
|
|
std::fs::create_dir_all(tree_root.join("sub")).expect("mkdir tree root");
|
|
std::fs::write(
|
|
module_bucket.join("meta.json"),
|
|
format!(
|
|
r#"{{"version":1,"module":"{module_uri}","createdAt":"2026-03-15T00:00:00Z","lastSeenAt":"2026-03-15T00:00:01Z"}}"#
|
|
),
|
|
)
|
|
.expect("write rsync meta");
|
|
std::fs::write(
|
|
module_bucket.join("files.json"),
|
|
format!(
|
|
r#"{{"version":1,"module":"{module_uri}","fileCount":2,"files":["{module_uri}a.roa","{module_uri}sub/b.cer"]}}"#
|
|
),
|
|
)
|
|
.expect("write files json");
|
|
std::fs::write(tree_root.join("a.roa"), b"roa").expect("write a.roa");
|
|
std::fs::write(tree_root.join("sub").join("b.cer"), b"cer").expect("write b.cer");
|
|
|
|
let locks_path = temp.path().join("locks-delta.json");
|
|
std::fs::write(
|
|
&locks_path,
|
|
format!(
|
|
r#"{{"version":1,"capture":"{capture}","baseCapture":"{base_capture}","baseLocksSha256":"{base_sha}","rrdp":{{"{notify_uri}":{{"kind":"delta","base":{{"transport":"rrdp","session":"{session}","serial":10}},"target":{{"transport":"rrdp","session":"{session}","serial":12}},"delta_count":2,"deltas":[11,12]}}}},"rsync":{{"{module_uri}":{{"file_count":2,"overlay_only":true}}}}}}"#
|
|
),
|
|
)
|
|
.expect("write locks-delta");
|
|
(temp, archive_root, locks_path, notify_uri, module_uri)
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_loads_unchanged_and_fallback_rsync_rrdp_entries() {
|
|
let (_temp, archive_root, locks_path, notify_uri, _module_uri) = build_delta_fixture();
|
|
let capture_root = archive_root.join("v1/captures/delta-cap");
|
|
let repo_hash = sha256_hex(notify_uri.as_bytes());
|
|
let repo_dir = capture_root.join("rrdp/repos").join(&repo_hash);
|
|
std::fs::write(
|
|
repo_dir.join("transition.json"),
|
|
r#"{"kind":"unchanged","base":{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":10},"target":{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":10},"delta_count":0,"deltas":[]}"#,
|
|
)
|
|
.expect("rewrite transition unchanged");
|
|
std::fs::write(
|
|
&locks_path,
|
|
r#"{"version":1,"capture":"delta-cap","baseCapture":"base-cap","baseLocksSha256":"deadbeef","rrdp":{"https://rrdp.example.test/notification.xml":{"kind":"unchanged","base":{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":10},"target":{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":10},"delta_count":0,"deltas":[]}},"rsync":{"rsync://rsync.example.test/repo/":{"file_count":2,"overlay_only":true}}}"#,
|
|
).expect("rewrite locks unchanged");
|
|
let index =
|
|
ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).expect("load unchanged index");
|
|
let repo = index.rrdp_repo(¬ify_uri).expect("rrdp repo");
|
|
assert_eq!(repo.transition.kind, ReplayDeltaRrdpKind::Unchanged);
|
|
assert!(repo.target_notification_path.is_none());
|
|
assert!(repo.delta_paths.is_empty());
|
|
assert!(repo.target_archive_path.is_none());
|
|
|
|
std::fs::write(
|
|
repo_dir.join("transition.json"),
|
|
r#"{"kind":"fallback-rsync","base":{"transport":"rsync","session":null,"serial":null},"target":{"transport":"rsync","session":null,"serial":null},"delta_count":0,"deltas":[]}"#,
|
|
).expect("rewrite transition fallback-rsync");
|
|
std::fs::write(
|
|
&locks_path,
|
|
r#"{"version":1,"capture":"delta-cap","baseCapture":"base-cap","baseLocksSha256":"deadbeef","rrdp":{"https://rrdp.example.test/notification.xml":{"kind":"fallback-rsync","base":{"transport":"rsync","session":null,"serial":null},"target":{"transport":"rsync","session":null,"serial":null},"delta_count":0,"deltas":[]}},"rsync":{"rsync://rsync.example.test/repo/":{"file_count":2,"overlay_only":true}}}"#,
|
|
).expect("rewrite locks fallback-rsync");
|
|
let index = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path)
|
|
.expect("load fallback-rsync index");
|
|
let repo = index.rrdp_repo(¬ify_uri).expect("rrdp repo");
|
|
assert_eq!(repo.transition.kind, ReplayDeltaRrdpKind::FallbackRsync);
|
|
assert!(repo.target_notification_path.is_none());
|
|
assert!(repo.delta_paths.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_resolves_rsync_module_from_base_uri() {
|
|
let (_temp, archive_root, locks_path, _notify_uri, _module_uri) = build_delta_fixture();
|
|
let index =
|
|
ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).expect("load delta index");
|
|
let module = index
|
|
.resolve_rsync_module_for_base_uri("rsync://rsync.example.test/repo/sub/path")
|
|
.expect("resolve module");
|
|
assert_eq!(module.module_uri, "rsync://rsync.example.test/repo/");
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_rejects_unsupported_versions_and_meta_mismatches() {
|
|
let (_temp, archive_root, locks_path, _notify_uri, _module_uri) = build_delta_fixture();
|
|
std::fs::write(
|
|
&locks_path,
|
|
r#"{"version":2,"capture":"delta-cap","baseCapture":"base-cap","baseLocksSha256":"deadbeef","rrdp":{},"rsync":{}}"#,
|
|
).expect("rewrite locks version");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(
|
|
err,
|
|
ReplayDeltaArchiveError::Base(ReplayArchiveError::UnsupportedVersion {
|
|
entity: "payload delta locks",
|
|
version: 2
|
|
})
|
|
),
|
|
"{err}"
|
|
);
|
|
|
|
let (_temp, archive_root, locks_path, notify_uri, _module_uri) = build_delta_fixture();
|
|
let repo_hash = sha256_hex(notify_uri.as_bytes());
|
|
std::fs::write(
|
|
archive_root.join("v1/captures/delta-cap/rrdp/repos").join(&repo_hash).join("meta.json"),
|
|
r#"{"version":1,"rpkiNotify":"https://other.example/notification.xml","createdAt":"2026-03-15T00:00:00Z","lastSeenAt":"2026-03-15T00:00:01Z"}"#,
|
|
).expect("rewrite rrdp meta");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::RrdpMetaMismatch { .. }),
|
|
"{err}"
|
|
);
|
|
|
|
let (_temp, archive_root, locks_path, _notify_uri, module_uri) = build_delta_fixture();
|
|
let module_hash = sha256_hex(module_uri.as_bytes());
|
|
std::fs::write(
|
|
archive_root.join("v1/captures/delta-cap/rsync/modules").join(&module_hash).join("meta.json"),
|
|
r#"{"version":1,"module":"rsync://other.example/repo/","createdAt":"2026-03-15T00:00:00Z","lastSeenAt":"2026-03-15T00:00:01Z"}"#,
|
|
).expect("rewrite rsync meta");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::RsyncMetaMismatch { .. }),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_rejects_transition_base_target_and_serial_mismatches() {
|
|
let (_temp, archive_root, locks_path, notify_uri, _module_uri) = build_delta_fixture();
|
|
let repo_hash = sha256_hex(notify_uri.as_bytes());
|
|
let repo_dir = archive_root
|
|
.join("v1/captures/delta-cap/rrdp/repos")
|
|
.join(&repo_hash);
|
|
|
|
std::fs::write(
|
|
repo_dir.join("transition.json"),
|
|
r#"{"kind":"delta","base":{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":9},"target":{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":12},"delta_count":2,"deltas":[11,12]}"#,
|
|
).expect("rewrite transition base");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::TransitionBaseMismatch { .. }),
|
|
"{err}"
|
|
);
|
|
|
|
let (_temp, archive_root, locks_path, notify_uri, _module_uri) = build_delta_fixture();
|
|
let repo_hash = sha256_hex(notify_uri.as_bytes());
|
|
let repo_dir = archive_root
|
|
.join("v1/captures/delta-cap/rrdp/repos")
|
|
.join(&repo_hash);
|
|
std::fs::write(
|
|
repo_dir.join("transition.json"),
|
|
r#"{"kind":"delta","base":{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":10},"target":{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":13},"delta_count":2,"deltas":[11,12]}"#,
|
|
).expect("rewrite transition target");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(
|
|
err,
|
|
ReplayDeltaArchiveError::TransitionTargetMismatch { .. }
|
|
),
|
|
"{err}"
|
|
);
|
|
|
|
let (_temp, archive_root, locks_path, notify_uri, _module_uri) = build_delta_fixture();
|
|
let repo_hash = sha256_hex(notify_uri.as_bytes());
|
|
let repo_dir = archive_root
|
|
.join("v1/captures/delta-cap/rrdp/repos")
|
|
.join(&repo_hash);
|
|
std::fs::write(
|
|
repo_dir.join("transition.json"),
|
|
r#"{"kind":"delta","base":{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":10},"target":{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":12},"delta_count":1,"deltas":[12]}"#,
|
|
).expect("rewrite transition deltas");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::DeltaSerialListMismatch { .. }),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_rejects_missing_session_dir_and_overlay_files() {
|
|
let (_temp, archive_root, locks_path, notify_uri, _module_uri) = build_delta_fixture();
|
|
let repo_hash = sha256_hex(notify_uri.as_bytes());
|
|
let session_dir = archive_root
|
|
.join("v1/captures/delta-cap/rrdp/repos")
|
|
.join(&repo_hash)
|
|
.join("11111111-1111-1111-1111-111111111111");
|
|
std::fs::remove_dir_all(&session_dir).expect("remove session dir");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::MissingDeltaSessionDir { .. }),
|
|
"{err}"
|
|
);
|
|
|
|
let (_temp, archive_root, locks_path, _notify_uri, module_uri) = build_delta_fixture();
|
|
let module_hash = sha256_hex(module_uri.as_bytes());
|
|
let overlay_path = archive_root
|
|
.join("v1/captures/delta-cap/rsync/modules")
|
|
.join(&module_hash)
|
|
.join("tree/rsync.example.test/repo/sub/b.cer");
|
|
std::fs::remove_file(overlay_path).expect("remove overlay file");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::MissingRsyncOverlayFile { .. }),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_accepts_missing_rsync_module_meta_when_files_and_tree_exist() {
|
|
let (_temp, archive_root, locks_path, _notify_uri, module_uri) = build_delta_fixture();
|
|
let module_hash = sha256_hex(module_uri.as_bytes());
|
|
let meta_path = archive_root
|
|
.join("v1/captures/delta-cap/rsync/modules")
|
|
.join(module_hash)
|
|
.join("meta.json");
|
|
std::fs::remove_file(&meta_path).expect("remove delta rsync module meta");
|
|
|
|
let index = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path)
|
|
.expect("load delta replay index without rsync meta");
|
|
let module = index
|
|
.rsync_modules
|
|
.get(&module_uri)
|
|
.expect("module present");
|
|
assert_eq!(module.meta.module, module_uri);
|
|
assert_eq!(module.meta.version, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_accepts_correct_base_locks_sha_and_rejects_missing_module_resolution() {
|
|
let (_temp, archive_root, locks_path, _notify_uri, _module_uri) = build_delta_fixture();
|
|
let index =
|
|
ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).expect("load delta index");
|
|
let locks_bytes = std::fs::read(&locks_path).expect("read locks bytes");
|
|
assert!(
|
|
index
|
|
.validate_base_locks_sha256_bytes(&locks_bytes)
|
|
.is_err()
|
|
);
|
|
let err = index
|
|
.resolve_rsync_module_for_base_uri("rsync://missing.example/repo/path")
|
|
.unwrap_err();
|
|
assert!(
|
|
matches!(
|
|
err,
|
|
ReplayDeltaArchiveError::MissingRsyncModuleBucket { .. }
|
|
),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_loads_session_reset_and_gap_entries_without_target_files() {
|
|
let (_temp, archive_root, locks_path, notify_uri, _module_uri) = build_delta_fixture();
|
|
let repo_hash = sha256_hex(notify_uri.as_bytes());
|
|
let repo_dir = archive_root
|
|
.join("v1/captures/delta-cap/rrdp/repos")
|
|
.join(&repo_hash);
|
|
|
|
for kind in ["session-reset", "gap"] {
|
|
std::fs::write(
|
|
repo_dir.join("transition.json"),
|
|
format!(
|
|
r#"{{"kind":"{kind}","base":{{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":10}},"target":{{"transport":"rrdp","session":"22222222-2222-2222-2222-222222222222","serial":12}},"delta_count":0,"deltas":[]}}"#,
|
|
),
|
|
)
|
|
.expect("rewrite transition kind");
|
|
std::fs::write(
|
|
&locks_path,
|
|
format!(
|
|
r#"{{"version":1,"capture":"delta-cap","baseCapture":"base-cap","baseLocksSha256":"deadbeef","rrdp":{{"{notify_uri}":{{"kind":"{kind}","base":{{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":10}},"target":{{"transport":"rrdp","session":"22222222-2222-2222-2222-222222222222","serial":12}},"delta_count":0,"deltas":[]}}}},"rsync":{{"rsync://rsync.example.test/repo/":{{"file_count":2,"overlay_only":true}}}}}}"#,
|
|
),
|
|
)
|
|
.expect("rewrite locks kind");
|
|
let index =
|
|
ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).expect("load delta index");
|
|
let repo = index.rrdp_repo(¬ify_uri).expect("rrdp repo");
|
|
assert!(repo.target_notification_path.is_none());
|
|
assert!(repo.delta_paths.is_empty());
|
|
}
|
|
}
|
|
#[test]
|
|
fn delta_archive_index_loads_rrdp_and_rsync_entries() {
|
|
let (_temp, archive_root, locks_path, notify_uri, module_uri) = build_delta_fixture();
|
|
let index =
|
|
ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).expect("load delta index");
|
|
assert_eq!(index.capture_meta.capture_id, "delta-cap");
|
|
assert_eq!(index.base_meta.base_capture, "base-cap");
|
|
assert_eq!(index.rrdp_repos.len(), 1);
|
|
assert_eq!(index.rsync_modules.len(), 1);
|
|
|
|
let repo = index.rrdp_repo(¬ify_uri).expect("rrdp repo");
|
|
assert_eq!(repo.transition.kind, ReplayDeltaRrdpKind::Delta);
|
|
assert_eq!(repo.transition.delta_count, 2);
|
|
assert_eq!(repo.delta_paths.len(), 2);
|
|
assert!(repo.target_notification_path.as_ref().unwrap().is_file());
|
|
assert!(repo.target_archive_path.as_ref().unwrap().is_file());
|
|
|
|
let module = index.rsync_module(&module_uri).expect("rsync module");
|
|
assert_eq!(module.files.file_count, 2);
|
|
assert_eq!(module.overlay_files.len(), 2);
|
|
assert!(module.overlay_files.iter().all(|(_, path)| path.is_file()));
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_rejects_capture_and_sha_mismatches() {
|
|
let (_temp, archive_root, locks_path, _notify_uri, _module_uri) = build_delta_fixture();
|
|
let capture_root = archive_root.join("v1/captures/delta-cap");
|
|
std::fs::write(
|
|
capture_root.join("capture.json"),
|
|
r#"{"version":1,"captureId":"other-cap","createdAt":"2026-03-15T00:00:00Z","notes":""}"#,
|
|
)
|
|
.expect("rewrite capture json");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::CaptureIdMismatch { .. }),
|
|
"{err}"
|
|
);
|
|
|
|
let (_temp, archive_root, locks_path, _notify_uri, _module_uri) = build_delta_fixture();
|
|
let capture_root = archive_root.join("v1/captures/delta-cap");
|
|
std::fs::write(
|
|
capture_root.join("base.json"),
|
|
r#"{"version":1,"baseCapture":"base-cap","baseLocksSha256":"beefdead","createdAt":"2026-03-15T00:00:00Z"}"#,
|
|
)
|
|
.expect("rewrite base json sha");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::BaseLocksShaMismatch { .. }),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_rejects_missing_target_notification_and_repo_bucket() {
|
|
let (_temp, archive_root, locks_path, notify_uri, _module_uri) = build_delta_fixture();
|
|
let repo_hash = sha256_hex(notify_uri.as_bytes());
|
|
let session_dir = archive_root
|
|
.join("v1/captures/delta-cap/rrdp/repos")
|
|
.join(&repo_hash)
|
|
.join("11111111-1111-1111-1111-111111111111");
|
|
std::fs::remove_file(session_dir.join("notification-target-12.xml"))
|
|
.expect("remove target notification");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(
|
|
err,
|
|
ReplayDeltaArchiveError::MissingTargetNotification { .. }
|
|
),
|
|
"{err}"
|
|
);
|
|
|
|
let (_temp, archive_root, locks_path, notify_uri, _module_uri) = build_delta_fixture();
|
|
let repo_hash = sha256_hex(notify_uri.as_bytes());
|
|
let repo_dir = archive_root
|
|
.join("v1/captures/delta-cap/rrdp/repos")
|
|
.join(&repo_hash);
|
|
std::fs::remove_dir_all(repo_dir).expect("remove repo dir");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::MissingDeltaRepoBucket { .. }),
|
|
"{err}"
|
|
);
|
|
}
|
|
#[test]
|
|
fn delta_archive_index_rejects_base_meta_mismatch() {
|
|
let (_temp, archive_root, locks_path, _notify_uri, _module_uri) = build_delta_fixture();
|
|
let capture_root = archive_root.join("v1/captures/delta-cap");
|
|
std::fs::write(
|
|
capture_root.join("base.json"),
|
|
r#"{"version":1,"baseCapture":"other","baseLocksSha256":"deadbeef","createdAt":"2026-03-15T00:00:00Z"}"#,
|
|
)
|
|
.expect("rewrite base json");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::BaseCaptureMismatch { .. }),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_rejects_transition_mismatch_and_missing_delta_file() {
|
|
let (_temp, archive_root, locks_path, notify_uri, _module_uri) = build_delta_fixture();
|
|
let repo_hash = sha256_hex(notify_uri.as_bytes());
|
|
let repo_dir = archive_root
|
|
.join("v1/captures/delta-cap/rrdp/repos")
|
|
.join(&repo_hash);
|
|
std::fs::write(
|
|
repo_dir.join("transition.json"),
|
|
r#"{"kind":"unchanged","base":{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":10},"target":{"transport":"rrdp","session":"11111111-1111-1111-1111-111111111111","serial":12},"delta_count":2,"deltas":[11,12]}"#,
|
|
)
|
|
.expect("rewrite transition");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::TransitionKindMismatch { .. }),
|
|
"{err}"
|
|
);
|
|
|
|
let (_temp, archive_root, locks_path, notify_uri, _module_uri) = build_delta_fixture();
|
|
let repo_hash = sha256_hex(notify_uri.as_bytes());
|
|
let delta_path = archive_root
|
|
.join("v1/captures/delta-cap/rrdp/repos")
|
|
.join(&repo_hash)
|
|
.join("11111111-1111-1111-1111-111111111111/deltas/delta-12-bbbb.xml");
|
|
std::fs::remove_file(delta_path).expect("remove delta");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::MissingDeltaFile { .. }),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_validates_base_locks_sha256_bytes_and_file() {
|
|
let (_temp, archive_root, locks_path, _notify_uri, _module_uri) = build_delta_fixture();
|
|
let index =
|
|
ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).expect("load delta index");
|
|
let err = index
|
|
.validate_base_locks_sha256_bytes(b"not-the-right-base-locks")
|
|
.unwrap_err();
|
|
assert!(
|
|
matches!(
|
|
err,
|
|
ReplayDeltaArchiveError::BaseLocksBytesShaMismatch { .. }
|
|
),
|
|
"{err}"
|
|
);
|
|
|
|
let temp_file = tempfile::NamedTempFile::new().expect("tempfile");
|
|
std::fs::write(temp_file.path(), b"still-wrong").expect("write base locks file");
|
|
let err = index
|
|
.validate_base_locks_sha256_file(temp_file.path())
|
|
.unwrap_err();
|
|
assert!(
|
|
matches!(
|
|
err,
|
|
ReplayDeltaArchiveError::BaseLocksBytesShaMismatch { .. }
|
|
),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delta_archive_index_rejects_rsync_files_mismatch() {
|
|
let (_temp, archive_root, locks_path, _notify_uri, module_uri) = build_delta_fixture();
|
|
let module_hash = sha256_hex(module_uri.as_bytes());
|
|
let module_dir = archive_root
|
|
.join("v1/captures/delta-cap/rsync/modules")
|
|
.join(&module_hash);
|
|
std::fs::write(
|
|
module_dir.join("files.json"),
|
|
format!(
|
|
r#"{{"version":1,"module":"{module_uri}","fileCount":3,"files":["{module_uri}a.roa"]}}"#
|
|
),
|
|
)
|
|
.expect("rewrite files json");
|
|
let err = ReplayDeltaArchiveIndex::load(&archive_root, &locks_path).unwrap_err();
|
|
assert!(
|
|
matches!(err, ReplayDeltaArchiveError::RsyncFileCountMismatch { .. }),
|
|
"{err}"
|
|
);
|
|
}
|