607 lines
20 KiB
Rust
607 lines
20 KiB
Rust
//! Manifest processing regression tests.
|
|
use super::*;
|
|
use crate::repository::current_repo_index::CurrentRepoIndex;
|
|
use crate::repository::storage::{RawByHashEntry, RocksStore};
|
|
use std::path::PathBuf;
|
|
|
|
fn fixture_repository() -> PathBuf {
|
|
crate::test_support::synthetic_repository().case_repository("baseline-v1")
|
|
}
|
|
|
|
fn manifest_fixture_path() -> PathBuf {
|
|
fixture_repository().join("child/child.mft")
|
|
}
|
|
|
|
fn issuer_ca_fixture_der() -> Vec<u8> {
|
|
std::fs::read(fixture_repository().join("root/child.cer")).expect("read synthetic issuer CA")
|
|
}
|
|
|
|
fn issuer_ca_rsync_uri() -> &'static str {
|
|
"rsync://rpki-local-rsync:873/custom/root/child.cer"
|
|
}
|
|
|
|
fn load_manifest_fixture() -> (
|
|
ManifestObject,
|
|
Vec<u8>,
|
|
String,
|
|
String,
|
|
time::OffsetDateTime,
|
|
) {
|
|
let manifest_path = manifest_fixture_path();
|
|
let manifest_bytes = std::fs::read(&manifest_path).expect("read synthetic manifest");
|
|
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode synthetic manifest");
|
|
let manifest_rsync_uri = "rsync://rpki-local-rsync:873/custom/child/child.mft".to_string();
|
|
let publication_point_rsync_uri = "rsync://rpki-local-rsync:873/custom/child/".to_string();
|
|
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
|
(
|
|
manifest,
|
|
manifest_bytes,
|
|
manifest_rsync_uri,
|
|
publication_point_rsync_uri,
|
|
validation_time,
|
|
)
|
|
}
|
|
|
|
fn raw_by_hash_entry(uri: &str, bytes: Vec<u8>, object_type: &str) -> RawByHashEntry {
|
|
let mut entry = RawByHashEntry::from_bytes(hex::encode(sha2::Sha256::digest(&bytes)), bytes);
|
|
entry.origin_uris.push(uri.to_string());
|
|
entry.object_type = Some(object_type.to_string());
|
|
entry.encoding = Some("der".to_string());
|
|
entry
|
|
}
|
|
|
|
fn put_current_object(store: &RocksStore, rsync_uri: &str, bytes: Vec<u8>, object_type: &str) {
|
|
let hash = hex::encode(sha2::Sha256::digest(&bytes));
|
|
store
|
|
.put_raw_by_hash_entry(&raw_by_hash_entry(rsync_uri, bytes, object_type))
|
|
.expect("put raw_by_hash entry");
|
|
store
|
|
.put_repository_view_entry(&crate::repository::storage::RepositoryViewEntry {
|
|
rsync_uri: rsync_uri.to_string(),
|
|
current_hash: Some(hash),
|
|
repository_source: Some("https://example.test/notification.xml".to_string()),
|
|
object_type: Some(object_type.to_string()),
|
|
state: crate::repository::storage::RepositoryViewState::Present,
|
|
})
|
|
.expect("put repository view entry");
|
|
}
|
|
|
|
fn put_complete_publication_point_current_objects(
|
|
store: &RocksStore,
|
|
manifest: &ManifestObject,
|
|
manifest_rsync_uri: &str,
|
|
manifest_bytes: Vec<u8>,
|
|
publication_point_rsync_uri: &str,
|
|
) {
|
|
put_current_object(store, manifest_rsync_uri, manifest_bytes, "mft");
|
|
for entry in manifest.manifest.parse_files().expect("parse files") {
|
|
let file_path = manifest_fixture_path()
|
|
.parent()
|
|
.unwrap()
|
|
.join(entry.file_name.as_str());
|
|
let bytes = std::fs::read(&file_path).expect("read fixture file");
|
|
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
|
let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin");
|
|
put_current_object(store, &rsync_uri, bytes, object_type);
|
|
}
|
|
}
|
|
|
|
fn put_raw_only(store: &RocksStore, rsync_uri: &str, bytes: Vec<u8>, object_type: &str) {
|
|
store
|
|
.put_raw_by_hash_entry(&raw_by_hash_entry(rsync_uri, bytes, object_type))
|
|
.expect("put raw_by_hash entry");
|
|
}
|
|
|
|
fn locked_files_for_manifest(
|
|
manifest: &ManifestObject,
|
|
publication_point_rsync_uri: &str,
|
|
) -> Vec<PackFile> {
|
|
let manifest_path = manifest_fixture_path();
|
|
manifest
|
|
.manifest
|
|
.parse_files()
|
|
.expect("parse files")
|
|
.into_iter()
|
|
.map(|entry| {
|
|
let file_path = manifest_path
|
|
.parent()
|
|
.unwrap()
|
|
.join(entry.file_name.as_str());
|
|
let bytes = std::fs::read(&file_path).unwrap_or_else(|_| {
|
|
panic!("read fixture file referenced by manifest: {file_path:?}")
|
|
});
|
|
PackFile::from_bytes_compute_sha256(
|
|
format!("{publication_point_rsync_uri}{}", entry.file_name),
|
|
bytes,
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn try_build_fresh_publication_point_rejects_manifest_outside_publication_point() {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
|
let (_, _, manifest_rsync_uri, _, validation_time) = load_manifest_fixture();
|
|
let err = try_build_fresh_publication_point(
|
|
&store,
|
|
&manifest_rsync_uri,
|
|
"rsync://example.test/other/",
|
|
&issuer_ca_fixture_der(),
|
|
Some(issuer_ca_rsync_uri()),
|
|
validation_time,
|
|
)
|
|
.unwrap_err();
|
|
assert!(
|
|
matches!(
|
|
err,
|
|
ManifestFreshError::ManifestOutsidePublicationPoint { .. }
|
|
),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn try_build_fresh_publication_point_reports_missing_manifest_when_raw_store_is_empty() {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
|
let (_, _, manifest_rsync_uri, publication_point_rsync_uri, validation_time) =
|
|
load_manifest_fixture();
|
|
let err = try_build_fresh_publication_point(
|
|
&store,
|
|
&manifest_rsync_uri,
|
|
&publication_point_rsync_uri,
|
|
&issuer_ca_fixture_der(),
|
|
Some(issuer_ca_rsync_uri()),
|
|
validation_time,
|
|
)
|
|
.unwrap_err();
|
|
assert!(
|
|
matches!(err, ManifestFreshError::MissingManifest { .. }),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn try_build_fresh_publication_point_reports_missing_locked_file() {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
|
let (
|
|
manifest,
|
|
manifest_bytes,
|
|
manifest_rsync_uri,
|
|
publication_point_rsync_uri,
|
|
validation_time,
|
|
) = load_manifest_fixture();
|
|
put_current_object(&store, &manifest_rsync_uri, manifest_bytes, "mft");
|
|
let first_non_crl = manifest
|
|
.manifest
|
|
.parse_files()
|
|
.expect("parse files")
|
|
.into_iter()
|
|
.find(|entry| !entry.file_name.ends_with(".crl"))
|
|
.expect("fixture non-crl entry");
|
|
let file_path = manifest_fixture_path()
|
|
.parent()
|
|
.unwrap()
|
|
.join(first_non_crl.file_name.as_str());
|
|
let bytes = std::fs::read(&file_path).expect("read fixture file");
|
|
let rsync_uri = format!("{publication_point_rsync_uri}{}", first_non_crl.file_name);
|
|
let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin");
|
|
put_current_object(&store, &rsync_uri, bytes, object_type);
|
|
|
|
let err = try_build_fresh_publication_point(
|
|
&store,
|
|
&manifest_rsync_uri,
|
|
&publication_point_rsync_uri,
|
|
&issuer_ca_fixture_der(),
|
|
Some(issuer_ca_rsync_uri()),
|
|
validation_time,
|
|
)
|
|
.unwrap_err();
|
|
assert!(
|
|
matches!(err, ManifestFreshError::MissingFile { .. }),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn try_build_fresh_publication_point_detects_hash_mismatch_via_repository_view_hash() {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
|
let (
|
|
manifest,
|
|
manifest_bytes,
|
|
manifest_rsync_uri,
|
|
publication_point_rsync_uri,
|
|
validation_time,
|
|
) = load_manifest_fixture();
|
|
put_current_object(&store, &manifest_rsync_uri, manifest_bytes, "mft");
|
|
|
|
let entries = manifest
|
|
.manifest
|
|
.parse_files()
|
|
.expect("parse files")
|
|
.into_iter()
|
|
.collect::<Vec<_>>();
|
|
let first = entries
|
|
.iter()
|
|
.find(|entry| !entry.file_name.ends_with(".crl"))
|
|
.expect("synthetic manifest non-CRL entry");
|
|
|
|
let first_uri = format!("{publication_point_rsync_uri}{}", first.file_name);
|
|
let object_type = first_uri.rsplit('.').next().unwrap_or("bin");
|
|
put_current_object(
|
|
&store,
|
|
&first_uri,
|
|
b"wrong object bytes".to_vec(),
|
|
object_type,
|
|
);
|
|
|
|
for entry in entries
|
|
.iter()
|
|
.filter(|entry| entry.file_name != first.file_name)
|
|
{
|
|
let file_path = manifest_fixture_path()
|
|
.parent()
|
|
.unwrap()
|
|
.join(entry.file_name.as_str());
|
|
let bytes = std::fs::read(&file_path).expect("read fixture file");
|
|
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
|
let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin");
|
|
put_current_object(&store, &rsync_uri, bytes, object_type);
|
|
}
|
|
|
|
let err = try_build_fresh_publication_point(
|
|
&store,
|
|
&manifest_rsync_uri,
|
|
&publication_point_rsync_uri,
|
|
&issuer_ca_fixture_der(),
|
|
Some(issuer_ca_rsync_uri()),
|
|
validation_time,
|
|
)
|
|
.unwrap_err();
|
|
assert!(
|
|
matches!(err, ManifestFreshError::HashMismatch { .. }),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn try_build_fresh_publication_point_uses_current_repo_index_without_repository_view() {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
|
let (
|
|
manifest,
|
|
manifest_bytes,
|
|
manifest_rsync_uri,
|
|
publication_point_rsync_uri,
|
|
validation_time,
|
|
) = load_manifest_fixture();
|
|
|
|
put_raw_only(&store, &manifest_rsync_uri, manifest_bytes.clone(), "mft");
|
|
let current_index = CurrentRepoIndex::shared();
|
|
let mut entries = vec![crate::repository::storage::RepositoryViewEntry {
|
|
rsync_uri: manifest_rsync_uri.clone(),
|
|
current_hash: Some(hex::encode(sha2::Sha256::digest(&manifest_bytes))),
|
|
repository_source: Some("https://example.test/notification.xml".to_string()),
|
|
object_type: Some("mft".to_string()),
|
|
state: crate::repository::storage::RepositoryViewState::Present,
|
|
}];
|
|
|
|
for entry in manifest.manifest.parse_files().expect("parse files") {
|
|
let file_path = manifest_fixture_path()
|
|
.parent()
|
|
.unwrap()
|
|
.join(entry.file_name.as_str());
|
|
let bytes = std::fs::read(&file_path).expect("read fixture file");
|
|
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
|
let object_type = rsync_uri.rsplit('.').next().unwrap_or("bin").to_string();
|
|
put_raw_only(&store, &rsync_uri, bytes.clone(), &object_type);
|
|
entries.push(crate::repository::storage::RepositoryViewEntry {
|
|
rsync_uri,
|
|
current_hash: Some(hex::encode(sha2::Sha256::digest(&bytes))),
|
|
repository_source: Some("https://example.test/notification.xml".to_string()),
|
|
object_type: Some(object_type),
|
|
state: crate::repository::storage::RepositoryViewState::Present,
|
|
});
|
|
}
|
|
|
|
current_index
|
|
.write()
|
|
.expect("index write lock")
|
|
.apply_repository_view_entries(&entries)
|
|
.expect("apply current index");
|
|
|
|
assert!(
|
|
store
|
|
.get_repository_view_entry(&manifest_rsync_uri)
|
|
.expect("get repository view")
|
|
.is_none()
|
|
);
|
|
|
|
let (fresh, _timing) = try_build_fresh_publication_point_with_timing(
|
|
&store,
|
|
&manifest_rsync_uri,
|
|
&publication_point_rsync_uri,
|
|
Some(¤t_index),
|
|
&issuer_ca_fixture_der(),
|
|
Some(issuer_ca_rsync_uri()),
|
|
validation_time,
|
|
)
|
|
.expect("fresh publication point via current index");
|
|
|
|
assert_eq!(fresh.manifest_rsync_uri, manifest_rsync_uri);
|
|
assert_eq!(fresh.files.len(), manifest.manifest.file_count());
|
|
}
|
|
|
|
#[test]
|
|
fn try_build_fresh_publication_point_records_anti_rollback_meta_miss() {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
|
let (
|
|
manifest,
|
|
manifest_bytes,
|
|
manifest_rsync_uri,
|
|
publication_point_rsync_uri,
|
|
validation_time,
|
|
) = load_manifest_fixture();
|
|
put_complete_publication_point_current_objects(
|
|
&store,
|
|
&manifest,
|
|
&manifest_rsync_uri,
|
|
manifest_bytes,
|
|
&publication_point_rsync_uri,
|
|
);
|
|
|
|
let (_fresh, timing) = try_build_fresh_publication_point_with_timing(
|
|
&store,
|
|
&manifest_rsync_uri,
|
|
&publication_point_rsync_uri,
|
|
None,
|
|
&issuer_ca_fixture_der(),
|
|
Some(issuer_ca_rsync_uri()),
|
|
validation_time,
|
|
)
|
|
.expect("fresh publication point without freshness metadata");
|
|
|
|
assert!(timing.anti_rollback_meta_miss);
|
|
assert!(!timing.anti_rollback_meta_hit);
|
|
}
|
|
|
|
#[test]
|
|
fn try_build_fresh_publication_point_uses_anti_rollback_meta_hit_for_same_manifest() {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
|
let (
|
|
manifest,
|
|
manifest_bytes,
|
|
manifest_rsync_uri,
|
|
publication_point_rsync_uri,
|
|
validation_time,
|
|
) = load_manifest_fixture();
|
|
let previous_meta = sample_manifest_meta(
|
|
&manifest,
|
|
&manifest_rsync_uri,
|
|
&publication_point_rsync_uri,
|
|
&manifest_bytes,
|
|
validation_time,
|
|
);
|
|
store
|
|
.put_manifest_anti_rollback_meta(&previous_meta)
|
|
.expect("persist freshness");
|
|
put_complete_publication_point_current_objects(
|
|
&store,
|
|
&manifest,
|
|
&manifest_rsync_uri,
|
|
manifest_bytes,
|
|
&publication_point_rsync_uri,
|
|
);
|
|
|
|
let (_fresh, timing) = try_build_fresh_publication_point_with_timing(
|
|
&store,
|
|
&manifest_rsync_uri,
|
|
&publication_point_rsync_uri,
|
|
None,
|
|
&issuer_ca_fixture_der(),
|
|
Some(issuer_ca_rsync_uri()),
|
|
validation_time,
|
|
)
|
|
.expect("fresh publication point with matching freshness metadata");
|
|
|
|
assert!(timing.anti_rollback_meta_hit);
|
|
assert!(!timing.anti_rollback_meta_miss);
|
|
}
|
|
|
|
#[test]
|
|
fn validate_manifest_embedded_ee_cert_path_rejects_missing_crl_files() {
|
|
let (manifest, _, _, publication_point_rsync_uri, validation_time) = load_manifest_fixture();
|
|
let files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri)
|
|
.into_iter()
|
|
.filter(|f| !f.rsync_uri.ends_with(".crl"))
|
|
.collect::<Vec<_>>();
|
|
|
|
let err = validate_manifest_embedded_ee_cert_path(
|
|
&manifest,
|
|
&files,
|
|
&issuer_ca_fixture_der(),
|
|
Some(issuer_ca_rsync_uri()),
|
|
validation_time,
|
|
)
|
|
.unwrap_err();
|
|
assert!(matches!(err, ManifestFreshError::NoCrlFiles), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn validate_manifest_embedded_ee_cert_path_rejects_missing_ee_crldp() {
|
|
let (mut manifest, _, _, publication_point_rsync_uri, validation_time) =
|
|
load_manifest_fixture();
|
|
manifest.signed_object.signed_data.certificates[0]
|
|
.resource_cert
|
|
.tbs
|
|
.extensions
|
|
.crl_distribution_points_uris = None;
|
|
let files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri);
|
|
|
|
let err = validate_manifest_embedded_ee_cert_path(
|
|
&manifest,
|
|
&files,
|
|
&issuer_ca_fixture_der(),
|
|
Some(issuer_ca_rsync_uri()),
|
|
validation_time,
|
|
)
|
|
.unwrap_err();
|
|
assert!(matches!(err, ManifestFreshError::EeCrlDpMissing), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn validate_manifest_embedded_ee_cert_path_rejects_unlisted_crldp_uri() {
|
|
let (manifest, _, _, publication_point_rsync_uri, validation_time) = load_manifest_fixture();
|
|
let mut files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri)
|
|
.into_iter()
|
|
.filter(|f| !f.rsync_uri.ends_with(".crl"))
|
|
.collect::<Vec<_>>();
|
|
files.push(PackFile::from_bytes_compute_sha256(
|
|
"rsync://example.test/repo/unrelated.crl",
|
|
b"dummy".to_vec(),
|
|
));
|
|
|
|
let err = validate_manifest_embedded_ee_cert_path(
|
|
&manifest,
|
|
&files,
|
|
&issuer_ca_fixture_der(),
|
|
Some(issuer_ca_rsync_uri()),
|
|
validation_time,
|
|
)
|
|
.unwrap_err();
|
|
assert!(matches!(err, ManifestFreshError::EeCrlNotFound(_)), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn validate_manifest_embedded_ee_cert_path_rejects_expired_crl() {
|
|
let (manifest, _, _, publication_point_rsync_uri, _) = load_manifest_fixture();
|
|
let files = locked_files_for_manifest(&manifest, &publication_point_rsync_uri);
|
|
let ee = &manifest.signed_object.signed_data.certificates[0];
|
|
let crldp_uri = ee
|
|
.resource_cert
|
|
.tbs
|
|
.extensions
|
|
.crl_distribution_points_uris
|
|
.as_ref()
|
|
.and_then(|uris| uris.first())
|
|
.expect("fixture manifest EE CRLDP")
|
|
.as_str()
|
|
.to_string();
|
|
let crl_file = files
|
|
.iter()
|
|
.find(|file| file.rsync_uri == crldp_uri)
|
|
.expect("fixture CRL referenced by manifest EE");
|
|
let crl =
|
|
crate::model::crl::RpkixCrl::decode_der(crl_file.bytes().expect("read fixture crl bytes"))
|
|
.expect("decode fixture crl");
|
|
let validation_time = crl.next_update.utc;
|
|
|
|
let err = validate_manifest_embedded_ee_cert_path(
|
|
&manifest,
|
|
&files,
|
|
&issuer_ca_fixture_der(),
|
|
Some(issuer_ca_rsync_uri()),
|
|
validation_time,
|
|
)
|
|
.unwrap_err();
|
|
|
|
assert!(
|
|
matches!(
|
|
err,
|
|
ManifestFreshError::EeCertPath(
|
|
crate::validation::cert_path::CertPathError::CrlNotValidAtTime
|
|
)
|
|
),
|
|
"{err}"
|
|
);
|
|
}
|
|
|
|
fn sample_manifest_meta(
|
|
manifest: &ManifestObject,
|
|
uri: &str,
|
|
_base: &str,
|
|
bytes: &[u8],
|
|
now: time::OffsetDateTime,
|
|
) -> crate::repository::storage::ManifestAntiRollbackMeta {
|
|
crate::repository::storage::ManifestAntiRollbackMeta {
|
|
manifest_rsync_uri: uri.into(),
|
|
manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
|
|
manifest_this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update),
|
|
manifest_sha256: sha2::Sha256::digest(bytes).to_vec(),
|
|
updated_at_validation_time: PackTime::from_utc_offset_datetime(now),
|
|
}
|
|
}
|
|
#[test]
|
|
fn history_survives_restart_and_rejects_rollback_without_advancing() {
|
|
for case in 0..4 {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let (manifest, bytes, uri, base, now) = load_manifest_fixture();
|
|
let mut meta = sample_manifest_meta(&manifest, &uri, &base, &bytes, now);
|
|
match case {
|
|
0 => {}
|
|
1 => {
|
|
meta.manifest_number_be = vec![0x7f; 20];
|
|
}
|
|
2 => {
|
|
meta.manifest_sha256 = vec![0x55; 32];
|
|
}
|
|
3 => {
|
|
meta.manifest_number_be = vec![0];
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
{
|
|
let store = RocksStore::open(dir.path()).unwrap();
|
|
store.put_manifest_anti_rollback_meta(&meta).unwrap();
|
|
put_complete_publication_point_current_objects(
|
|
&store,
|
|
&manifest,
|
|
&uri,
|
|
bytes.clone(),
|
|
&base,
|
|
);
|
|
}
|
|
let store = RocksStore::open(dir.path()).unwrap();
|
|
assert_eq!(
|
|
store.get_manifest_anti_rollback_meta(&uri).unwrap(),
|
|
Some(meta.clone())
|
|
);
|
|
let result = try_build_fresh_publication_point_with_timing(
|
|
&store,
|
|
&uri,
|
|
&base,
|
|
None,
|
|
&issuer_ca_fixture_der(),
|
|
Some(issuer_ca_rsync_uri()),
|
|
now,
|
|
);
|
|
match case {
|
|
0 => {
|
|
assert!(result.unwrap().1.anti_rollback_meta_hit);
|
|
}
|
|
1 | 2 => {
|
|
assert!(matches!(
|
|
result,
|
|
Err(ManifestFreshError::ManifestNumberNotIncreasing { .. })
|
|
));
|
|
}
|
|
3 => {
|
|
assert!(matches!(
|
|
result,
|
|
Err(ManifestFreshError::ThisUpdateNotIncreasing { .. })
|
|
));
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
assert_eq!(
|
|
store.get_manifest_anti_rollback_meta(&uri).unwrap(),
|
|
Some(meta)
|
|
);
|
|
}
|
|
}
|