// RRDP test group: edge cases.
#[test]
fn load_rrdp_local_state_uses_source_record_only() {
let tmp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(tmp.path()).expect("open rocksdb");
let notif_uri = "https://example.net/notification.xml";
assert_eq!(
load_rrdp_local_state(&store, notif_uri).expect("load empty"),
None
);
let record = build_rrdp_source_record_on_success(
&store,
notif_uri,
"source-session",
9,
Some(crate::repository::storage::RrdpSourceSyncState::DeltaReady),
Some("https://example.net/snapshot.xml"),
Some(&hex::encode([0x11; 32])),
)
.expect("build source record");
store.put_rrdp_source_record(&record).expect("write source record");
let got = load_rrdp_local_state(&store, notif_uri)
.expect("load source preferred")
.expect("source present");
assert_eq!(
got,
RrdpState {
session_id: "source-session".to_string(),
serial: 9,
delta_hashes: Default::default(),
}
);
}
#[test]
fn sync_from_notification_falls_back_to_snapshot_if_missing_required_deltas() {
let tmp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(tmp.path()).expect("open rocksdb");
let sid = "550e8400-e29b-41d4-a716-446655440000";
let notif_uri = "https://example.net/notification.xml";
// Seed state serial=1 with a only.
let snapshot_uri_1 = "https://example.net/snapshot-1.xml";
let snapshot_1 = snapshot_xml(sid, 1, &[("rsync://example.net/repo/a.mft", b"a1")]);
let snapshot_hash_1 = hex::encode(sha2::Sha256::digest(&snapshot_1));
let notif_1 = notification_xml(sid, 1, snapshot_uri_1, &snapshot_hash_1);
let fetcher_1 = MapFetcher {
map: HashMap::from([(snapshot_uri_1.to_string(), snapshot_1)]),
};
sync_from_notification_snapshot(&store, notif_uri, ¬if_1, &fetcher_1).expect("seed");
// Notification serial=3 only includes delta serial=3 (contiguous per RFC, but missing
// serial=2 relative to our local state, so we must use snapshot).
let snapshot_uri_3 = "https://example.net/snapshot-3.xml";
let snapshot_3 = snapshot_xml(sid, 3, &[("rsync://example.net/repo/z.roa", b"z3")]);
let snapshot_hash_3 = hex::encode(sha2::Sha256::digest(&snapshot_3));
let delta_3_hash_hex = "11".repeat(32);
let notif_3 = notification_xml_with_deltas(
sid,
3,
snapshot_uri_3,
&snapshot_hash_3,
&[(
"d3",
3,
"https://example.net/delta-3.xml",
&delta_3_hash_hex,
)],
);
let fetcher = MapFetcher {
map: HashMap::from([(snapshot_uri_3.to_string(), snapshot_3)]),
};
let published = sync_from_notification(&store, notif_uri, ¬if_3, &fetcher).expect("sync");
assert_eq!(published, 1);
assert!(
store
.load_current_object_bytes_by_uri("rsync://example.net/repo/z.roa")
.expect("get current")
.is_some()
);
}
#[test]
fn sync_from_notification_snapshot_rejects_snapshot_hash_mismatch() {
let tmp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(tmp.path()).expect("open rocksdb");
let sid = "550e8400-e29b-41d4-a716-446655440000";
let serial = 1u64;
let notif_uri = "https://example.net/notification.xml";
let snapshot_uri = "https://example.net/snapshot.xml";
let snapshot = snapshot_xml(sid, serial, &[("rsync://example.net/repo/a.mft", b"x")]);
let notif = notification_xml(sid, serial, snapshot_uri, &"00".repeat(32));
let fetcher = MapFetcher {
map: HashMap::from([(snapshot_uri.to_string(), snapshot)]),
};
let err = sync_from_notification_snapshot(&store, notif_uri, ¬if, &fetcher).unwrap_err();
assert!(matches!(
err,
RrdpSyncError::Rrdp(RrdpError::SnapshotHashMismatch)
));
}
#[test]
fn apply_snapshot_rejects_session_id_and_serial_mismatch() {
let tmp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(tmp.path()).expect("open rocksdb");
let notif_uri = "https://example.net/notification.xml";
let expected_sid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let got_sid = "550e8400-e29b-41d4-a716-446655440001";
let snapshot = snapshot_xml(got_sid, 2, &[("rsync://example.net/repo/a.mft", b"x")]);
let err = apply_snapshot(&store, notif_uri, None, &snapshot, expected_sid, 2).unwrap_err();
assert!(matches!(
err,
RrdpSyncError::Rrdp(RrdpError::SnapshotSessionIdMismatch { .. })
));
let snapshot = snapshot_xml(
expected_sid.to_string().as_str(),
3,
&[("rsync://example.net/repo/a.mft", b"x")],
);
let err = apply_snapshot(&store, notif_uri, None, &snapshot, expected_sid, 2).unwrap_err();
assert!(matches!(
err,
RrdpSyncError::Rrdp(RrdpError::SnapshotSerialMismatch { .. })
));
}
#[test]
fn strip_all_ascii_whitespace_removes_newlines_and_spaces() {
assert_eq!(strip_all_ascii_whitespace(" a \n b\tc "), "abc");
}
#[test]
fn apply_snapshot_reports_publish_errors() {
let tmp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(tmp.path()).expect("open rocksdb");
let notif_uri = "https://example.net/notification.xml";
let sid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
// Missing publish/@uri
let xml = format!(
r#"AA=="#
)
.into_bytes();
let err = apply_snapshot(&store, notif_uri, None, &xml, sid, 1).unwrap_err();
assert!(matches!(
err,
RrdpSyncError::Rrdp(RrdpError::PublishUriMissing)
));
// Missing base64 content (no text nodes).
let xml = format!(
r#""#
)
.into_bytes();
let err = apply_snapshot(&store, notif_uri, None, &xml, sid, 1).unwrap_err();
assert!(matches!(
err,
RrdpSyncError::Rrdp(RrdpError::PublishContentMissing)
));
// Invalid base64 content.
let xml = format!(
r#"!!!"#
)
.into_bytes();
let err = apply_snapshot(&store, notif_uri, None, &xml, sid, 1).unwrap_err();
assert!(matches!(
err,
RrdpSyncError::Rrdp(RrdpError::PublishBase64(_))
));
}
#[test]
fn apply_snapshot_handles_multiple_publish_batches() {
let tmp = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(tmp.path()).expect("open rocksdb");
let notif_uri = "https://example.net/notification.xml";
let sid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let total = RRDP_SNAPSHOT_APPLY_BATCH_SIZE + 7;
let mut xml =
format!(r#""#);
for i in 0..total {
let uri = format!("rsync://example.net/repo/{i:04}.roa");
let bytes = format!("payload-{i}").into_bytes();
let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
xml.push_str(&format!(r#"{b64}"#));
}
xml.push_str("");
let published =
apply_snapshot(&store, notif_uri, None, xml.as_bytes(), sid, 1).expect("apply snapshot");
assert_eq!(published, total);
for idx in [0usize, RRDP_SNAPSHOT_APPLY_BATCH_SIZE - 1, total - 1] {
let uri = format!("rsync://example.net/repo/{idx:04}.roa");
let got = store
.load_current_object_bytes_by_uri(&uri)
.expect("load object")
.expect("object exists");
assert_eq!(got, format!("payload-{idx}").into_bytes());
}
}