use super::*; fn pack_time(hour: i64) -> PackTime { PackTime::from_utc_offset_datetime( time::OffsetDateTime::UNIX_EPOCH + time::Duration::hours(hour), ) } fn sha256_hex(input: &[u8]) -> String { hex::encode(compute_sha256_32(input)) } fn sha256_32(input: &[u8]) -> [u8; 32] { compute_sha256_32(input) } fn sample_child_certificate_cache_projection( cache_key_sha256_hex: String, child_cert_uri: &str, child_cert_sha256_hex: &str, ) -> ChildCertificateCacheProjection { ChildCertificateCacheProjection { schema_version: CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION, algorithm_version: CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION, cache_key_sha256_hex, child_cert_uri: child_cert_uri.to_string(), child_cert_sha256_hex: child_cert_sha256_hex.to_string(), child_cert_serial: vec![1], issuer_ca_sha256_hex: sha256_hex(b"issuer-ca"), issuer_crl_uri: "rsync://example.test/repo/issuer.crl".to_string(), issuer_crl_sha256_hex: sha256_hex(b"issuer-crl"), parent_context_digest: sha256_32(b"parent-context"), validation_policy_fingerprint: sha256_32(b"policy"), effective_not_before: pack_time(0), effective_until: pack_time(24), payload: ChildCertificateCachePayload::ChildCa { child_manifest_rsync_uri: format!("{child_cert_uri}.mft"), child_ski: "11".repeat(20), child_rsync_base_uri: "rsync://example.test/repo/child/".to_string(), child_publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(), child_rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()), child_effective_ip_resources: None, child_effective_as_resources: None, }, } } #[test] fn parse_work_db_blob_mode_accepts_supported_values() { assert_eq!(default_work_db_blob_mode(), WorkDbBlobMode::Disabled); assert_eq!( parse_work_db_blob_mode("default"), Some(WorkDbBlobMode::Disabled) ); assert_eq!( parse_work_db_blob_mode("current"), Some(WorkDbBlobMode::Current) ); assert_eq!( parse_work_db_blob_mode("legacy"), Some(WorkDbBlobMode::Current) ); assert_eq!( parse_work_db_blob_mode("disabled"), Some(WorkDbBlobMode::Disabled) ); assert_eq!( parse_work_db_blob_mode("no-blob"), Some(WorkDbBlobMode::Disabled) ); assert_eq!(parse_work_db_blob_mode("lz4"), Some(WorkDbBlobMode::Lz4)); assert_eq!( parse_work_db_blob_mode("blob-lz4"), Some(WorkDbBlobMode::Lz4) ); assert_eq!(parse_work_db_blob_mode("unexpected"), None); } #[test] fn parse_work_db_memory_profile_accepts_supported_values() { assert_eq!( parse_work_db_memory_profile("default"), Some(WorkDbMemoryProfile::Default) ); assert_eq!( parse_work_db_memory_profile("none"), Some(WorkDbMemoryProfile::Default) ); assert_eq!( parse_work_db_memory_profile("compact"), Some(WorkDbMemoryProfile::Compact) ); assert_eq!( parse_work_db_memory_profile("low-memory"), Some(WorkDbMemoryProfile::Compact) ); assert_eq!(parse_work_db_memory_profile("unexpected"), None); } #[test] fn vcir_field_size_breakdown_counts_local_outputs_and_artifacts() { let vcir = sample_vcir("rsync://example.test/repo/current.mft"); let breakdown = VcirFieldSizeBreakdown::from_vcir(&vcir); assert_eq!(breakdown.local_output_count, 2); assert_eq!( breakdown.local_output_payload_json_bytes, vcir.local_outputs .iter() .map(|output| output.payload_json().len() as u64) .sum::() ); assert_eq!( breakdown.local_output_rule_hash_hex_bytes, vcir.local_outputs.len() as u64 * 64 ); assert_eq!(breakdown.related_artifact_count, 2); assert!(breakdown.related_artifact_uri_bytes > 0); assert_eq!(breakdown.child_entry_count, 1); assert!(breakdown.child_entry_uri_bytes > 0); assert_eq!( breakdown.local_output_old_projection_bytes(), breakdown.local_output_source_type_bytes + breakdown.local_output_source_hash_hex_bytes + breakdown.local_output_source_ee_hash_hex_bytes + breakdown.local_output_payload_json_bytes + breakdown.local_output_rule_hash_hex_bytes ); assert!( breakdown.local_output_old_projection_bytes() > breakdown.local_output_typed_projection_bytes() ); } fn sample_repository_view_entry(rsync_uri: &str, bytes: &[u8]) -> RepositoryViewEntry { RepositoryViewEntry { rsync_uri: rsync_uri.to_string(), current_hash: Some(sha256_hex(bytes)), repository_source: Some("https://rrdp.example.test/notification.xml".to_string()), object_type: Some("cer".to_string()), state: RepositoryViewState::Present, } } fn sample_raw_by_hash_entry(bytes: Vec) -> RawByHashEntry { RawByHashEntry { sha256_hex: sha256_hex(&bytes), bytes, origin_uris: vec!["rsync://example.test/repo/object.cer".to_string()], object_type: Some("cer".to_string()), encoding: Some("der".to_string()), } } fn sample_ccr_manifest_projection( manifest_rsync_uri: &str, manifest_this_update: PackTime, subordinate_skis: Vec>, ) -> VcirCcrManifestProjection { VcirCcrManifestProjection { manifest_rsync_uri: manifest_rsync_uri.to_string(), manifest_sha256: vec![0x11; 32], manifest_size: 4096, manifest_ee_aki: vec![0x22; 20], manifest_number_be: vec![3], manifest_this_update, manifest_sia_locations_der: vec![vec![ 0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05, b'r', b's', b'y', b'n', b'c', ]], subordinate_skis, } } fn sample_vcir(manifest_rsync_uri: &str) -> ValidatedCaInstanceResult { let roa_bytes = b"roa-object".to_vec(); let ee_bytes = b"ee-cert".to_vec(); let child_bytes = b"child-cert".to_vec(); let child_ski = "1234567890abcdef1234567890abcdef12345678".to_string(); ValidatedCaInstanceResult { manifest_rsync_uri: manifest_rsync_uri.to_string(), parent_manifest_rsync_uri: Some("rsync://example.test/repo/parent/parent.mft".to_string()), tal_id: "apnic".to_string(), ca_subject_name: "CN=Example CA".to_string(), ca_ski: "00112233445566778899aabbccddeeff00112233".to_string(), issuer_ski: "ffeeddccbbaa99887766554433221100ffeeddcc".to_string(), last_successful_validation_time: pack_time(0), current_manifest_rsync_uri: manifest_rsync_uri.to_string(), current_crl_rsync_uri: "rsync://example.test/repo/current.crl".to_string(), validated_manifest_meta: ValidatedManifestMeta { validated_manifest_number: vec![3], validated_manifest_this_update: pack_time(0), validated_manifest_next_update: pack_time(24), }, ccr_manifest_projection: sample_ccr_manifest_projection( manifest_rsync_uri, pack_time(0), vec![hex::decode(&child_ski).expect("decode child ski")], ), instance_gate: VcirInstanceGate { manifest_next_update: pack_time(24), current_crl_next_update: pack_time(12), self_ca_not_after: pack_time(48), instance_effective_until: pack_time(12), }, child_entries: vec![VcirChildEntry { child_manifest_rsync_uri: "rsync://example.test/repo/child/child.mft".to_string(), child_cert_rsync_uri: "rsync://example.test/repo/child/child.cer".to_string(), child_cert_hash: sha256_hex(&child_bytes), child_ski, child_rsync_base_uri: "rsync://example.test/repo/child/".to_string(), child_publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(), child_rrdp_notification_uri: Some("https://example.test/child-notify.xml".to_string()), child_effective_ip_resources: None, child_effective_as_resources: None, accepted_at_validation_time: pack_time(0), }], local_outputs: vec![ VcirLocalOutput { output_type: VcirOutputType::Vrp, item_effective_until: pack_time(12), source_object_uri: "rsync://example.test/repo/object.roa".to_string(), source_object_type: VcirSourceObjectType::Roa, source_object_hash: sha256_32(&roa_bytes), source_ee_cert_hash: sha256_32(&ee_bytes), payload: VcirLocalOutputPayload::Vrp { asn: 64496, afi: crate::data_model::roa::RoaAfi::Ipv4, prefix_len: 24, addr: { let mut addr = [0u8; 16]; addr[..4].copy_from_slice(&[203, 0, 113, 0]); addr }, max_length: 24, }, rule_hash: sha256_32(b"vrp-rule-1"), }, VcirLocalOutput { output_type: VcirOutputType::Aspa, item_effective_until: pack_time(10), source_object_uri: "rsync://example.test/repo/object.asa".to_string(), source_object_type: VcirSourceObjectType::Aspa, source_object_hash: sha256_32(b"aspa-object"), source_ee_cert_hash: sha256_32(b"aspa-ee-cert"), payload: VcirLocalOutputPayload::Aspa { customer_as_id: 64496, provider_as_ids: vec![64497], }, rule_hash: sha256_32(b"aspa-rule-1"), }, ], related_artifacts: vec![ VcirRelatedArtifact { artifact_role: VcirArtifactRole::Manifest, artifact_kind: VcirArtifactKind::Mft, uri: Some(manifest_rsync_uri.to_string()), sha256: sha256_hex(b"manifest-object"), object_type: Some("mft".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, }, VcirRelatedArtifact { artifact_role: VcirArtifactRole::CurrentCrl, artifact_kind: VcirArtifactKind::Crl, uri: Some("rsync://example.test/repo/current.crl".to_string()), sha256: sha256_hex(b"current-crl"), object_type: Some("crl".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, }, ], summary: VcirSummary { local_vrp_count: 1, local_aspa_count: 1, local_router_key_count: 0, child_count: 1, accepted_object_count: 4, rejected_object_count: 0, }, audit_summary: VcirAuditSummary { failed_fetch_eligible: true, last_failed_fetch_reason: None, warning_count: 0, audit_flags: vec!["validated-fresh".to_string()], }, } } #[test] fn vcir_ccr_manifest_projection_validate_accepts_valid_projection() { let projection = sample_ccr_manifest_projection( "rsync://example.test/repo/current.mft", pack_time(0), vec![vec![0x33; 20], vec![0x44; 20]], ); projection.validate_internal().expect("valid projection"); } #[test] fn vcir_ccr_manifest_projection_validate_rejects_invalid_fields() { let mut bad_hash = sample_ccr_manifest_projection( "rsync://example.test/repo/current.mft", pack_time(0), vec![vec![0x33; 20]], ); bad_hash.manifest_sha256 = vec![0x11; 31]; assert!(matches!( bad_hash.validate_internal(), Err(StorageError::InvalidData { .. }) )); let mut bad_locations = sample_ccr_manifest_projection( "rsync://example.test/repo/current.mft", pack_time(0), vec![vec![0x33; 20]], ); bad_locations.manifest_sia_locations_der = vec![vec![0x04, 0x00]]; assert!(matches!( bad_locations.validate_internal(), Err(StorageError::InvalidData { .. }) )); let bad_subordinates = sample_ccr_manifest_projection( "rsync://example.test/repo/current.mft", pack_time(0), vec![vec![0x44; 20], vec![0x33; 20]], ); assert!(matches!( bad_subordinates.validate_internal(), Err(StorageError::InvalidData { .. }) )); } #[test] fn roa_cache_projection_from_vcir_keeps_only_roa_vrp_outputs() { let vcir = sample_vcir("rsync://example.test/repo/current.mft"); let projection = RoaCacheProjection::from_vcir(&vcir) .expect("projection build") .expect("projection exists"); assert_eq!(projection.manifest_rsync_uri, vcir.manifest_rsync_uri); assert_eq!(projection.instance_effective_until, pack_time(12)); assert_eq!(projection.crl_sha256_by_uri.len(), 1); assert_eq!(projection.entries.len(), 1); assert_eq!( projection.entries[0].source_object_uri, "rsync://example.test/repo/object.roa" ); assert_eq!( projection.entries[0].outputs_effective_until_unix, 12 * 3600 ); assert_eq!(projection.entries[0].outputs.len(), 1); assert!(matches!( projection.entries[0].outputs[0].payload, VcirLocalOutputPayload::Vrp { .. } )); } #[test] fn roa_cache_projection_groups_multiple_outputs_by_roa_uri() { let mut vcir = sample_vcir("rsync://example.test/repo/current.mft"); let mut second = vcir.local_outputs[0].clone(); second.rule_hash = sha256_32(b"vrp-rule-2"); if let VcirLocalOutputPayload::Vrp { max_length, .. } = &mut second.payload { *max_length = 25; } vcir.local_outputs.push(second); vcir.summary.local_vrp_count = 2; let projection = RoaCacheProjection::from_vcir(&vcir) .expect("projection build") .expect("projection exists"); assert_eq!(projection.entries.len(), 1); assert_eq!(projection.entries[0].outputs.len(), 2); assert_eq!( projection.entries[0].outputs_effective_until_unix, 12 * 3600 ); } #[test] fn publication_point_cache_projection_roundtrips_with_vcir() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let vcir = sample_vcir("rsync://example.test/repo/current.mft"); let projection = PublicationPointCacheProjection::from_vcir_with_context( &vcir, "rsync://example.test/repo/".to_string(), Some("rsync://example.test/repo/ca.cer".to_string()), sha256_32(b"ca-cert"), sha256_32(b"manifest"), sha256_32(b"ta-context"), sha256_32(b"parent-context"), sha256_32(b"policy"), ) .expect("build publication point projection"); store .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) .expect("put vcir with publication point projection"); let got_vcir = store .get_vcir(&vcir.manifest_rsync_uri) .expect("get vcir") .expect("vcir exists"); assert_eq!(got_vcir, vcir); let got_projection = store .get_publication_point_cache_projection(&vcir.manifest_rsync_uri) .expect("get publication point projection") .expect("projection exists"); assert_eq!(got_projection, projection); assert_eq!(got_projection.outputs.len(), 2); assert_eq!(got_projection.children.len(), 1); assert_eq!(got_projection.related_objects.len(), 2); } #[test] fn publication_point_cache_projection_index_updates_after_first_read() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let vcir = sample_vcir("rsync://example.test/repo/current.mft"); let mut projection = PublicationPointCacheProjection::from_vcir_with_context( &vcir, "rsync://example.test/repo/".to_string(), Some("rsync://example.test/repo/ca.cer".to_string()), sha256_32(b"ca-cert"), sha256_32(b"manifest-old"), sha256_32(b"ta-context"), sha256_32(b"parent-context"), sha256_32(b"policy"), ) .expect("build old publication point projection"); store .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) .expect("put old projection"); let got_old = store .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) .expect("get old projection") .expect("old projection exists"); assert_eq!(got_old.manifest_sha256, sha256_32(b"manifest-old")); projection.manifest_sha256 = sha256_32(b"manifest-new"); store .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) .expect("put new projection"); let got_new = store .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) .expect("get new projection") .expect("new projection exists"); assert_eq!(got_new.manifest_sha256, sha256_32(b"manifest-new")); } #[test] fn publication_point_cache_projection_cached_empty_db_accepts_bounded_new_entries() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let vcir = sample_vcir("rsync://example.test/repo/current.mft"); let projection = PublicationPointCacheProjection::from_vcir_with_context( &vcir, "rsync://example.test/repo/".to_string(), Some("rsync://example.test/repo/ca.cer".to_string()), sha256_32(b"ca-cert"), sha256_32(b"manifest"), sha256_32(b"ta-context"), sha256_32(b"parent-context"), sha256_32(b"policy"), ) .expect("build publication point projection"); assert!( store .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) .expect("cached empty lookup") .is_none() ); store .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) .expect("put projection after cached empty lookup"); assert_eq!( store .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) .expect("cached lookup sees bounded new entry") .expect("cached projection exists"), projection ); assert_eq!( store .get_publication_point_cache_projection(&vcir.manifest_rsync_uri) .expect("direct db lookup") .expect("projection exists"), projection ); } #[test] fn publication_point_cache_mmap_index_refresh_roundtrips_after_reopen() { let td = tempfile::tempdir().expect("tempdir"); let db_path = td.path().join("work-db"); let vcir = sample_vcir("rsync://example.test/repo/current.mft"); let projection = PublicationPointCacheProjection::from_vcir_with_context( &vcir, "rsync://example.test/repo/".to_string(), Some("rsync://example.test/repo/ca.cer".to_string()), sha256_32(b"ca-cert"), sha256_32(b"manifest"), sha256_32(b"ta-context"), sha256_32(b"parent-context"), sha256_32(b"policy"), ) .expect("build publication point projection"); { let store = RocksStore::open(&db_path).expect("open rocksdb"); store .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) .expect("put projection"); let stats = store .refresh_publication_point_cache_mmap_index() .expect("refresh mmap index") .expect("refresh stats"); assert_eq!(stats.new_entries, 1); assert_eq!(stats.state, "written"); } let store = RocksStore::open(&db_path).expect("reopen rocksdb"); let got = store .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) .expect("get cached projection from mmap") .expect("projection exists"); assert_eq!(got, projection); } #[test] fn publication_point_cache_mmap_index_dirty_overlay_wins_and_refreshes_segment() { let td = tempfile::tempdir().expect("tempdir"); let db_path = td.path().join("work-db"); let vcir = sample_vcir("rsync://example.test/repo/current.mft"); let mut projection = PublicationPointCacheProjection::from_vcir_with_context( &vcir, "rsync://example.test/repo/".to_string(), Some("rsync://example.test/repo/ca.cer".to_string()), sha256_32(b"ca-cert"), sha256_32(b"manifest-old"), sha256_32(b"ta-context"), sha256_32(b"parent-context"), sha256_32(b"policy"), ) .expect("build publication point projection"); { let store = RocksStore::open(&db_path).expect("open rocksdb"); store .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) .expect("put old projection"); store .refresh_publication_point_cache_mmap_index() .expect("refresh base mmap index"); } { let store = RocksStore::open(&db_path).expect("reopen rocksdb"); assert_eq!( store .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) .expect("get old projection") .expect("old projection exists") .manifest_sha256, sha256_32(b"manifest-old") ); projection.manifest_sha256 = sha256_32(b"manifest-new"); store .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) .expect("put new projection"); assert_eq!( store .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) .expect("get dirty projection") .expect("dirty projection exists") .manifest_sha256, sha256_32(b"manifest-new") ); let stats = store .refresh_publication_point_cache_mmap_index() .expect("refresh dirty mmap segment") .expect("refresh stats"); assert_eq!(stats.state, "segment_written"); assert_eq!(stats.dirty_entries, 1); } let store = RocksStore::open(&db_path).expect("reopen rocksdb after segment"); let got = store .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) .expect("get refreshed projection") .expect("projection exists"); assert_eq!(got.manifest_sha256, sha256_32(b"manifest-new")); } #[test] fn publication_point_cache_mmap_index_write_before_read_refreshes_segment() { let td = tempfile::tempdir().expect("tempdir"); let db_path = td.path().join("work-db"); let vcir = sample_vcir("rsync://example.test/repo/current.mft"); let mut projection = PublicationPointCacheProjection::from_vcir_with_context( &vcir, "rsync://example.test/repo/".to_string(), Some("rsync://example.test/repo/ca.cer".to_string()), sha256_32(b"ca-cert"), sha256_32(b"manifest-old"), sha256_32(b"ta-context"), sha256_32(b"parent-context"), sha256_32(b"policy"), ) .expect("build publication point projection"); { let store = RocksStore::open(&db_path).expect("open rocksdb"); store .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) .expect("put old projection"); let stats = store .refresh_publication_point_cache_mmap_index() .expect("refresh base mmap index") .expect("refresh stats"); assert_eq!(stats.state, "written"); } { let store = RocksStore::open(&db_path).expect("reopen rocksdb"); projection.manifest_sha256 = sha256_32(b"manifest-new"); store .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) .expect("put new projection before any cached read"); let stats = store .refresh_publication_point_cache_mmap_index() .expect("refresh dirty mmap segment") .expect("refresh stats"); assert_eq!(stats.state, "segment_written"); assert_eq!(stats.dirty_entries, 1); assert_eq!(stats.new_entries, 1); } let store = RocksStore::open(&db_path).expect("reopen rocksdb after segment"); let got = store .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) .expect("get refreshed projection") .expect("projection exists"); assert_eq!(got.manifest_sha256, sha256_32(b"manifest-new")); } #[test] fn publication_point_cache_mmap_index_delete_before_read_refreshes_tombstone_segment() { let td = tempfile::tempdir().expect("tempdir"); let db_path = td.path().join("work-db"); let vcir = sample_vcir("rsync://example.test/repo/current.mft"); let projection = PublicationPointCacheProjection::from_vcir_with_context( &vcir, "rsync://example.test/repo/".to_string(), Some("rsync://example.test/repo/ca.cer".to_string()), sha256_32(b"ca-cert"), sha256_32(b"manifest"), sha256_32(b"ta-context"), sha256_32(b"parent-context"), sha256_32(b"policy"), ) .expect("build publication point projection"); { let store = RocksStore::open(&db_path).expect("open rocksdb"); store .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) .expect("put projection"); let stats = store .refresh_publication_point_cache_mmap_index() .expect("refresh base mmap index") .expect("refresh stats"); assert_eq!(stats.state, "written"); } { let store = RocksStore::open(&db_path).expect("reopen rocksdb"); store .replace_vcir_manifest_replay_meta_and_projection_action( &vcir, None, PublicationPointCacheProjectionWriteAction::Delete { manifest_rsync_uri: &vcir.manifest_rsync_uri, }, ) .expect("delete projection before any cached read"); assert!( store .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) .expect("dirty tombstone lookup") .is_none() ); let stats = store .refresh_publication_point_cache_mmap_index() .expect("refresh tombstone mmap segment") .expect("refresh stats"); assert_eq!(stats.state, "segment_written"); assert_eq!(stats.dirty_entries, 1); assert_eq!(stats.new_entries, 1); } let store = RocksStore::open(&db_path).expect("reopen rocksdb after tombstone segment"); assert!( store .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) .expect("tombstone shadows old current index") .is_none() ); assert!( store .get_publication_point_cache_projection(&vcir.manifest_rsync_uri) .expect("direct db lookup") .is_none() ); } #[test] fn publication_point_cache_projection_rejects_version_mismatch() { let vcir = sample_vcir("rsync://example.test/repo/current.mft"); let mut projection = PublicationPointCacheProjection::from_vcir_with_context( &vcir, "rsync://example.test/repo/".to_string(), Some("rsync://example.test/repo/ca.cer".to_string()), sha256_32(b"ca-cert"), sha256_32(b"manifest"), sha256_32(b"ta-context"), sha256_32(b"parent-context"), sha256_32(b"policy"), ) .expect("build publication point projection"); projection.schema_version = PUBLICATION_POINT_CACHE_SCHEMA_VERSION + 1; assert!(matches!( projection.validate_internal(), Err(StorageError::InvalidData { .. }) )); } #[test] fn roa_cache_projection_rejects_duplicate_uri_with_different_hash() { let mut vcir = sample_vcir("rsync://example.test/repo/current.mft"); let mut duplicate = vcir.local_outputs[0].clone(); duplicate.source_object_hash = sha256_32(b"different-roa"); duplicate.rule_hash = sha256_32(b"vrp-rule-2"); vcir.local_outputs.push(duplicate); vcir.summary.local_vrp_count = 2; let err = RoaCacheProjection::from_vcir(&vcir) .expect_err("same ROA URI with different hash must fail"); assert!(err.to_string().contains("source object hash mismatch")); } fn sample_rrdp_source_record(notify_uri: &str) -> RrdpSourceRecord { RrdpSourceRecord { notify_uri: notify_uri.to_string(), last_session_id: Some("session-1".to_string()), last_serial: Some(42), first_seen_at: pack_time(0), last_seen_at: pack_time(1), last_sync_at: Some(pack_time(1)), sync_state: RrdpSourceSyncState::DeltaReady, last_snapshot_uri: Some("https://rrdp.example.test/snapshot.xml".to_string()), last_snapshot_hash: Some(sha256_hex(b"snapshot-bytes")), last_error: None, } } fn sample_rrdp_source_member_record( notify_uri: &str, rsync_uri: &str, serial: u64, ) -> RrdpSourceMemberRecord { RrdpSourceMemberRecord { notify_uri: notify_uri.to_string(), rsync_uri: rsync_uri.to_string(), current_hash: Some(sha256_hex(rsync_uri.as_bytes())), object_type: Some("cer".to_string()), present: true, last_confirmed_session_id: "session-1".to_string(), last_confirmed_serial: serial, last_changed_at: pack_time(serial as i64), } } fn sample_rrdp_uri_owner_record(notify_uri: &str, rsync_uri: &str) -> RrdpUriOwnerRecord { RrdpUriOwnerRecord { rsync_uri: rsync_uri.to_string(), notify_uri: notify_uri.to_string(), current_hash: Some(sha256_hex(rsync_uri.as_bytes())), last_confirmed_session_id: "session-1".to_string(), last_confirmed_serial: 7, last_changed_at: pack_time(7), owner_state: RrdpUriOwnerState::Active, } } #[test] fn repository_view_and_raw_by_hash_roundtrip() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let entry1 = sample_repository_view_entry("rsync://example.test/repo/a.cer", b"object-a"); let entry2 = sample_repository_view_entry("rsync://example.test/repo/sub/b.roa", b"object-b"); store .put_repository_view_entry(&entry1) .expect("put repository view entry1"); store .put_repository_view_entry(&entry2) .expect("put repository view entry2"); let got1 = store .get_repository_view_entry(&entry1.rsync_uri) .expect("get repository view entry1") .expect("entry1 exists"); assert_eq!(got1, entry1); let got_prefix = store .list_repository_view_entries_with_prefix("rsync://example.test/repo/sub/") .expect("list repository view prefix"); assert_eq!(got_prefix, vec![entry2.clone()]); store .delete_repository_view_entry(&entry1.rsync_uri) .expect("delete repository view entry1"); assert!( store .get_repository_view_entry(&entry1.rsync_uri) .expect("get deleted repository view entry1") .is_none() ); let raw = sample_raw_by_hash_entry(b"raw-der-object".to_vec()); store .put_raw_by_hash_entry(&raw) .expect("put raw_by_hash entry"); let got_raw = store .get_raw_by_hash_entry(&raw.sha256_hex) .expect("get raw_by_hash entry") .expect("raw entry exists"); assert_eq!(got_raw, raw); } #[test] fn raw_by_hash_routes_to_external_raw_store_when_configured() { let td = tempfile::tempdir().expect("tempdir"); let main_db = td.path().join("main-db"); let raw_db = td.path().join("raw-store.db"); let raw = sample_raw_by_hash_entry(b"external-raw".to_vec()); { let store = RocksStore::open_with_external_raw_store(&main_db, &raw_db).expect("open store"); store.put_raw_by_hash_entry(&raw).expect("put external raw"); let got = store .get_raw_by_hash_entry(&raw.sha256_hex) .expect("get external raw") .expect("raw exists"); assert_eq!(got, raw); } let main_store = RocksStore::open(&main_db).expect("open main only"); assert!( main_store .get_raw_by_hash_entry(&raw.sha256_hex) .expect("read main store") .is_none(), "main db should not contain raw entry when external raw store is configured" ); } #[test] fn put_blob_bytes_batch_uses_internal_blob_cf_without_raw_entry() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let bytes = b"internal-blob-only".to_vec(); let hash = sha256_hex(&bytes); store .put_blob_bytes_batch(&[(hash.clone(), bytes.clone())]) .expect("put blob bytes"); assert_eq!( store.get_blob_bytes(&hash).expect("get blob bytes"), Some(bytes.clone()) ); assert!( store .get_raw_by_hash_entry(&hash) .expect("get raw entry") .is_none() ); } #[test] fn put_blob_bytes_batch_routes_to_external_raw_store_without_raw_entry() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open_with_external_raw_store( &td.path().join("main-db"), &td.path().join("raw-store.db"), ) .expect("open store"); let bytes = b"external-blob-only".to_vec(); let hash = sha256_hex(&bytes); store .put_blob_bytes_batch(&[(hash.clone(), bytes.clone())]) .expect("put external blob bytes"); assert_eq!(store.get_blob_bytes(&hash).unwrap(), Some(bytes)); assert!(store.get_raw_by_hash_entry(&hash).unwrap().is_none()); } #[test] fn repo_bytes_db_is_physically_separate_from_external_raw_store() { let td = tempfile::tempdir().expect("tempdir"); let main_db = td.path().join("main-db"); let raw_db = td.path().join("raw-store.db"); let repo_bytes_db = td.path().join("repo-bytes.db"); let store = RocksStore::open_with_external_stores(&main_db, Some(&raw_db), Some(&repo_bytes_db)) .expect("open store"); let repo_bytes = b"repo-object".to_vec(); let repo_hash = sha256_hex(&repo_bytes); let raw = sample_raw_by_hash_entry(b"raw-evidence".to_vec()); store .put_blob_bytes_batch(&[(repo_hash.clone(), repo_bytes.clone())]) .expect("put repo bytes"); store.put_raw_by_hash_entry(&raw).expect("put raw evidence"); assert_eq!(store.get_blob_bytes(&repo_hash).unwrap(), Some(repo_bytes)); assert_eq!( store.get_raw_by_hash_entry(&raw.sha256_hex).unwrap(), Some(raw.clone()) ); drop(store); let raw_only = RocksStore::open_with_external_raw_store(&td.path().join("raw-reader"), &raw_db) .expect("open raw only"); assert!( raw_only.get_blob_bytes(&repo_hash).unwrap().is_none(), "repo object bytes must not be written into raw-store.db" ); let repo_only = RocksStore::open_with_external_repo_bytes(&td.path().join("repo-reader"), &repo_bytes_db) .expect("open repo bytes only"); assert_eq!( repo_only.get_blob_bytes(&repo_hash).unwrap(), Some(b"repo-object".to_vec()) ); assert!( repo_only.get_blob_bytes(&raw.sha256_hex).unwrap().is_none(), "raw evidence bytes must not be written into repo-bytes.db" ); } #[test] fn memory_snapshot_includes_work_db_and_external_stores() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open_with_external_stores( &td.path().join("main-db"), Some(&td.path().join("raw-store.db")), Some(&td.path().join("repo-bytes.db")), ) .expect("open store"); let snapshot = store.memory_snapshot(); let labels: Vec<&str> = snapshot .databases .iter() .map(|db| db.label.as_str()) .collect(); assert_eq!(labels, vec!["work-db", "raw-store.db", "repo-bytes.db"]); assert!( snapshot.databases[0] .column_families .iter() .any(|cf| cf.name == CF_REPOSITORY_VIEW) ); serde_json::to_value(&snapshot).expect("serialize memory snapshot"); } #[test] fn put_blob_bytes_batch_accepts_empty_batch_with_external_raw_store() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open_with_external_raw_store( &td.path().join("main-db"), &td.path().join("raw-store.db"), ) .expect("open store"); store .put_blob_bytes_batch(&[]) .expect("empty external blob batch should be a no-op"); } #[test] fn get_blob_bytes_internal_falls_back_to_raw_entry_when_blob_missing() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let raw = sample_raw_by_hash_entry(b"raw-fallback".to_vec()); store.put_raw_by_hash_entry(&raw).expect("put raw entry"); assert_eq!( store .get_blob_bytes(&raw.sha256_hex) .expect("get blob bytes via raw fallback"), Some(raw.bytes.clone()) ); } #[test] fn get_blob_bytes_batch_internal_prefers_blob_cf_and_falls_back_to_raw_entry() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let blob_bytes = b"blob-cf-object".to_vec(); let blob_hash = sha256_hex(&blob_bytes); store .put_blob_bytes_batch(&[(blob_hash.clone(), blob_bytes.clone())]) .expect("put blob bytes"); let raw = sample_raw_by_hash_entry(b"raw-fallback-batch".to_vec()); store.put_raw_by_hash_entry(&raw).expect("put raw fallback"); let batch = store .get_blob_bytes_batch(&[blob_hash.clone(), raw.sha256_hex.clone(), "00".repeat(32)]) .expect("get blob bytes batch"); assert_eq!(batch, vec![Some(blob_bytes), Some(raw.bytes.clone()), None]); } #[test] fn get_blob_bytes_batch_routes_to_external_raw_store_without_raw_entry() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open_with_external_raw_store( &td.path().join("main-db"), &td.path().join("raw-store.db"), ) .expect("open store"); let bytes = b"external-batch-blob".to_vec(); let hash = sha256_hex(&bytes); store .put_blob_bytes_batch(&[(hash.clone(), bytes.clone())]) .expect("put external blob bytes"); assert_eq!( store .get_blob_bytes_batch(&[hash, "00".repeat(32)]) .expect("get external blob batch"), vec![Some(bytes), None] ); } #[test] fn get_blob_bytes_rejects_invalid_hash_for_internal_store() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let err = store .get_blob_bytes("not-a-valid-hash") .expect_err("invalid hash must fail"); assert!(matches!(err, StorageError::InvalidData { .. })); } #[test] fn get_blob_bytes_batch_rejects_invalid_hash_for_internal_store() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let err = store .get_blob_bytes_batch(&["not-a-valid-hash".to_string()]) .expect_err("invalid hash must fail"); assert!(matches!(err, StorageError::InvalidData { .. })); } #[test] fn get_blob_bytes_batch_returns_empty_for_empty_request_internal() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); assert!( store .get_blob_bytes_batch(&[]) .expect("empty blob batch request") .is_empty() ); } #[test] fn put_blob_bytes_batch_accepts_empty_batch() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); store .put_blob_bytes_batch(&[]) .expect("empty blob batch should be a no-op"); } #[test] fn put_blob_bytes_batch_rejects_empty_bytes() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let err = store .put_blob_bytes_batch(&[(sha256_hex(b"valid"), Vec::new())]) .expect_err("empty bytes must fail"); assert!(matches!(err, StorageError::InvalidData { .. })); } #[test] fn delete_raw_by_hash_entry_internal_preserves_blob_bytes() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let bytes = b"blob-persists-after-raw-delete".to_vec(); let hash = sha256_hex(&bytes); let raw = RawByHashEntry::from_bytes(hash.clone(), bytes.clone()); store .put_blob_bytes_batch(&[(hash.clone(), bytes.clone())]) .expect("put blob bytes"); store.put_raw_by_hash_entry(&raw).expect("put raw entry"); store .delete_raw_by_hash_entry(&hash) .expect("delete raw entry only"); assert!(store.get_raw_by_hash_entry(&hash).unwrap().is_none()); assert_eq!(store.get_blob_bytes(&hash).unwrap(), Some(bytes)); } #[test] fn delete_raw_by_hash_entry_rejects_invalid_hash() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let err = store .delete_raw_by_hash_entry("not-a-valid-hash") .expect_err("invalid hash must fail"); assert!(matches!(err, StorageError::InvalidData { .. })); } #[test] fn delete_raw_by_hash_entry_routes_to_external_raw_store() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open_with_external_raw_store( &td.path().join("main-db"), &td.path().join("raw-store.db"), ) .expect("open store"); let raw = sample_raw_by_hash_entry(b"external-delete".to_vec()); store.put_raw_by_hash_entry(&raw).expect("put raw entry"); store .delete_raw_by_hash_entry(&raw.sha256_hex) .expect("delete external raw entry"); assert!( store .get_raw_by_hash_entry(&raw.sha256_hex) .unwrap() .is_none() ); assert!(store.get_blob_bytes(&raw.sha256_hex).unwrap().is_none()); } #[test] fn repository_view_and_raw_by_hash_validation_errors_are_reported() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let invalid_view = RepositoryViewEntry { rsync_uri: "rsync://example.test/repo/withdrawn.cer".to_string(), current_hash: None, repository_source: None, object_type: None, state: RepositoryViewState::Present, }; let err = store .put_repository_view_entry(&invalid_view) .expect_err("missing current_hash must fail"); assert!(err.to_string().contains("current_hash is required")); let invalid_raw = RawByHashEntry { sha256_hex: sha256_hex(b"expected"), bytes: b"actual".to_vec(), origin_uris: vec!["rsync://example.test/repo/object.cer".to_string()], object_type: None, encoding: None, }; let err = store .put_raw_by_hash_entry(&invalid_raw) .expect_err("mismatched raw_by_hash entry must fail"); assert!(err.to_string().contains("does not match bytes")); } #[test] fn vcir_roundtrip_and_validation_failures_are_reported() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let vcir = sample_vcir("rsync://example.test/repo/current.mft"); store.put_vcir(&vcir).expect("put vcir"); let got = store .get_vcir(&vcir.manifest_rsync_uri) .expect("get vcir") .expect("vcir exists"); assert_eq!(got, vcir); let replay_meta = store .get_manifest_replay_meta(&vcir.manifest_rsync_uri) .expect("get manifest replay meta") .expect("manifest replay meta exists"); assert_eq!( replay_meta, ManifestReplayMeta { manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), manifest_number_be: vcir .validated_manifest_meta .validated_manifest_number .clone(), manifest_this_update: vcir .validated_manifest_meta .validated_manifest_this_update .clone(), manifest_sha256: vcir.ccr_manifest_projection.manifest_sha256.clone(), updated_at_validation_time: vcir.last_successful_validation_time.clone(), } ); let projection = store .get_roa_cache_projection(&vcir.manifest_rsync_uri) .expect("get roa cache projection") .expect("roa cache projection exists"); assert_eq!(projection.manifest_rsync_uri, vcir.manifest_rsync_uri); assert_eq!(projection.entries.len(), 1); assert_eq!( projection.entries[0].source_object_uri, "rsync://example.test/repo/object.roa" ); let mut invalid = sample_vcir("rsync://example.test/repo/invalid.mft"); invalid.summary.local_vrp_count = 9; let err = store .put_vcir(&invalid) .expect_err("invalid vcir must fail"); assert!(err.to_string().contains("local_vrp_count=9")); let mut invalid = sample_vcir("rsync://example.test/repo/invalid-2.mft"); invalid.instance_gate.instance_effective_until = pack_time(11); let err = store .put_vcir(&invalid) .expect_err("invalid instance gate must fail"); assert!(err.to_string().contains("instance_effective_until")); store .delete_vcir(&vcir.manifest_rsync_uri) .expect("delete vcir"); assert!( store .get_vcir(&vcir.manifest_rsync_uri) .expect("get deleted vcir") .is_none() ); assert!( store .get_manifest_replay_meta(&vcir.manifest_rsync_uri) .expect("get deleted manifest replay meta") .is_none() ); assert!( store .get_roa_cache_projection(&vcir.manifest_rsync_uri) .expect("get deleted roa cache projection") .is_none() ); } #[test] fn transport_prefetch_snapshot_roundtrips() { use crate::parallel::transport_prefetch::{ TransportPrefetchDedupKey, TransportPrefetchMode, TransportPrefetchRepoIdentity, TransportPrefetchRequest, TransportPrefetchRequester, TransportPrefetchSnapshot, }; use crate::policy::SyncPreference; let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); assert!( store .get_transport_prefetch_snapshot() .expect("get empty prefetch snapshot") .is_none() ); let snapshot = TransportPrefetchSnapshot::new( SyncPreference::RrdpThenRsync, vec![TransportPrefetchRequest { dedup_key: TransportPrefetchDedupKey::RrdpNotify { notification_uri: "https://example.test/notification.xml".to_string(), }, rsync_scope_uri: "rsync://example.test/repo/".to_string(), rsync_failure_scope_uri: Some("rsync://example.test/".to_string()), repo_identity: TransportPrefetchRepoIdentity { notification_uri: Some("https://example.test/notification.xml".to_string()), rsync_base_uri: "rsync://example.test/repo/".to_string(), }, mode: TransportPrefetchMode::Rrdp, last_result: None, last_rsync_result: None, tal_id: "apnic".to_string(), rir_id: "apnic".to_string(), priority: 0, requesters: vec![TransportPrefetchRequester { tal_id: "apnic".to_string(), rir_id: "apnic".to_string(), parent_node_id: None, ca_instance_handle_id: "apnic:rsync://example.test/repo/root.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/root.mft".to_string(), }], }], ); store .put_transport_prefetch_snapshot(&snapshot) .expect("put prefetch snapshot"); let got = store .get_transport_prefetch_snapshot() .expect("get prefetch snapshot") .expect("snapshot exists"); assert_eq!(got, snapshot); } #[test] fn manifest_replay_meta_validation_reports_invalid_fields() { let mut meta = ManifestReplayMeta { manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(), manifest_number_be: vec![3], manifest_this_update: pack_time(0), manifest_sha256: vec![0x11; 32], updated_at_validation_time: pack_time(1), }; meta.validate_internal().expect("valid replay meta"); meta.manifest_sha256 = vec![0x11; 31]; let err = meta .validate_internal() .expect_err("short manifest sha must fail"); assert!(err.to_string().contains("must be 32 bytes")); meta.manifest_sha256 = vec![0x11; 32]; meta.manifest_number_be = vec![0, 3]; let err = meta .validate_internal() .expect_err("non-minimal manifest number must fail"); assert!(err.to_string().contains("minimal big-endian")); } #[test] fn list_vcirs_returns_all_entries() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let vcir1 = sample_vcir("rsync://example.test/repo/a.mft"); let vcir2 = sample_vcir("rsync://example.test/repo/b.mft"); store.put_vcir(&vcir1).expect("put vcir1"); store.put_vcir(&vcir2).expect("put vcir2"); let mut got = store.list_vcirs().expect("list vcirs"); got.sort_by(|a, b| a.manifest_rsync_uri.cmp(&b.manifest_rsync_uri)); assert_eq!(got, vec![vcir1, vcir2]); } #[test] fn summarize_vcir_storage_aggregates_values_and_field_sizes() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let vcir1 = sample_vcir("rsync://example.test/repo/a.mft"); let vcir2 = sample_vcir("rsync://example.test/repo/b.mft"); store.put_vcir(&vcir1).expect("put vcir1"); store.put_vcir(&vcir2).expect("put vcir2"); let summary = store.summarize_vcir_storage().expect("summarize vcirs"); let mut expected_fields = VcirFieldSizeBreakdown::default(); expected_fields.add_assign(&VcirFieldSizeBreakdown::from_vcir(&vcir1)); expected_fields.add_assign(&VcirFieldSizeBreakdown::from_vcir(&vcir2)); assert_eq!(summary.entry_count, 2); assert!(summary.vcir_value_bytes > 0); assert!(summary.vcir_value_bytes_max > 0); assert!(summary.vcir_value_bytes_max_manifest_rsync_uri.is_some()); assert_eq!(summary.top_entries_by_vcir_value_bytes.len(), 2); assert!( summary.top_entries_by_vcir_value_bytes[0].vcir_value_bytes >= summary.top_entries_by_vcir_value_bytes[1].vcir_value_bytes ); assert_eq!(summary.field_sizes, expected_fields); assert!(summary.core_fields.manifest_rsync_uri_bytes > 0); assert!(summary.ccr_projection.manifest_sha256_bytes > 0); assert!(summary.child_resources.effective_ip_resource_cbor_bytes > 0); assert_eq!( summary.local_output_old_projection_bytes, expected_fields.local_output_old_projection_bytes() ); assert_eq!( summary.local_output_typed_projection_bytes, expected_fields.local_output_typed_projection_bytes() ); assert_eq!( summary.local_output_projection_saved_bytes, expected_fields.local_output_projection_saved_bytes() ); } #[test] fn replace_vcir_and_manifest_replay_meta_replaces_current_entry() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let mut previous = sample_vcir("rsync://example.test/repo/current.mft"); previous.local_outputs = vec![VcirLocalOutput { output_type: VcirOutputType::Vrp, item_effective_until: pack_time(10), source_object_uri: "rsync://example.test/repo/old.roa".to_string(), source_object_type: VcirSourceObjectType::Roa, source_object_hash: sha256_32(b"old-roa"), source_ee_cert_hash: sha256_32(b"old-ee"), payload: VcirLocalOutputPayload::Vrp { asn: 64496, afi: crate::data_model::roa::RoaAfi::Ipv4, prefix_len: 24, addr: { let mut addr = [0u8; 16]; addr[..4].copy_from_slice(&[203, 0, 113, 0]); addr }, max_length: 24, }, rule_hash: sha256_32(b"old-rule"), }]; previous.summary.local_vrp_count = 1; previous.summary.local_aspa_count = 0; previous.summary.local_router_key_count = 0; let previous_timing = store .replace_vcir_and_manifest_replay_meta(&previous) .expect("store previous vcir"); assert!(previous_timing.vcir_value_bytes > 0); assert!(previous_timing.replay_meta_value_bytes > 0); assert!(previous_timing.roa_cache_projection_value_bytes > 0); assert_eq!( previous_timing.total_encoded_bytes, previous_timing.vcir_value_bytes + previous_timing.replay_meta_value_bytes + previous_timing.roa_cache_projection_value_bytes ); assert!( store .get_roa_cache_projection(&previous.manifest_rsync_uri) .expect("get previous projection") .is_some() ); let mut current = sample_vcir("rsync://example.test/repo/current.mft"); current.local_outputs = vec![VcirLocalOutput { output_type: VcirOutputType::Aspa, item_effective_until: pack_time(11), source_object_uri: "rsync://example.test/repo/new.asa".to_string(), source_object_type: VcirSourceObjectType::Aspa, source_object_hash: sha256_32(b"new-aspa"), source_ee_cert_hash: sha256_32(b"new-ee"), payload: VcirLocalOutputPayload::Aspa { customer_as_id: 64496, provider_as_ids: vec![64497], }, rule_hash: sha256_32(b"new-rule"), }]; current.summary.local_vrp_count = 0; current.summary.local_aspa_count = 1; let current_timing = store .replace_vcir_and_manifest_replay_meta(¤t) .expect("replace vcir and replay meta"); assert!(current_timing.vcir_value_bytes > 0); assert!(current_timing.replay_meta_value_bytes > 0); assert_eq!(current_timing.roa_cache_projection_value_bytes, 0); assert_eq!( current_timing.total_encoded_bytes, current_timing.vcir_value_bytes + current_timing.replay_meta_value_bytes ); let got = store .get_vcir(¤t.manifest_rsync_uri) .expect("get replaced vcir") .expect("vcir exists"); assert_eq!(got, current); let replay_meta = store .get_manifest_replay_meta(¤t.manifest_rsync_uri) .expect("get replaced replay meta") .expect("replay meta exists"); assert_eq!( replay_meta.manifest_number_be, current.validated_manifest_meta.validated_manifest_number ); assert_eq!( replay_meta.manifest_sha256, current.ccr_manifest_projection.manifest_sha256 ); assert!( store .get_roa_cache_projection(¤t.manifest_rsync_uri) .expect("get current projection") .is_none() ); } #[test] fn get_child_certificate_cache_projections_batch_preserves_order_and_misses() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let first_key = sha256_hex(b"child-cache-key-first"); let missing_key = sha256_hex(b"child-cache-key-missing"); let second_key = sha256_hex(b"child-cache-key-second"); let first_hash = sha256_hex(b"first-child-cert"); let second_hash = sha256_hex(b"second-child-cert"); let first = sample_child_certificate_cache_projection( first_key.clone(), "rsync://example.test/repo/first.cer", &first_hash, ); let second = sample_child_certificate_cache_projection( second_key.clone(), "rsync://example.test/repo/second.cer", &second_hash, ); store .put_child_certificate_cache_projection(&first) .expect("put first projection"); store .put_child_certificate_cache_projection(&second) .expect("put second projection"); let got = store .get_child_certificate_cache_projections_batch(&[ second_key.clone(), missing_key, first_key.clone(), ]) .expect("batch get child projections"); assert_eq!(got.len(), 3); assert_eq!( got[0] .as_ref() .expect("second projection") .child_cert_sha256_hex, second_hash ); assert!(got[1].is_none()); assert_eq!( got[2] .as_ref() .expect("first projection") .child_cert_sha256_hex, first_hash ); } #[test] fn child_certificate_cache_mmap_segment_preserves_order_and_misses() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let manifest_uri = "rsync://example.test/repo/parent.mft"; let first_key = sha256_hex(b"child-cache-mmap-key-first"); let missing_key = sha256_hex(b"child-cache-mmap-key-missing"); let second_key = sha256_hex(b"child-cache-mmap-key-second"); let first_hash = sha256_hex(b"first-child-cert-mmap"); let second_hash = sha256_hex(b"second-child-cert-mmap"); let first = sample_child_certificate_cache_projection( first_key.clone(), "rsync://example.test/repo/first.cer", &first_hash, ); let second = sample_child_certificate_cache_projection( second_key.clone(), "rsync://example.test/repo/second.cer", &second_hash, ); assert!( store .get_child_certificate_cache_projections_mmap_segment( manifest_uri, &[first_key.clone()] ) .expect("missing segment lookup") .is_none() ); let write_stats = store .write_child_certificate_cache_mmap_segment(manifest_uri, &[first.clone(), second.clone()]) .expect("write child projection segment"); assert_eq!(write_stats.new_entries, 2); assert!(write_stats.file_bytes > 0); let got = store .get_child_certificate_cache_projections_mmap_segment( manifest_uri, &[second_key.clone(), missing_key, first_key.clone()], ) .expect("lookup child projection segment") .expect("segment exists"); assert_eq!(got.hits, 2); assert_eq!(got.misses, 1); assert!(got.file_bytes > 0); assert_eq!(got.projections.len(), 3); assert_eq!( got.projections[0] .as_ref() .expect("second projection") .child_cert_sha256_hex, second_hash ); assert!(got.projections[1].is_none()); assert_eq!( got.projections[2] .as_ref() .expect("first projection") .child_cert_sha256_hex, first_hash ); } #[test] fn child_certificate_cache_mmap_segment_overlay_preserves_existing_values() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let manifest_uri = "rsync://example.test/repo/parent-overlay.mft"; let first_key = sha256_hex(b"child-cache-overlay-key-first"); let second_key = sha256_hex(b"child-cache-overlay-key-second"); let missing_key = sha256_hex(b"child-cache-overlay-key-missing"); let first_hash = sha256_hex(b"first-child-cert-overlay"); let old_second_hash = sha256_hex(b"old-second-child-cert-overlay"); let new_second_hash = sha256_hex(b"new-second-child-cert-overlay"); let first = sample_child_certificate_cache_projection( first_key.clone(), "rsync://example.test/repo/first-overlay.cer", &first_hash, ); let old_second = sample_child_certificate_cache_projection( second_key.clone(), "rsync://example.test/repo/second-overlay.cer", &old_second_hash, ); let new_second = sample_child_certificate_cache_projection( second_key.clone(), "rsync://example.test/repo/second-overlay.cer", &new_second_hash, ); store .write_child_certificate_cache_mmap_segment( manifest_uri, &[first.clone(), old_second.clone()], ) .expect("write initial segment"); let stats = store .write_child_certificate_cache_mmap_segment_overlay( manifest_uri, &[first_key.clone(), second_key.clone(), missing_key.clone()], std::slice::from_ref(&new_second), ) .expect("write segment overlay"); assert_eq!(stats.new_entries, 2); let got = store .get_child_certificate_cache_projections_mmap_segment( manifest_uri, &[first_key.clone(), second_key.clone(), missing_key], ) .expect("lookup segment") .expect("segment exists"); assert_eq!(got.hits, 2); assert_eq!(got.misses, 1); assert_eq!( got.projections[0] .as_ref() .expect("first projection") .child_cert_sha256_hex, first_hash ); assert_eq!( got.projections[1] .as_ref() .expect("updated second projection") .child_cert_sha256_hex, new_second_hash ); assert!(got.projections[2].is_none()); } #[test] fn storage_helpers_cover_optional_validation_paths() { let withdrawn = RepositoryViewEntry { rsync_uri: "rsync://example.test/repo/withdrawn.cer".to_string(), current_hash: Some(sha256_hex(b"withdrawn")), repository_source: Some("https://rrdp.example.test/notification.xml".to_string()), object_type: Some("cer".to_string()), state: RepositoryViewState::Withdrawn, }; withdrawn .validate_internal() .expect("withdrawn repository view validates"); let raw = RawByHashEntry::from_bytes(sha256_hex(b"helper-bytes"), b"helper-bytes".to_vec()); raw.validate_internal() .expect("raw_by_hash helper validates"); let empty_raw = RawByHashEntry { sha256_hex: sha256_hex(b"x"), bytes: Vec::new(), origin_uris: Vec::new(), object_type: None, encoding: None, }; let err = empty_raw .validate_internal() .expect_err("empty raw bytes must fail"); assert!(err.to_string().contains("bytes must not be empty")); let duplicate_origin_raw = RawByHashEntry { sha256_hex: sha256_hex(b"dup-origin"), bytes: b"dup-origin".to_vec(), origin_uris: vec![ "rsync://example.test/repo/object.cer".to_string(), "rsync://example.test/repo/object.cer".to_string(), ], object_type: Some("cer".to_string()), encoding: Some("der".to_string()), }; let err = duplicate_origin_raw .validate_internal() .expect_err("duplicate origin URI must fail"); assert!(err.to_string().contains("duplicate origin URI")); } #[test] fn rrdp_source_optional_fields_and_owner_without_hash_validate() { let source = RrdpSourceRecord { notify_uri: "https://rrdp.example.test/notification.xml".to_string(), last_session_id: None, last_serial: None, first_seen_at: pack_time(0), last_seen_at: pack_time(1), last_sync_at: None, sync_state: RrdpSourceSyncState::Empty, last_snapshot_uri: None, last_snapshot_hash: None, last_error: Some("network timeout".to_string()), }; source .validate_internal() .expect("source with optional fields validates"); let owner = RrdpUriOwnerRecord { rsync_uri: "rsync://example.test/repo/object.cer".to_string(), notify_uri: "https://rrdp.example.test/notification.xml".to_string(), current_hash: None, last_confirmed_session_id: "session-1".to_string(), last_confirmed_serial: 5, last_changed_at: pack_time(5), owner_state: RrdpUriOwnerState::Withdrawn, }; owner .validate_internal() .expect("owner without hash validates when withdrawn"); } #[test] fn rrdp_source_binding_records_roundtrip_and_prefix_iteration() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let notify_uri = "https://rrdp.example.test/notification.xml"; let source = sample_rrdp_source_record(notify_uri); store .put_rrdp_source_record(&source) .expect("put rrdp source record"); let got_source = store .get_rrdp_source_record(notify_uri) .expect("get rrdp source record") .expect("rrdp source exists"); assert_eq!(got_source, source); let member1 = sample_rrdp_source_member_record(notify_uri, "rsync://example.test/repo/a.cer", 1); let member2 = sample_rrdp_source_member_record(notify_uri, "rsync://example.test/repo/b.roa", 2); let other_member = sample_rrdp_source_member_record( "https://other.example.test/notification.xml", "rsync://other.example.test/repo/c.cer", 3, ); store .put_rrdp_source_member_record(&member1) .expect("put member1"); store .put_rrdp_source_member_record(&member2) .expect("put member2"); store .put_rrdp_source_member_record(&other_member) .expect("put other member"); let mut members = store .list_rrdp_source_member_records(notify_uri) .expect("list rrdp source members"); members.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri)); assert_eq!(members, vec![member1.clone(), member2.clone()]); let got_member = store .get_rrdp_source_member_record(notify_uri, &member1.rsync_uri) .expect("get member1") .expect("member1 exists"); assert_eq!(got_member, member1); let owner = sample_rrdp_uri_owner_record(notify_uri, &member1.rsync_uri); store .put_rrdp_uri_owner_record(&owner) .expect("put uri owner record"); let got_owner = store .get_rrdp_uri_owner_record(&member1.rsync_uri) .expect("get uri owner record") .expect("uri owner exists"); assert_eq!(got_owner, owner); store .delete_rrdp_uri_owner_record(&member1.rsync_uri) .expect("delete uri owner record"); assert!( store .get_rrdp_uri_owner_record(&member1.rsync_uri) .expect("get deleted uri owner") .is_none() ); let mut invalid_source = sample_rrdp_source_record("https://invalid.example/notification.xml"); invalid_source.last_snapshot_hash = Some("bad".to_string()); let err = store .put_rrdp_source_record(&invalid_source) .expect_err("invalid source hash must fail"); assert!(err.to_string().contains("last_snapshot_hash")); let invalid_member = RrdpSourceMemberRecord { notify_uri: notify_uri.to_string(), rsync_uri: "rsync://example.test/repo/deleted.cer".to_string(), current_hash: None, object_type: None, present: true, last_confirmed_session_id: "session-1".to_string(), last_confirmed_serial: 10, last_changed_at: pack_time(10), }; let err = store .put_rrdp_source_member_record(&invalid_member) .expect_err("present member without hash must fail"); assert!(err.to_string().contains("current_hash is required")); } #[test] fn projection_batch_roundtrip_writes_repository_view_member_and_owner_records() { let dir = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(dir.path()).expect("open store"); let view = RepositoryViewEntry { rsync_uri: "rsync://example.test/repo/a.roa".to_string(), current_hash: Some(hex::encode([1u8; 32])), repository_source: Some("https://example.test/notify.xml".to_string()), object_type: Some("roa".to_string()), state: RepositoryViewState::Present, }; let member = RrdpSourceMemberRecord { notify_uri: "https://example.test/notify.xml".to_string(), rsync_uri: "rsync://example.test/repo/a.roa".to_string(), current_hash: Some(hex::encode([1u8; 32])), object_type: Some("roa".to_string()), present: true, last_confirmed_session_id: "session-1".to_string(), last_confirmed_serial: 7, last_changed_at: pack_time(1), }; let owner = RrdpUriOwnerRecord { rsync_uri: "rsync://example.test/repo/a.roa".to_string(), notify_uri: "https://example.test/notify.xml".to_string(), current_hash: Some(hex::encode([1u8; 32])), last_confirmed_session_id: "session-1".to_string(), last_confirmed_serial: 7, last_changed_at: pack_time(1), owner_state: RrdpUriOwnerState::Active, }; store .put_projection_batch(&[view.clone()], &[member.clone()], &[owner.clone()]) .expect("write projection batch"); assert_eq!( store .get_repository_view_entry(&view.rsync_uri) .expect("get view") .expect("present view"), view ); assert_eq!( store .get_rrdp_source_member_record(&member.notify_uri, &member.rsync_uri) .expect("get member") .expect("present member"), member ); assert_eq!( store .get_rrdp_uri_owner_record(&owner.rsync_uri) .expect("get owner") .expect("present owner"), owner ); } #[test] fn current_rrdp_source_member_helpers_filter_present_records() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let notify_uri = "https://rrdp.example.test/notification.xml"; let mut present_a = sample_rrdp_source_member_record(notify_uri, "rsync://example.test/repo/a.cer", 1); let mut withdrawn_b = sample_rrdp_source_member_record(notify_uri, "rsync://example.test/repo/b.roa", 2); withdrawn_b.present = false; let present_c = sample_rrdp_source_member_record(notify_uri, "rsync://example.test/repo/c.crl", 3); let other_source = sample_rrdp_source_member_record( "https://other.example.test/notification.xml", "rsync://other.example.test/repo/x.cer", 4, ); present_a.last_confirmed_serial = 10; store .put_rrdp_source_member_record(&present_a) .expect("put present a"); store .put_rrdp_source_member_record(&withdrawn_b) .expect("put withdrawn b"); store .put_rrdp_source_member_record(&present_c) .expect("put present c"); store .put_rrdp_source_member_record(&other_source) .expect("put other source"); let members = store .list_current_rrdp_source_members(notify_uri) .expect("list current members"); assert_eq!( members .iter() .map(|record| record.rsync_uri.as_str()) .collect::>(), vec![ "rsync://example.test/repo/a.cer", "rsync://example.test/repo/c.crl", ] ); assert!( store .is_current_rrdp_source_member(notify_uri, &present_a.rsync_uri) .expect("current a") ); assert!( !store .is_current_rrdp_source_member(notify_uri, &withdrawn_b.rsync_uri) .expect("withdrawn b") ); assert!( !store .is_current_rrdp_source_member(notify_uri, &other_source.rsync_uri) .expect("other source") ); } #[test] fn load_current_object_bytes_by_uri_uses_repository_view_and_raw_by_hash() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let present_bytes = b"present-object".to_vec(); let present_hash = sha256_hex(&present_bytes); let mut present_raw = RawByHashEntry::from_bytes(present_hash.clone(), present_bytes.clone()); present_raw .origin_uris .push("rsync://example.test/repo/present.roa".to_string()); present_raw.object_type = Some("roa".to_string()); store .put_raw_by_hash_entry(&present_raw) .expect("put present raw"); store .put_repository_view_entry(&RepositoryViewEntry { rsync_uri: "rsync://example.test/repo/present.roa".to_string(), current_hash: Some(present_hash), repository_source: Some("https://rrdp.example.test/notification.xml".to_string()), object_type: Some("roa".to_string()), state: RepositoryViewState::Present, }) .expect("put present view"); let replaced_bytes = b"replaced-object".to_vec(); let replaced_hash = sha256_hex(&replaced_bytes); let mut replaced_raw = RawByHashEntry::from_bytes(replaced_hash.clone(), replaced_bytes.clone()); replaced_raw .origin_uris .push("rsync://example.test/repo/replaced.cer".to_string()); replaced_raw.object_type = Some("cer".to_string()); store .put_raw_by_hash_entry(&replaced_raw) .expect("put replaced raw"); store .put_repository_view_entry(&RepositoryViewEntry { rsync_uri: "rsync://example.test/repo/replaced.cer".to_string(), current_hash: Some(replaced_hash), repository_source: Some("https://rrdp.example.test/notification.xml".to_string()), object_type: Some("cer".to_string()), state: RepositoryViewState::Replaced, }) .expect("put replaced view"); store .put_repository_view_entry(&RepositoryViewEntry { rsync_uri: "rsync://example.test/repo/withdrawn.crl".to_string(), current_hash: Some(sha256_hex(b"withdrawn")), repository_source: Some("https://rrdp.example.test/notification.xml".to_string()), object_type: Some("crl".to_string()), state: RepositoryViewState::Withdrawn, }) .expect("put withdrawn view"); assert_eq!( store .load_current_object_bytes_by_uri("rsync://example.test/repo/present.roa") .expect("load present"), Some(present_bytes) ); assert_eq!( store .load_current_object_bytes_by_uri("rsync://example.test/repo/replaced.cer") .expect("load replaced"), Some(replaced_bytes) ); assert_eq!( store .load_current_object_bytes_by_uri("rsync://example.test/repo/withdrawn.crl") .expect("load withdrawn"), None ); assert_eq!( store .load_current_object_bytes_by_uri("rsync://example.test/repo/missing.roa") .expect("load missing"), None ); } #[test] fn load_current_object_bytes_by_uri_errors_when_raw_by_hash_is_missing() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let rsync_uri = "rsync://example.test/repo/missing.cer"; store .put_repository_view_entry(&RepositoryViewEntry { rsync_uri: rsync_uri.to_string(), current_hash: Some(hex::encode([0x11; 32])), repository_source: Some("https://rrdp.example.test/notification.xml".to_string()), object_type: Some("cer".to_string()), state: RepositoryViewState::Present, }) .expect("put view"); let err = store .load_current_object_bytes_by_uri(rsync_uri) .expect_err("missing raw_by_hash should error"); assert!(matches!(err, StorageError::InvalidData { .. })); } #[test] fn load_current_object_with_hash_by_uri_returns_hash_and_bytes() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let rsync_uri = "rsync://example.test/repo/present.roa"; let bytes = b"present-object".to_vec(); let hash = sha256_hex(&bytes); let mut raw = RawByHashEntry::from_bytes(hash.clone(), bytes.clone()); raw.origin_uris.push(rsync_uri.to_string()); raw.object_type = Some("roa".to_string()); store.put_raw_by_hash_entry(&raw).expect("put raw"); store .put_repository_view_entry(&RepositoryViewEntry { rsync_uri: rsync_uri.to_string(), current_hash: Some(hash.clone()), repository_source: Some("https://rrdp.example.test/notification.xml".to_string()), object_type: Some("roa".to_string()), state: RepositoryViewState::Present, }) .expect("put view"); let got = store .load_current_object_with_hash_by_uri(rsync_uri) .expect("load current object") .expect("current object exists"); assert_eq!(got.current_hash_hex, hash); assert_eq!(got.current_hash, compute_sha256_32(&bytes)); assert_eq!(got.bytes, bytes); } #[test] fn load_current_object_with_hash_by_uri_uses_internal_blob_cf_without_raw_entry() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let rsync_uri = "rsync://example.test/repo/blob-only.roa"; let bytes = b"blob-only-current-object".to_vec(); let hash = sha256_hex(&bytes); store .put_blob_bytes_batch(&[(hash.clone(), bytes.clone())]) .expect("put blob bytes"); store .put_repository_view_entry(&RepositoryViewEntry { rsync_uri: rsync_uri.to_string(), current_hash: Some(hash.clone()), repository_source: Some("https://rrdp.example.test/notification.xml".to_string()), object_type: Some("roa".to_string()), state: RepositoryViewState::Present, }) .expect("put view"); let got = store .load_current_object_with_hash_by_uri(rsync_uri) .expect("load current object") .expect("current object exists"); assert_eq!(got.current_hash_hex, hash); assert_eq!(got.current_hash, compute_sha256_32(&bytes)); assert_eq!(got.bytes, bytes); assert!( store .get_raw_by_hash_entry(&got.current_hash_hex) .expect("get raw entry") .is_none() ); } #[test] fn load_current_object_bytes_by_uri_uses_internal_blob_cf_without_raw_entry() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let rsync_uri = "rsync://example.test/repo/blob-only-bytes.roa"; let bytes = b"blob-only-current-object-bytes".to_vec(); let hash = sha256_hex(&bytes); store .put_blob_bytes_batch(&[(hash, bytes.clone())]) .expect("put blob bytes"); store .put_repository_view_entry(&RepositoryViewEntry { rsync_uri: rsync_uri.to_string(), current_hash: Some(sha256_hex(&bytes)), repository_source: Some("https://rrdp.example.test/notification.xml".to_string()), object_type: Some("roa".to_string()), state: RepositoryViewState::Present, }) .expect("put view"); assert_eq!( store .load_current_object_bytes_by_uri(rsync_uri) .expect("load current object bytes"), Some(bytes) ); } #[test] fn pack_file_can_lazy_load_bytes_from_external_raw_store() { let td = tempfile::tempdir().expect("tempdir"); let raw_store = std::sync::Arc::new( ExternalRawStoreDb::open(td.path().join("raw-store.db")).expect("open raw store"), ); let bytes = b"lazy-pack-file".to_vec(); let sha256_hex = sha256_hex(&bytes); raw_store .put_raw_entry(&RawByHashEntry::from_bytes( sha256_hex.clone(), bytes.clone(), )) .expect("put raw entry"); let file = PackFile::from_lazy_external_raw_store( "rsync://example.test/repo/a.roa", sha256_hex, compute_sha256_32(&bytes), raw_store, ); assert_eq!(file.bytes().expect("lazy bytes"), bytes.as_slice()); assert_eq!(file.bytes_cloned().expect("cloned bytes"), bytes); } #[test] fn pack_file_can_lazy_load_bytes_from_external_repo_bytes_store() { let td = tempfile::tempdir().expect("tempdir"); let repo_bytes_store = std::sync::Arc::new( ExternalRepoBytesDb::open(td.path().join("repo-bytes.db")).expect("open repo bytes"), ); let bytes = b"repo-object-pack-file".to_vec(); let sha256_hex = sha256_hex(&bytes); repo_bytes_store .put_blob_bytes_batch(&[(sha256_hex.clone(), bytes.clone())]) .expect("put repo bytes"); let file = PackFile::from_lazy_repo_bytes( "rsync://example.test/repo/a.roa", sha256_hex, compute_sha256_32(&bytes), repo_bytes_store, ); assert_eq!(file.bytes().expect("lazy repo bytes"), bytes.as_slice()); assert_eq!(file.bytes_cloned().expect("cloned repo bytes"), bytes); assert_eq!(file.compute_sha256().expect("compute sha256"), file.sha256); }