rpki/src/storage/tests.rs

2246 lines
80 KiB
Rust

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::<u64>()
);
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<u8>) -> 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<Vec<u8>>,
) -> 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(&current)
.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(&current.manifest_rsync_uri)
.expect("get replaced vcir")
.expect("vcir exists");
assert_eq!(got, current);
let replay_meta = store
.get_manifest_replay_meta(&current.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(&current.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<_>>(),
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);
}