// 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()); } }