mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-28 00:58:59 +00:00
632 lines
23 KiB
Rust
632 lines
23 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use crate::http::{
|
|
AMZ_BUCKET_REPLICATION_STATUS, AMZ_OBJECT_TAGGING, SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER,
|
|
SUFFIX_REPLICATION_RESET_STATUS, get_header_metadata,
|
|
};
|
|
use s3s::dto::ReplicationConfiguration;
|
|
use time::OffsetDateTime;
|
|
use uuid::Uuid;
|
|
|
|
use crate::storage_api::ObjectToDelete;
|
|
use crate::{
|
|
ObjectOpts, ReplicateDecision, ReplicateTargetDecision, ReplicationConfigurationExt as _, ReplicationState,
|
|
ReplicationStatusType, ReplicationType, ResyncTargetDecision, VersionPurgeStatusType, replication_statuses_map,
|
|
target_reset_header, version_purge_statuses_map,
|
|
};
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct MustReplicateOptions {
|
|
meta: HashMap<String, String>,
|
|
status: ReplicationStatusType,
|
|
op_type: ReplicationType,
|
|
replication_request: bool,
|
|
}
|
|
|
|
impl MustReplicateOptions {
|
|
pub fn new(meta: &HashMap<String, String>, user_tags: String, op_type: ReplicationType, replication_request: bool) -> Self {
|
|
let mut meta = meta.clone();
|
|
if !user_tags.is_empty() {
|
|
meta.insert(AMZ_OBJECT_TAGGING.to_string(), user_tags);
|
|
}
|
|
|
|
Self {
|
|
meta,
|
|
status: ReplicationStatusType::default(),
|
|
op_type,
|
|
replication_request,
|
|
}
|
|
}
|
|
|
|
pub fn with_replication_status(mut self, status: ReplicationStatusType) -> Self {
|
|
self.status = status;
|
|
self
|
|
}
|
|
|
|
pub fn replication_status(&self) -> ReplicationStatusType {
|
|
if let Some(rs) = self.meta.get(AMZ_BUCKET_REPLICATION_STATUS) {
|
|
let status = ReplicationStatusType::from(rs.as_str());
|
|
if !status.is_empty() {
|
|
return status;
|
|
}
|
|
}
|
|
self.status.clone()
|
|
}
|
|
|
|
pub fn is_existing_object_replication(&self) -> bool {
|
|
self.op_type == ReplicationType::ExistingObject
|
|
}
|
|
|
|
pub fn is_metadata_replication(&self) -> bool {
|
|
self.op_type == ReplicationType::Metadata
|
|
}
|
|
|
|
pub fn is_replication_request(&self) -> bool {
|
|
self.replication_request
|
|
}
|
|
|
|
pub fn user_tags(&self) -> &str {
|
|
self.meta.get(AMZ_OBJECT_TAGGING).map(String::as_str).unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
pub fn is_ssec_encrypted(user_defined: &HashMap<String, String>) -> bool {
|
|
user_defined.contains_key(SSEC_ALGORITHM_HEADER)
|
|
|| user_defined.contains_key(SSEC_KEY_HEADER)
|
|
|| user_defined.contains_key(SSEC_KEY_MD5_HEADER)
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ReplicationDeleteSource<'a> {
|
|
pub user_defined: &'a HashMap<String, String>,
|
|
pub user_tags: &'a str,
|
|
pub delete_marker: bool,
|
|
pub replication_status: ReplicationStatusType,
|
|
}
|
|
|
|
pub fn delete_replication_object_opts(dobj: &ObjectToDelete, source: &ReplicationDeleteSource<'_>) -> ObjectOpts {
|
|
ObjectOpts {
|
|
name: dobj.object_name.clone(),
|
|
ssec: is_ssec_encrypted(source.user_defined),
|
|
user_tags: source.user_tags.to_string(),
|
|
delete_marker: source.delete_marker,
|
|
version_id: dobj.version_id,
|
|
op_type: ReplicationType::Delete,
|
|
replica: source.replication_status == ReplicationStatusType::Replica,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ReplicationDeleteStateSource {
|
|
pub name: String,
|
|
pub user_tags: String,
|
|
pub version_id: Option<Uuid>,
|
|
pub delete_marker: bool,
|
|
pub replica: bool,
|
|
}
|
|
|
|
pub fn delete_replication_state_from_config(
|
|
config: &ReplicationConfiguration,
|
|
source: &ReplicationDeleteStateSource,
|
|
) -> Option<ReplicationState> {
|
|
let opts = ObjectOpts {
|
|
name: source.name.clone(),
|
|
user_tags: source.user_tags.clone(),
|
|
version_id: source.version_id,
|
|
delete_marker: source.delete_marker,
|
|
op_type: ReplicationType::Delete,
|
|
replica: source.replica,
|
|
..Default::default()
|
|
};
|
|
let target_arns = config.filter_target_arns(&opts);
|
|
if target_arns.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let mut decision = ReplicateDecision::new();
|
|
for target_arn in target_arns {
|
|
let mut target_opts = opts.clone();
|
|
target_opts.target_arn = target_arn.clone();
|
|
let replicate = config.replicate(&target_opts);
|
|
decision.set(ReplicateTargetDecision::new(target_arn, replicate, false));
|
|
}
|
|
if !decision.replicate_any() {
|
|
return None;
|
|
}
|
|
|
|
let pending_status = decision.pending_status();
|
|
let mut state = ReplicationState {
|
|
replicate_decision_str: decision.to_string(),
|
|
..Default::default()
|
|
};
|
|
if source.version_id.is_some() {
|
|
state.version_purge_status_internal = pending_status.clone();
|
|
state.purge_targets = version_purge_statuses_map(pending_status.as_deref().unwrap_or_default());
|
|
} else {
|
|
state.replication_status_internal = pending_status.clone();
|
|
state.targets = replication_statuses_map(pending_status.as_deref().unwrap_or_default());
|
|
}
|
|
Some(state)
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct ReplicationDeleteScheduleInput<'a> {
|
|
pub replication_request: bool,
|
|
pub version_id_requested: bool,
|
|
pub source_delete_marker: bool,
|
|
pub source_replication_status: &'a ReplicationStatusType,
|
|
pub source_version_purge_status: &'a VersionPurgeStatusType,
|
|
pub deleted_delete_marker_version: bool,
|
|
}
|
|
|
|
fn delete_version_purge_source_status(status: &ReplicationStatusType) -> bool {
|
|
status == &ReplicationStatusType::Replica
|
|
|| status == &ReplicationStatusType::Pending
|
|
|| status == &ReplicationStatusType::Completed
|
|
|| status == &ReplicationStatusType::Failed
|
|
}
|
|
|
|
pub fn should_schedule_delete_replication(input: ReplicationDeleteScheduleInput<'_>) -> bool {
|
|
if input.replication_request {
|
|
return false;
|
|
}
|
|
|
|
if input.version_id_requested && !input.deleted_delete_marker_version && !input.source_delete_marker {
|
|
return delete_version_purge_source_status(input.source_replication_status);
|
|
}
|
|
|
|
input.source_replication_status == &ReplicationStatusType::Replica
|
|
|| input.source_replication_status == &ReplicationStatusType::Pending
|
|
|| input.source_version_purge_status == &VersionPurgeStatusType::Pending
|
|
|| (input.deleted_delete_marker_version && input.source_replication_status == &ReplicationStatusType::Completed)
|
|
}
|
|
|
|
pub fn delete_replication_version_id(
|
|
source_delete_marker: bool,
|
|
source_version_id: Option<Uuid>,
|
|
deleted_delete_marker_version: bool,
|
|
) -> Option<Uuid> {
|
|
if source_delete_marker && !deleted_delete_marker_version {
|
|
None
|
|
} else {
|
|
source_version_id
|
|
}
|
|
}
|
|
|
|
pub fn should_use_existing_delete_replication_source(
|
|
replication_request: bool,
|
|
deleted_delete_marker: bool,
|
|
has_existing_source: bool,
|
|
) -> bool {
|
|
replication_request && deleted_delete_marker && has_existing_source
|
|
}
|
|
|
|
pub fn should_use_existing_delete_replication_info(version_id_requested: bool, delete_marker_request: bool) -> bool {
|
|
version_id_requested && !delete_marker_request
|
|
}
|
|
|
|
pub fn heal_uses_delete_replication_path(delete_marker: bool, version_purge_status: &VersionPurgeStatusType) -> bool {
|
|
delete_marker || !version_purge_status.is_empty()
|
|
}
|
|
|
|
pub fn delete_replication_missing_source_decision(
|
|
delete_marker: bool,
|
|
target_status: ReplicationStatusType,
|
|
rule_replicates: bool,
|
|
version_purge_status: &VersionPurgeStatusType,
|
|
) -> Option<bool> {
|
|
let valid_replication_status = matches!(
|
|
target_status,
|
|
ReplicationStatusType::Pending | ReplicationStatusType::Completed | ReplicationStatusType::Failed
|
|
);
|
|
|
|
if delete_marker && (valid_replication_status || rule_replicates) {
|
|
return Some(rule_replicates);
|
|
}
|
|
|
|
if !version_purge_status.is_empty() {
|
|
return Some(matches!(
|
|
version_purge_status,
|
|
&VersionPurgeStatusType::Pending | &VersionPurgeStatusType::Failed
|
|
));
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ReplicationResyncTargetObject<'a> {
|
|
pub mod_time: Option<OffsetDateTime>,
|
|
pub user_defined: &'a HashMap<String, String>,
|
|
}
|
|
|
|
pub fn resync_target_for_object(
|
|
object: &ReplicationResyncTargetObject<'_>,
|
|
arn: &str,
|
|
reset_id: &str,
|
|
reset_before_date: Option<OffsetDateTime>,
|
|
status: ReplicationStatusType,
|
|
) -> ResyncTargetDecision {
|
|
let rs = object
|
|
.user_defined
|
|
.get(target_reset_header(arn).as_str())
|
|
.cloned()
|
|
.or_else(|| get_header_metadata(object.user_defined, SUFFIX_REPLICATION_RESET_STATUS));
|
|
|
|
let mut dec = ResyncTargetDecision::default();
|
|
|
|
let mod_time = object.mod_time.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
|
|
|
let Some(rs) = rs else {
|
|
let reset_before_date = reset_before_date.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
|
if !reset_id.is_empty() && mod_time <= reset_before_date {
|
|
dec.replicate = true;
|
|
return dec;
|
|
}
|
|
|
|
dec.replicate = status == ReplicationStatusType::Empty;
|
|
|
|
return dec;
|
|
};
|
|
|
|
let Some(reset_before_date) = reset_before_date else {
|
|
return dec;
|
|
};
|
|
|
|
if reset_id.is_empty() {
|
|
return dec;
|
|
}
|
|
|
|
let parts: Vec<&str> = rs.splitn(2, ';').collect();
|
|
|
|
if parts.len() != 2 {
|
|
return dec;
|
|
}
|
|
|
|
let new_reset = parts[1] != reset_id;
|
|
|
|
if !new_reset && status == ReplicationStatusType::Completed {
|
|
return dec;
|
|
}
|
|
|
|
dec.replicate = new_reset && mod_time <= reset_before_date;
|
|
|
|
dec
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
MustReplicateOptions, ReplicationDeleteScheduleInput, ReplicationDeleteSource, ReplicationDeleteStateSource,
|
|
ReplicationResyncTargetObject, delete_replication_missing_source_decision, delete_replication_object_opts,
|
|
delete_replication_state_from_config, delete_replication_version_id, heal_uses_delete_replication_path,
|
|
is_ssec_encrypted, resync_target_for_object, should_schedule_delete_replication,
|
|
should_use_existing_delete_replication_info, should_use_existing_delete_replication_source,
|
|
};
|
|
use crate::http::{AMZ_BUCKET_REPLICATION_STATUS, SSEC_ALGORITHM_HEADER};
|
|
use crate::storage_api::ObjectToDelete;
|
|
use crate::{ReplicationStatusType, ReplicationType, VersionPurgeStatusType, target_reset_header};
|
|
use s3s::dto::{
|
|
DeleteMarkerReplication, DeleteMarkerReplicationStatus, Destination, ExistingObjectReplication,
|
|
ExistingObjectReplicationStatus, ReplicaModifications, ReplicaModificationsStatus, ReplicationConfiguration,
|
|
ReplicationRule, ReplicationRuleStatus, SourceSelectionCriteria,
|
|
};
|
|
use std::collections::HashMap;
|
|
use time::{Duration, OffsetDateTime};
|
|
use uuid::Uuid;
|
|
|
|
#[test]
|
|
fn must_replicate_options_preserves_user_tags_and_operation_type() {
|
|
let options = MustReplicateOptions::new(&HashMap::new(), "env=prod".to_string(), ReplicationType::ExistingObject, true);
|
|
|
|
assert_eq!(options.user_tags(), "env=prod");
|
|
assert!(options.is_existing_object_replication());
|
|
assert!(options.is_replication_request());
|
|
}
|
|
|
|
#[test]
|
|
fn must_replicate_options_reads_replication_status_header() {
|
|
let meta = HashMap::from([(AMZ_BUCKET_REPLICATION_STATUS.to_string(), "COMPLETED".to_string())]);
|
|
let options = MustReplicateOptions::new(&meta, String::new(), ReplicationType::Object, false)
|
|
.with_replication_status(ReplicationStatusType::Replica);
|
|
|
|
assert_eq!(options.replication_status(), ReplicationStatusType::Completed);
|
|
}
|
|
|
|
#[test]
|
|
fn must_replicate_options_falls_back_to_explicit_replication_status() {
|
|
let options = MustReplicateOptions::new(&HashMap::new(), String::new(), ReplicationType::Delete, false)
|
|
.with_replication_status(ReplicationStatusType::Replica);
|
|
|
|
assert_eq!(options.replication_status(), ReplicationStatusType::Replica);
|
|
}
|
|
|
|
#[test]
|
|
fn ssec_detection_uses_existing_metadata_headers() {
|
|
let meta = HashMap::from([(SSEC_ALGORITHM_HEADER.to_string(), "AES256".to_string())]);
|
|
|
|
assert!(is_ssec_encrypted(&meta));
|
|
assert!(!is_ssec_encrypted(&HashMap::new()));
|
|
}
|
|
|
|
#[test]
|
|
fn heal_delete_path_is_limited_to_delete_markers_and_version_purges() {
|
|
assert!(heal_uses_delete_replication_path(true, &VersionPurgeStatusType::Empty));
|
|
assert!(heal_uses_delete_replication_path(false, &VersionPurgeStatusType::Pending));
|
|
assert!(!heal_uses_delete_replication_path(false, &VersionPurgeStatusType::Empty));
|
|
}
|
|
|
|
#[test]
|
|
fn missing_source_delete_decision_keeps_delete_marker_and_purge_rules() {
|
|
assert_eq!(
|
|
delete_replication_missing_source_decision(
|
|
true,
|
|
ReplicationStatusType::Completed,
|
|
false,
|
|
&VersionPurgeStatusType::Empty,
|
|
),
|
|
Some(false)
|
|
);
|
|
assert_eq!(
|
|
delete_replication_missing_source_decision(
|
|
false,
|
|
ReplicationStatusType::Empty,
|
|
false,
|
|
&VersionPurgeStatusType::Pending,
|
|
),
|
|
Some(true)
|
|
);
|
|
assert_eq!(
|
|
delete_replication_missing_source_decision(
|
|
false,
|
|
ReplicationStatusType::Empty,
|
|
false,
|
|
&VersionPurgeStatusType::Empty
|
|
),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn delete_replication_object_opts_marks_replica_deletes() {
|
|
let version_id = Uuid::new_v4();
|
|
let user_defined = HashMap::from([(SSEC_ALGORITHM_HEADER.to_string(), "AES256".to_string())]);
|
|
let dobj = ObjectToDelete {
|
|
object_name: "obj".to_string(),
|
|
version_id: Some(version_id),
|
|
..Default::default()
|
|
};
|
|
let source = ReplicationDeleteSource {
|
|
user_defined: &user_defined,
|
|
user_tags: "env=prod",
|
|
delete_marker: true,
|
|
replication_status: ReplicationStatusType::Replica,
|
|
};
|
|
|
|
let opts = delete_replication_object_opts(&dobj, &source);
|
|
|
|
assert!(opts.replica);
|
|
assert!(opts.ssec);
|
|
assert_eq!(opts.user_tags, "env=prod");
|
|
assert_eq!(opts.version_id, Some(version_id));
|
|
assert_eq!(opts.name, "obj");
|
|
assert_eq!(opts.op_type, ReplicationType::Delete);
|
|
}
|
|
|
|
fn delete_replication_rule(arn: &str, replica_modifications: bool) -> ReplicationRule {
|
|
ReplicationRule {
|
|
delete_marker_replication: Some(DeleteMarkerReplication {
|
|
status: Some(DeleteMarkerReplicationStatus::from_static(DeleteMarkerReplicationStatus::ENABLED)),
|
|
}),
|
|
delete_replication: None,
|
|
destination: Destination {
|
|
bucket: arn.to_string(),
|
|
..Default::default()
|
|
},
|
|
existing_object_replication: Some(ExistingObjectReplication {
|
|
status: ExistingObjectReplicationStatus::from_static(ExistingObjectReplicationStatus::ENABLED),
|
|
}),
|
|
filter: None,
|
|
id: Some("rule-1".to_string()),
|
|
prefix: Some("test/".to_string()),
|
|
priority: Some(1),
|
|
source_selection_criteria: replica_modifications.then_some(SourceSelectionCriteria {
|
|
replica_modifications: Some(ReplicaModifications {
|
|
status: ReplicaModificationsStatus::from_static(ReplicaModificationsStatus::ENABLED),
|
|
}),
|
|
sse_kms_encrypted_objects: None,
|
|
}),
|
|
status: ReplicationRuleStatus::from_static(ReplicationRuleStatus::ENABLED),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn delete_replication_state_tracks_downstream_delete_marker_targets() {
|
|
let arn = "arn:aws:s3:::target-bucket";
|
|
let config = ReplicationConfiguration {
|
|
role: arn.to_string(),
|
|
rules: vec![delete_replication_rule(arn, true)],
|
|
};
|
|
let source = ReplicationDeleteStateSource {
|
|
name: "test/object.txt".to_string(),
|
|
user_tags: String::new(),
|
|
version_id: None,
|
|
delete_marker: true,
|
|
replica: true,
|
|
};
|
|
|
|
let state = delete_replication_state_from_config(&config, &source)
|
|
.expect("replica delete marker should be forwarded to downstream targets");
|
|
let pending = format!("{arn}=PENDING;");
|
|
|
|
assert_eq!(state.replication_status_internal.as_deref(), Some(pending.as_str()));
|
|
assert_eq!(state.replicate_decision_str, format!("{arn}=true;false;{arn};"));
|
|
assert!(state.targets.contains_key(arn));
|
|
}
|
|
|
|
#[test]
|
|
fn delete_replication_state_skips_replica_delete_without_replica_modifications() {
|
|
let arn = "arn:aws:s3:::target-bucket";
|
|
let config = ReplicationConfiguration {
|
|
role: arn.to_string(),
|
|
rules: vec![delete_replication_rule(arn, false)],
|
|
};
|
|
let source = ReplicationDeleteStateSource {
|
|
name: "test/object.txt".to_string(),
|
|
user_tags: String::new(),
|
|
version_id: None,
|
|
delete_marker: true,
|
|
replica: true,
|
|
};
|
|
|
|
assert!(delete_replication_state_from_config(&config, &source).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn delete_replication_state_tracks_delete_marker_version_purges() {
|
|
let arn = "arn:aws:s3:::target-bucket";
|
|
let config = ReplicationConfiguration {
|
|
role: arn.to_string(),
|
|
rules: vec![delete_replication_rule(arn, false)],
|
|
};
|
|
let source = ReplicationDeleteStateSource {
|
|
name: "test/object.txt".to_string(),
|
|
user_tags: String::new(),
|
|
version_id: Some(Uuid::new_v4()),
|
|
delete_marker: true,
|
|
replica: false,
|
|
};
|
|
|
|
let state = delete_replication_state_from_config(&config, &source)
|
|
.expect("delete-marker version purge should honor delete-marker replication rules");
|
|
let pending = format!("{arn}=PENDING;");
|
|
|
|
assert_eq!(state.version_purge_status_internal.as_deref(), Some(pending.as_str()));
|
|
assert_eq!(state.replicate_decision_str, format!("{arn}=true;false;{arn};"));
|
|
assert!(state.purge_targets.contains_key(arn));
|
|
}
|
|
|
|
#[test]
|
|
fn delete_replication_schedule_skips_replica_requests() {
|
|
assert!(!should_schedule_delete_replication(ReplicationDeleteScheduleInput {
|
|
replication_request: true,
|
|
version_id_requested: true,
|
|
source_delete_marker: true,
|
|
source_replication_status: &ReplicationStatusType::Completed,
|
|
source_version_purge_status: &VersionPurgeStatusType::Empty,
|
|
deleted_delete_marker_version: true,
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn delete_replication_schedule_keeps_marker_and_version_purges() {
|
|
assert!(should_schedule_delete_replication(ReplicationDeleteScheduleInput {
|
|
replication_request: false,
|
|
version_id_requested: true,
|
|
source_delete_marker: true,
|
|
source_replication_status: &ReplicationStatusType::Completed,
|
|
source_version_purge_status: &VersionPurgeStatusType::Empty,
|
|
deleted_delete_marker_version: true,
|
|
}));
|
|
assert!(should_schedule_delete_replication(ReplicationDeleteScheduleInput {
|
|
replication_request: false,
|
|
version_id_requested: true,
|
|
source_delete_marker: false,
|
|
source_replication_status: &ReplicationStatusType::Completed,
|
|
source_version_purge_status: &VersionPurgeStatusType::Empty,
|
|
deleted_delete_marker_version: false,
|
|
}));
|
|
assert!(should_schedule_delete_replication(ReplicationDeleteScheduleInput {
|
|
replication_request: false,
|
|
version_id_requested: false,
|
|
source_delete_marker: false,
|
|
source_replication_status: &ReplicationStatusType::Empty,
|
|
source_version_purge_status: &VersionPurgeStatusType::Pending,
|
|
deleted_delete_marker_version: false,
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn delete_replication_version_id_splits_marker_creation_and_purge() {
|
|
let version_id = Uuid::new_v4();
|
|
|
|
assert_eq!(delete_replication_version_id(true, Some(version_id), false), None);
|
|
assert_eq!(delete_replication_version_id(true, Some(version_id), true), Some(version_id));
|
|
}
|
|
|
|
#[test]
|
|
fn delete_replication_source_selection_prefers_existing_marker_source_only_for_replica_requests() {
|
|
assert!(should_use_existing_delete_replication_source(true, true, true));
|
|
assert!(!should_use_existing_delete_replication_source(false, true, true));
|
|
assert!(!should_use_existing_delete_replication_source(true, false, true));
|
|
assert!(!should_use_existing_delete_replication_source(true, true, false));
|
|
}
|
|
|
|
#[test]
|
|
fn existing_delete_replication_info_is_limited_to_version_delete_requests() {
|
|
assert!(should_use_existing_delete_replication_info(true, false));
|
|
assert!(!should_use_existing_delete_replication_info(true, true));
|
|
assert!(!should_use_existing_delete_replication_info(false, false));
|
|
}
|
|
|
|
#[test]
|
|
fn resync_target_includes_object_at_reset_before_boundary() {
|
|
let reset_before = OffsetDateTime::UNIX_EPOCH + Duration::seconds(30);
|
|
let user_defined = HashMap::new();
|
|
let object = ReplicationResyncTargetObject {
|
|
mod_time: Some(reset_before),
|
|
user_defined: &user_defined,
|
|
};
|
|
|
|
let decision =
|
|
resync_target_for_object(&object, "arn:target", "reset-1", Some(reset_before), ReplicationStatusType::Completed);
|
|
|
|
assert!(decision.replicate);
|
|
}
|
|
|
|
#[test]
|
|
fn resync_target_replicates_when_reset_id_changes() {
|
|
let reset_before = OffsetDateTime::UNIX_EPOCH + Duration::seconds(30);
|
|
let user_defined = HashMap::from([(target_reset_header("arn:target"), "1970-01-01T00:00:20Z;old-reset".to_string())]);
|
|
let object = ReplicationResyncTargetObject {
|
|
mod_time: Some(OffsetDateTime::UNIX_EPOCH + Duration::seconds(10)),
|
|
user_defined: &user_defined,
|
|
};
|
|
|
|
let decision =
|
|
resync_target_for_object(&object, "arn:target", "new-reset", Some(reset_before), ReplicationStatusType::Completed);
|
|
|
|
assert!(decision.replicate);
|
|
}
|
|
|
|
#[test]
|
|
fn resync_target_skips_completed_object_for_same_reset_id() {
|
|
let reset_before = OffsetDateTime::UNIX_EPOCH + Duration::seconds(30);
|
|
let user_defined = HashMap::from([(target_reset_header("arn:target"), "1970-01-01T00:00:20Z;same-reset".to_string())]);
|
|
let object = ReplicationResyncTargetObject {
|
|
mod_time: Some(OffsetDateTime::UNIX_EPOCH + Duration::seconds(10)),
|
|
user_defined: &user_defined,
|
|
};
|
|
|
|
let decision =
|
|
resync_target_for_object(&object, "arn:target", "same-reset", Some(reset_before), ReplicationStatusType::Completed);
|
|
|
|
assert!(!decision.replicate);
|
|
}
|
|
}
|