2246 lines
80 KiB
Rust
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(¤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<_>>(),
|
|
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);
|
|
}
|