mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-01 17:58:22 +00:00
fix(admin): preserve raw XML in metadata backups (#6936)
This commit is contained in:
@@ -68,6 +68,35 @@ const LOG_COMPONENT_ADMIN: &str = "admin";
|
|||||||
const LOG_SUBSYSTEM_BUCKET_META: &str = "bucket_meta";
|
const LOG_SUBSYSTEM_BUCKET_META: &str = "bucket_meta";
|
||||||
const EVENT_ADMIN_BUCKET_META_STATE: &str = "admin_bucket_meta_state";
|
const EVENT_ADMIN_BUCKET_META_STATE: &str = "admin_bucket_meta_state";
|
||||||
|
|
||||||
|
fn export_internal_error(message: impl Into<String>) -> s3s::S3Error {
|
||||||
|
let message = message.into();
|
||||||
|
s3_error!(InternalError, "{message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checked_raw_xml<T, E, F>(validated: &T, raw: Vec<u8>, parse: F) -> S3Result<Vec<u8>>
|
||||||
|
where
|
||||||
|
T: PartialEq,
|
||||||
|
E: std::fmt::Display,
|
||||||
|
F: FnOnce(&[u8]) -> Result<T, E>,
|
||||||
|
{
|
||||||
|
let selected = parse(&raw)
|
||||||
|
.map_err(|e| export_internal_error(format!("persisted bucket metadata changed to invalid XML during export: {e}")))?;
|
||||||
|
if selected != *validated {
|
||||||
|
return Err(export_internal_error("bucket metadata changed during export"));
|
||||||
|
}
|
||||||
|
Ok(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checked_versioning_xml(validated: &VersioningConfiguration, raw: Vec<u8>) -> S3Result<Vec<u8>> {
|
||||||
|
if raw.is_empty() {
|
||||||
|
if *validated != VersioningConfiguration::default() {
|
||||||
|
return Err(export_internal_error("bucket metadata changed during export"));
|
||||||
|
}
|
||||||
|
return serialize(validated).map_err(|e| export_internal_error(format!("serialize config failed: {e}")));
|
||||||
|
}
|
||||||
|
checked_raw_xml(validated, raw, deserialize::<VersioningConfiguration>)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, serde::Deserialize)]
|
#[derive(Debug, Default, serde::Deserialize)]
|
||||||
pub struct ExportBucketMetadataQuery {
|
pub struct ExportBucketMetadataQuery {
|
||||||
pub bucket: String,
|
pub bucket: String,
|
||||||
@@ -190,8 +219,13 @@ impl Operation for ExportBucketMetadata {
|
|||||||
Ok(None) => continue,
|
Ok(None) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let raw_config = metadata_sys::get(&bucket.name)
|
||||||
|
.await
|
||||||
|
.map_err(|e| export_internal_error(format!("get bucket metadata failed: {e}")))?
|
||||||
|
.notification_config_xml
|
||||||
|
.clone();
|
||||||
let config_xml =
|
let config_xml =
|
||||||
serialize(&config).map_err(|e| s3_error!(InternalError, "serialize config failed: {e}"))?;
|
checked_raw_xml(&config, raw_config, deserialize::<s3s::dto::NotificationConfiguration>)?;
|
||||||
|
|
||||||
zip_writer
|
zip_writer
|
||||||
.start_file(conf_path, SimpleFileOptions::default())
|
.start_file(conf_path, SimpleFileOptions::default())
|
||||||
@@ -210,8 +244,12 @@ impl Operation for ExportBucketMetadata {
|
|||||||
return Err(s3_error!(InternalError, "failed to load bucket metadata: {e}"));
|
return Err(s3_error!(InternalError, "failed to load bucket metadata: {e}"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let config_xml =
|
let raw_config = metadata_sys::get(&bucket.name)
|
||||||
serialize(&config).map_err(|e| s3_error!(InternalError, "failed to serialize config: {e}"))?;
|
.await
|
||||||
|
.map_err(|e| export_internal_error(format!("failed to load bucket metadata: {e}")))?
|
||||||
|
.lifecycle_config_xml
|
||||||
|
.clone();
|
||||||
|
let config_xml = checked_raw_xml(&config, raw_config, deserialize::<BucketLifecycleConfiguration>)?;
|
||||||
|
|
||||||
zip_writer
|
zip_writer
|
||||||
.start_file(conf_path, SimpleFileOptions::default())
|
.start_file(conf_path, SimpleFileOptions::default())
|
||||||
@@ -230,8 +268,12 @@ impl Operation for ExportBucketMetadata {
|
|||||||
return Err(s3_error!(InternalError, "failed to load bucket metadata: {e}"));
|
return Err(s3_error!(InternalError, "failed to load bucket metadata: {e}"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let config_xml =
|
let raw_config = metadata_sys::get(&bucket.name)
|
||||||
serialize(&config).map_err(|e| s3_error!(InternalError, "failed to serialize config: {e}"))?;
|
.await
|
||||||
|
.map_err(|e| export_internal_error(format!("failed to load bucket metadata: {e}")))?
|
||||||
|
.tagging_config_xml
|
||||||
|
.clone();
|
||||||
|
let config_xml = checked_raw_xml(&config, raw_config, deserialize::<Tagging>)?;
|
||||||
|
|
||||||
zip_writer
|
zip_writer
|
||||||
.start_file(conf_path, SimpleFileOptions::default())
|
.start_file(conf_path, SimpleFileOptions::default())
|
||||||
@@ -270,8 +312,12 @@ impl Operation for ExportBucketMetadata {
|
|||||||
return Err(s3_error!(InternalError, "get bucket metadata failed: {e}"));
|
return Err(s3_error!(InternalError, "get bucket metadata failed: {e}"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let config_xml =
|
let raw_config = metadata_sys::get(&bucket.name)
|
||||||
serialize(&config).map_err(|e| s3_error!(InternalError, "serialize config failed: {e}"))?;
|
.await
|
||||||
|
.map_err(|e| export_internal_error(format!("get bucket metadata failed: {e}")))?
|
||||||
|
.object_lock_config_xml
|
||||||
|
.clone();
|
||||||
|
let config_xml = checked_raw_xml(&config, raw_config, deserialize::<ObjectLockConfiguration>)?;
|
||||||
|
|
||||||
zip_writer
|
zip_writer
|
||||||
.start_file(conf_path, SimpleFileOptions::default())
|
.start_file(conf_path, SimpleFileOptions::default())
|
||||||
@@ -290,8 +336,12 @@ impl Operation for ExportBucketMetadata {
|
|||||||
return Err(s3_error!(InternalError, "get bucket metadata failed: {e}"));
|
return Err(s3_error!(InternalError, "get bucket metadata failed: {e}"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let config_xml =
|
let raw_config = metadata_sys::get(&bucket.name)
|
||||||
serialize(&config).map_err(|e| s3_error!(InternalError, "serialize config failed: {e}"))?;
|
.await
|
||||||
|
.map_err(|e| export_internal_error(format!("get bucket metadata failed: {e}")))?
|
||||||
|
.encryption_config_xml
|
||||||
|
.clone();
|
||||||
|
let config_xml = checked_raw_xml(&config, raw_config, deserialize::<ServerSideEncryptionConfiguration>)?;
|
||||||
|
|
||||||
zip_writer
|
zip_writer
|
||||||
.start_file(conf_path, SimpleFileOptions::default())
|
.start_file(conf_path, SimpleFileOptions::default())
|
||||||
@@ -310,8 +360,12 @@ impl Operation for ExportBucketMetadata {
|
|||||||
return Err(s3_error!(InternalError, "get bucket metadata failed: {e}"));
|
return Err(s3_error!(InternalError, "get bucket metadata failed: {e}"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let config_xml =
|
let raw_config = metadata_sys::get(&bucket.name)
|
||||||
serialize(&config).map_err(|e| s3_error!(InternalError, "serialize config failed: {e}"))?;
|
.await
|
||||||
|
.map_err(|e| export_internal_error(format!("get bucket metadata failed: {e}")))?
|
||||||
|
.versioning_config_xml
|
||||||
|
.clone();
|
||||||
|
let config_xml = checked_versioning_xml(&config, raw_config)?;
|
||||||
|
|
||||||
zip_writer
|
zip_writer
|
||||||
.start_file(conf_path, SimpleFileOptions::default())
|
.start_file(conf_path, SimpleFileOptions::default())
|
||||||
@@ -330,8 +384,12 @@ impl Operation for ExportBucketMetadata {
|
|||||||
return Err(s3_error!(InternalError, "get bucket metadata failed: {e}"));
|
return Err(s3_error!(InternalError, "get bucket metadata failed: {e}"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let config_xml =
|
let raw_config = metadata_sys::get(&bucket.name)
|
||||||
serialize(&config).map_err(|e| s3_error!(InternalError, "serialize config failed: {e}"))?;
|
.await
|
||||||
|
.map_err(|e| export_internal_error(format!("get bucket metadata failed: {e}")))?
|
||||||
|
.replication_config_xml
|
||||||
|
.clone();
|
||||||
|
let config_xml = checked_raw_xml(&config, raw_config, deserialize::<ReplicationConfiguration>)?;
|
||||||
|
|
||||||
zip_writer
|
zip_writer
|
||||||
.start_file(conf_path, SimpleFileOptions::default())
|
.start_file(conf_path, SimpleFileOptions::default())
|
||||||
@@ -1275,6 +1333,235 @@ mod import_persist_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod backup_zip_compatibility_tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::admin::runtime_sources::{AppContext, publish_test_app_context};
|
||||||
|
use http::{Extensions, Uri};
|
||||||
|
use http_body_util::BodyExt as _;
|
||||||
|
use rustfs_iam::store::{Store as _, object::IAM_CONFIG_PREFIX};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
const ROOT_ACCESS_KEY: &str = "BUCKETMETABACKUPROOT";
|
||||||
|
const ROOT_SECRET_KEY: &str = "bucketMetaBackupRootSecret123";
|
||||||
|
const BUCKET: &str = "backup-compatibility";
|
||||||
|
const NOTIFICATION_XML: &[u8] = b"<NotificationConfiguration>\n</NotificationConfiguration>";
|
||||||
|
const LIFECYCLE_XML: &[u8] = b"<LifecycleConfiguration>\n<Rule><ID>expire</ID><Status>Enabled</Status><Filter><Prefix>logs/</Prefix></Filter><Expiration><Days>30</Days></Expiration></Rule>\n</LifecycleConfiguration>";
|
||||||
|
const SSE_XML: &[u8] = b"<ServerSideEncryptionConfiguration>\n<Rule><ApplyServerSideEncryptionByDefault><SSEAlgorithm>AES256</SSEAlgorithm></ApplyServerSideEncryptionByDefault></Rule>\n</ServerSideEncryptionConfiguration>";
|
||||||
|
const TAGGING_XML: &[u8] = b"<Tagging>\n<TagSet><Tag><Key>team</Key><Value>storage</Value></Tag></TagSet>\n</Tagging>";
|
||||||
|
const OBJECT_LOCK_XML: &[u8] =
|
||||||
|
b"<ObjectLockConfiguration>\n<ObjectLockEnabled>Enabled</ObjectLockEnabled>\n</ObjectLockConfiguration>";
|
||||||
|
const VERSIONING_XML: &[u8] = b"<VersioningConfiguration>\n<Status>Enabled</Status>\n</VersioningConfiguration>";
|
||||||
|
const OLD_REPLICATION_XML: &[u8] = b"<ReplicationConfiguration><Role>arn:aws:iam::123456789012:role/replication</Role><FutureTopLevel>preserve-me</FutureTopLevel><Rule><Status>Enabled</Status><Priority>1</Priority><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><Filter><Prefix></Prefix></Filter><Destination><Bucket>arn:aws:s3:::backup</Bucket></Destination></Rule></ReplicationConfiguration>";
|
||||||
|
const DIFFERENT_REPLICATION_XML: &[u8] = b"<ReplicationConfiguration><Role>arn:aws:iam::123456789012:role/replication</Role><Rule><Status>Enabled</Status><Priority>2</Priority><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><Filter><Prefix>changed/</Prefix></Filter><Destination><Bucket>arn:aws:s3:::replacement</Bucket></Destination></Rule></ReplicationConfiguration>";
|
||||||
|
|
||||||
|
fn persisted_xml_fixtures() -> [(&'static str, &'static [u8]); 7] {
|
||||||
|
[
|
||||||
|
(BUCKET_NOTIFICATION_CONFIG, NOTIFICATION_XML),
|
||||||
|
(BUCKET_LIFECYCLE_CONFIG, LIFECYCLE_XML),
|
||||||
|
(BUCKET_SSECONFIG, SSE_XML),
|
||||||
|
(BUCKET_TAGGING_CONFIG, TAGGING_XML),
|
||||||
|
(OBJECT_LOCK_CONFIG, OBJECT_LOCK_XML),
|
||||||
|
(BUCKET_VERSIONING_CONFIG, VERSIONING_XML),
|
||||||
|
(BUCKET_REPLICATION_CONFIG, OLD_REPLICATION_XML),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn persisted_xml<'a>(metadata: &'a BucketMetadata, config_file: &str) -> &'a [u8] {
|
||||||
|
match config_file {
|
||||||
|
BUCKET_NOTIFICATION_CONFIG => &metadata.notification_config_xml,
|
||||||
|
BUCKET_LIFECYCLE_CONFIG => &metadata.lifecycle_config_xml,
|
||||||
|
BUCKET_SSECONFIG => &metadata.encryption_config_xml,
|
||||||
|
BUCKET_TAGGING_CONFIG => &metadata.tagging_config_xml,
|
||||||
|
OBJECT_LOCK_CONFIG => &metadata.object_lock_config_xml,
|
||||||
|
BUCKET_VERSIONING_CONFIG => &metadata.versioning_config_xml,
|
||||||
|
BUCKET_REPLICATION_CONFIG => &metadata.replication_config_xml,
|
||||||
|
_ => panic!("unexpected persisted XML config {config_file}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn zip_with_entries(bucket: &str, entries: &[(&str, &[u8])]) -> Vec<u8> {
|
||||||
|
let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||||||
|
for (config_file, payload) in entries {
|
||||||
|
writer
|
||||||
|
.start_file(format!("{bucket}/{config_file}"), SimpleFileOptions::default())
|
||||||
|
.expect("start compatibility archive entry");
|
||||||
|
writer.write_all(payload).expect("write compatibility archive entry");
|
||||||
|
}
|
||||||
|
writer.finish().expect("finish compatibility archive").into_inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_request(method: Method, uri: Uri, body: Vec<u8>) -> S3Request<Body> {
|
||||||
|
S3Request {
|
||||||
|
input: Body::from(body),
|
||||||
|
method,
|
||||||
|
uri,
|
||||||
|
headers: HeaderMap::new(),
|
||||||
|
extensions: Extensions::new(),
|
||||||
|
credentials: Some(s3s::auth::Credentials {
|
||||||
|
access_key: ROOT_ACCESS_KEY.to_string(),
|
||||||
|
secret_key: s3s::auth::SecretKey::from(ROOT_SECRET_KEY.to_string()),
|
||||||
|
}),
|
||||||
|
region: None,
|
||||||
|
service: None,
|
||||||
|
trailing_headers: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn import_archive(archive: Vec<u8>) {
|
||||||
|
let response = ImportBucketMetadata {}
|
||||||
|
.call(
|
||||||
|
admin_request(Method::PUT, Uri::from_static("/rustfs/admin/v3/import-bucket-metadata"), archive),
|
||||||
|
Params::new(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("root admin must import the compatibility archive");
|
||||||
|
assert_eq!(response.output.0, StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
|
async fn g_zip_001_002_003_use_real_admin_archive_and_persistence_paths() {
|
||||||
|
let _ = rustfs_credentials::init_global_action_credentials(
|
||||||
|
Some(ROOT_ACCESS_KEY.to_string()),
|
||||||
|
Some(ROOT_SECRET_KEY.to_string()),
|
||||||
|
);
|
||||||
|
let temp = tempfile::tempdir().expect("create bucket metadata backup test root");
|
||||||
|
let env = rustfs_test_utils::TestECStoreEnv::builder()
|
||||||
|
.base_dir(temp.path())
|
||||||
|
.disk_count(1)
|
||||||
|
.build()
|
||||||
|
.await;
|
||||||
|
env.make_bucket(BUCKET, false).await;
|
||||||
|
rustfs_iam::store::object::ObjectStore::new(Arc::clone(&env.ecstore))
|
||||||
|
.save_iam_config(serde_json::json!({"version": 1}), format!("{}/format.json", *IAM_CONFIG_PREFIX))
|
||||||
|
.await
|
||||||
|
.expect("seed IAM format");
|
||||||
|
let iam = rustfs_iam::build_iam_sys(Arc::clone(&env.ecstore))
|
||||||
|
.await
|
||||||
|
.expect("build test IAM");
|
||||||
|
publish_test_app_context(Arc::new(AppContext::with_default_interfaces(
|
||||||
|
Arc::clone(&env.ecstore),
|
||||||
|
iam,
|
||||||
|
Arc::new(rustfs_kms::KmsServiceManager::new()),
|
||||||
|
)));
|
||||||
|
|
||||||
|
let corrupt_error = ImportBucketMetadata {}
|
||||||
|
.call(
|
||||||
|
admin_request(
|
||||||
|
Method::PUT,
|
||||||
|
Uri::from_static("/rustfs/admin/v3/import-bucket-metadata"),
|
||||||
|
b"not a zip archive".to_vec(),
|
||||||
|
),
|
||||||
|
Params::new(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect_err("a corrupt compatibility archive must be rejected");
|
||||||
|
assert!(
|
||||||
|
corrupt_error
|
||||||
|
.message()
|
||||||
|
.is_some_and(|message| message.contains("failed to read import archive")),
|
||||||
|
"corrupt archive returned the wrong error: {corrupt_error:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let fixtures = persisted_xml_fixtures();
|
||||||
|
import_archive(zip_with_entries(BUCKET, &fixtures)).await;
|
||||||
|
let imported = metadata_sys::get_config_from_disk(BUCKET)
|
||||||
|
.await
|
||||||
|
.expect("old archive must persist bucket metadata");
|
||||||
|
for (config_file, payload) in fixtures {
|
||||||
|
assert_eq!(persisted_xml(&imported, config_file), payload, "g-zip-001 changed {config_file} bytes");
|
||||||
|
}
|
||||||
|
import_archive(zip_with_entries(BUCKET, &[(BUCKET_REPLICATION_CONFIG, b"not xml")])).await;
|
||||||
|
let after_rejected_payload = metadata_sys::get_config_from_disk(BUCKET)
|
||||||
|
.await
|
||||||
|
.expect("rejected payload must leave persisted metadata readable");
|
||||||
|
assert_eq!(
|
||||||
|
after_rejected_payload.replication_config_xml, OLD_REPLICATION_XML,
|
||||||
|
"a rejected archive payload must not replace persisted bytes"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (validated_replication, _) = metadata_sys::get_replication_config(BUCKET)
|
||||||
|
.await
|
||||||
|
.expect("load the revision validated before export");
|
||||||
|
checked_raw_xml(
|
||||||
|
&validated_replication,
|
||||||
|
DIFFERENT_REPLICATION_XML.to_vec(),
|
||||||
|
deserialize::<ReplicationConfiguration>,
|
||||||
|
)
|
||||||
|
.expect_err("a different valid revision must not be exported after validating the old revision");
|
||||||
|
checked_raw_xml(&validated_replication, b"not xml".to_vec(), deserialize::<ReplicationConfiguration>)
|
||||||
|
.expect_err("an invalid raw revision must not be exported after validating the old revision");
|
||||||
|
let matching_raw = checked_raw_xml(
|
||||||
|
&validated_replication,
|
||||||
|
OLD_REPLICATION_XML.to_vec(),
|
||||||
|
deserialize::<ReplicationConfiguration>,
|
||||||
|
)
|
||||||
|
.expect("the exact validated raw revision must remain exportable");
|
||||||
|
assert_eq!(matching_raw, OLD_REPLICATION_XML);
|
||||||
|
let default_versioning = VersioningConfiguration::default();
|
||||||
|
assert!(
|
||||||
|
!checked_versioning_xml(&default_versioning, Vec::new())
|
||||||
|
.expect("empty persisted versioning keeps the existing default fallback")
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
let enabled_versioning: VersioningConfiguration = deserialize(VERSIONING_XML).expect("parse enabled versioning fixture");
|
||||||
|
checked_versioning_xml(&enabled_versioning, Vec::new())
|
||||||
|
.expect_err("an empty raw revision must not export a previously validated enabled revision");
|
||||||
|
|
||||||
|
let export_response = ExportBucketMetadata {}
|
||||||
|
.call(
|
||||||
|
admin_request(
|
||||||
|
Method::GET,
|
||||||
|
format!("/rustfs/admin/v3/export-bucket-metadata?bucket={BUCKET}")
|
||||||
|
.parse()
|
||||||
|
.expect("export URI"),
|
||||||
|
Vec::new(),
|
||||||
|
),
|
||||||
|
Params::new(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("root admin must export persisted bucket metadata");
|
||||||
|
assert_eq!(export_response.output.0, StatusCode::OK);
|
||||||
|
let exported_archive = export_response
|
||||||
|
.output
|
||||||
|
.1
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.expect("read exported archive body")
|
||||||
|
.to_bytes()
|
||||||
|
.to_vec();
|
||||||
|
let mut archive = ZipArchive::new(Cursor::new(&exported_archive)).expect("open exported archive");
|
||||||
|
for (config_file, payload) in persisted_xml_fixtures() {
|
||||||
|
let mut exported_payload = Vec::new();
|
||||||
|
archive
|
||||||
|
.by_name(&format!("{BUCKET}/{config_file}"))
|
||||||
|
.unwrap_or_else(|_| panic!("exported archive must contain {config_file}"))
|
||||||
|
.read_to_end(&mut exported_payload)
|
||||||
|
.unwrap_or_else(|_| panic!("read exported {config_file}"));
|
||||||
|
assert_eq!(exported_payload, payload, "g-zip-003 export must preserve {config_file} byte-for-byte");
|
||||||
|
}
|
||||||
|
drop(archive);
|
||||||
|
|
||||||
|
metadata_sys::update(BUCKET, BUCKET_REPLICATION_CONFIG, DIFFERENT_REPLICATION_XML.to_vec())
|
||||||
|
.await
|
||||||
|
.expect("replace persisted config before rollback import");
|
||||||
|
import_archive(exported_archive).await;
|
||||||
|
let restored = metadata_sys::get_config_from_disk(BUCKET)
|
||||||
|
.await
|
||||||
|
.expect("new archive must persist through old import");
|
||||||
|
for (config_file, payload) in persisted_xml_fixtures() {
|
||||||
|
assert_eq!(
|
||||||
|
persisted_xml(&restored, config_file),
|
||||||
|
payload,
|
||||||
|
"g-zip-002 rollback import did not restore {config_file}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let _: ReplicationConfiguration =
|
||||||
|
deserialize(&restored.replication_config_xml).expect("old parser must read the newly exported archive payload");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod shared_gate_tests {
|
mod shared_gate_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user