Files
rustfs/crates/ecstore/src/bucket/target/bucket_target.rs
T
Zhengchao An 6cf9cf7bb5 chore(ecstore): drop the bucket dead_code blanket (#6147)
* chore(ecstore): drop the bucket dead_code blanket

The last blanket of the backlog#1823 burn-down, and the largest: 71 items across lifecycle, replication, metadata, quota, object lock and bucket utils. Four are deleted.

Deleted, all trivial:

- check_valid_object_name and check_valid_object_name_prefix, a pair that only calls into each other with no external caller. Worth stating plainly so nobody reads this as a validation gap: object names are validated through check_object_name_for_length_and_slash, which is live; this pair is a second, unwired entry point.
- DEFAULT_HEALTH_CHECK_RELOAD_DURATION, a lone unused constant.
- The LifecycleReplicationConfig alias, which orphaned a re-export in replication/mod.rs that goes with it.

Everything else is kept, in four groups, because the blanket here was hiding structure rather than rot:

Windows platform gating. WINDOWS_RESERVED_NAMES, the two reason constants and object_name_has_windows_incompatible_segment are called from inside the #[cfg(target_os = "windows")] block in check_object_name_for_length_and_slash (utils.rs:228-255), so they only read as dead on non-Windows hosts. As with the Linux gating in the disk root, this cannot be adjudicated locally: cargo check for both x86_64-pc-windows-msvc and x86_64-unknown-linux-gnu fails in the aws-lc-sys build script for want of a cross C toolchain. CI covers both.

Declared boundary surface. The *_boundary.rs and *_bridge.rs files carry the replication split plan's contracts, which scripts/check_architecture_migration_rules.sh pins through the EcstoreReplicationBoundaryImports section of the split-plan doc. Their unused items are declarations, not leftovers.

test-util seams. ConfigWriteLockProbe with install/wait_until_attempted follows the same pattern as the barriers in the services and set_disk roots.

MinIO-parity tier/lifecycle entry points that this port never wired: apply_lifecycle_action, get_transitioned_object_reader, recover_tier_free_versions, delete_object_from_remote_tier, abort_tier_delete_journal_entry and the replication pool's worker-management surface. These are complete, substantial machinery with no caller — the same shape as data_usage's local_snapshot feature. Removing them is a product decision, so they are made explicit here rather than deleted.

Verification, four lanes warning-free: default, --tests, --features rio-v2 --tests, --features test-util --tests. cargo nextest run -p rustfs-ecstore 4096 passed; clippy --lib --tests -D warnings clean; make pre-commit exit 0. Note that clippy is what caught the orphaned re-export above: cargo check and pre-commit both treat unused_imports as a warning.

Ref rustfs/backlog#1823 (step 2, final root).

* chore(ecstore): correct inaccurate dead_code reasons in the bucket root

Six items were labelled 'asserted by this file's tests' or as MinIO-parity
entry points while having no caller at all - free get_bucket_acl_config and
created_at only reach their own live methods (production goes through
created_at_in), BucketVersioningSys::get_in, utils::serialize_content and
ServiceType have no reference anywhere, and with_transition_queue_env_async
is an unused test fixture, not a tier entry point. Name what each one is so
the next reader does not assume coverage that is not there.

Ref rustfs/backlog#1823.
2026-08-16 21:39:04 +08:00

1042 lines
39 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 crate::error::{Error, Result};
use jiff::Timestamp;
use rmp_serde::Serializer as rmpSerializer;
use serde::{Deserialize, Serialize};
use std::{
fmt::{self, Display},
str::FromStr,
time::Duration,
};
use time::OffsetDateTime;
use url::Url;
const REDACTED_CREDENTIAL: &str = "<redacted>";
#[derive(Deserialize, Serialize, Default, Clone)]
pub struct Credentials {
#[serde(rename = "accessKey")]
pub access_key: String,
#[serde(rename = "secretKey")]
pub secret_key: String,
pub session_token: Option<String>,
pub expiration: Option<Timestamp>,
}
impl Credentials {
pub fn redacted(&self) -> Self {
Self {
access_key: self.access_key.clone(),
secret_key: String::new(),
session_token: None,
expiration: self.expiration,
}
}
}
impl fmt::Debug for Credentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Credentials")
.field("access_key", &self.access_key)
.field("secret_key", &REDACTED_CREDENTIAL)
.field("session_token", &self.session_token.as_ref().map(|_| REDACTED_CREDENTIAL))
.field("expiration", &self.expiration)
.finish()
}
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
#[allow(
dead_code,
reason = "MinIO-parity bucket-target service discriminator with no caller in this port (backlog#1823)"
)]
pub enum ServiceType {
#[default]
Replication,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct LatencyStat {
#[serde(with = "duration_milliseconds")]
pub curr: Duration, // Current latency
#[serde(with = "duration_milliseconds")]
pub avg: Duration, // Average latency
#[serde(with = "duration_milliseconds")]
pub max: Duration, // Maximum latency
}
mod duration_milliseconds {
use serde::{Deserialize, Deserializer, Serializer};
use std::time::Duration;
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(duration.as_millis() as u64)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let millis = u64::deserialize(deserializer)?;
Ok(Duration::from_millis(millis))
}
}
/// Defensive decode for the two integer wire encodings of these duration
/// fields: RustFS persists (and legacy RustFS clients sent) plain seconds,
/// while Go `time.Duration` JSON — madmin/mc requests and MinIO-written
/// bucket-targets metadata — is nanoseconds. No meaningful interval lies
/// between 10^7 seconds (~115 days) and 10^7 nanoseconds (10ms), so the
/// magnitude disambiguates the unit.
pub fn duration_from_secs_or_nanos(value: u64) -> Duration {
const NANOS_THRESHOLD: u64 = 10_000_000;
if value < NANOS_THRESHOLD {
Duration::from_secs(value)
} else {
Duration::from_nanos(value)
}
}
mod duration_seconds {
use serde::{Deserialize, Deserializer, Serializer};
use std::time::Duration;
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(duration.as_secs())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let value = u64::deserialize(deserializer)?;
Ok(super::duration_from_secs_or_nanos(value))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub enum BucketTargetType {
#[default]
None,
#[serde(rename = "replication")]
ReplicationService,
#[serde(rename = "ilm")]
IlmService,
}
impl BucketTargetType {
pub fn is_valid(&self) -> bool {
match self {
BucketTargetType::None => false,
BucketTargetType::ReplicationService | BucketTargetType::IlmService => true,
}
}
}
impl FromStr for BucketTargetType {
type Err = std::io::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"replication" => Ok(BucketTargetType::ReplicationService),
"ilm" => Ok(BucketTargetType::IlmService),
_ => Ok(BucketTargetType::None),
}
}
}
impl fmt::Display for BucketTargetType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BucketTargetType::None => write!(f, ""),
BucketTargetType::ReplicationService => write!(f, "replication"),
BucketTargetType::IlmService => write!(f, "ilm"),
}
}
}
// Define BucketTarget structure
#[derive(Deserialize, Serialize, Default, Clone)]
pub struct BucketTarget {
#[serde(rename = "sourcebucket", default)]
pub source_bucket: String,
#[serde(default)]
pub endpoint: String,
#[serde(default)]
pub credentials: Option<Credentials>,
#[serde(rename = "targetbucket", default)]
pub target_bucket: String,
#[serde(default)]
pub secure: bool,
#[serde(default)]
pub path: String,
#[serde(default)]
pub api: String,
#[serde(default)]
pub arn: String,
#[serde(rename = "type", default)]
pub target_type: BucketTargetType,
#[serde(default)]
pub region: String,
#[serde(alias = "bandwidth", default)]
pub bandwidth_limit: i64,
#[serde(rename = "replicationSync", default)]
pub replication_sync: bool,
#[serde(default)]
pub storage_class: String,
#[serde(rename = "skipTlsVerify", default)]
pub skip_tls_verify: bool,
#[serde(rename = "caCertPem", default)]
pub ca_cert_pem: String,
#[serde(rename = "healthCheckDuration", with = "duration_seconds", default)]
pub health_check_duration: Duration,
#[serde(rename = "disableProxy", default)]
pub disable_proxy: bool,
#[serde(rename = "resetBeforeDate", with = "time::serde::rfc3339::option", default)]
pub reset_before_date: Option<OffsetDateTime>,
#[serde(default)]
pub reset_id: String,
#[serde(rename = "totalDowntime", with = "duration_seconds", default)]
pub total_downtime: Duration,
#[serde(rename = "lastOnline", with = "time::serde::rfc3339::option", default)]
pub last_online: Option<OffsetDateTime>,
#[serde(rename = "isOnline", default)]
pub online: bool,
#[serde(default)]
pub latency: LatencyStat,
#[serde(default)]
pub deployment_id: String,
#[serde(default)]
pub edge: bool,
#[serde(rename = "edgeSyncBeforeExpiry", default)]
pub edge_sync_before_expiry: bool,
#[serde(rename = "offlineCount", default)]
pub offline_count: u64,
}
impl BucketTarget {
pub fn redacted_credentials(&self) -> Self {
let mut target = self.clone();
target.credentials = target.credentials.as_ref().map(Credentials::redacted);
target
}
pub fn is_empty(self) -> bool {
self.target_bucket.is_empty() && self.endpoint.is_empty() && self.arn.is_empty()
}
pub fn url(&self) -> Result<Url> {
let scheme = if self.secure { "https" } else { "http" };
Url::parse(&format!("{}://{}", scheme, self.endpoint)).map_err(Error::other)
}
}
impl fmt::Debug for BucketTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BucketTarget")
.field("source_bucket", &self.source_bucket)
.field("endpoint", &self.endpoint)
.field("credentials", &self.credentials)
.field("target_bucket", &self.target_bucket)
.field("secure", &self.secure)
.field("path", &self.path)
.field("api", &self.api)
.field("arn", &self.arn)
.field("target_type", &self.target_type)
.field("region", &self.region)
.field("bandwidth_limit", &self.bandwidth_limit)
.field("replication_sync", &self.replication_sync)
.field("storage_class", &self.storage_class)
.field("skip_tls_verify", &self.skip_tls_verify)
.field("has_custom_ca", &!self.ca_cert_pem.trim().is_empty())
.field("health_check_duration", &self.health_check_duration)
.field("disable_proxy", &self.disable_proxy)
.field("reset_before_date", &self.reset_before_date)
.field("reset_id", &self.reset_id)
.field("total_downtime", &self.total_downtime)
.field("last_online", &self.last_online)
.field("online", &self.online)
.field("latency", &self.latency)
.field("deployment_id", &self.deployment_id)
.field("edge", &self.edge)
.field("edge_sync_before_expiry", &self.edge_sync_before_expiry)
.field("offline_count", &self.offline_count)
.finish()
}
}
impl Display for BucketTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ", self.endpoint)?;
write!(f, "{}", self.target_bucket.clone())?;
Ok(())
}
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct BucketTargets {
pub targets: Vec<BucketTarget>,
}
impl BucketTargets {
pub fn redacted_credentials(&self) -> Self {
Self {
targets: self.targets.iter().map(BucketTarget::redacted_credentials).collect(),
}
}
pub fn marshal_msg(&self) -> Result<Vec<u8>> {
let mut buf = Vec::new();
self.serialize(&mut rmpSerializer::new(&mut buf).with_struct_map())?;
Ok(buf)
}
pub fn unmarshal(buf: &[u8]) -> Result<Self> {
let t: BucketTargets = rmp_serde::from_slice(buf)?;
Ok(t)
}
pub fn is_empty(&self) -> bool {
if self.targets.is_empty() {
return true;
}
for target in &self.targets {
if !target.clone().is_empty() {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json;
use std::time::Duration;
use time::OffsetDateTime;
#[test]
fn test_bucket_target_json_deserialize() {
let json = r#"
{
"sourcebucket": "source-bucket-name",
"endpoint": "s3.amazonaws.com",
"credentials": {
"accessKey": "test-access-key",
"secretKey": "test-secret-key",
"session_token": "test-session-token",
"expiration": "2024-12-31T23:59:59Z"
},
"targetbucket": "target-bucket-name",
"secure": true,
"path": "/api/v1",
"api": "s3v4",
"arn": "arn:aws:s3:::target-bucket-name",
"type": "replication",
"region": "us-east-1",
"bandwidth_limit": 1000000,
"replicationSync": true,
"storage_class": "STANDARD",
"skipTlsVerify": true,
"caCertPem": "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n",
"healthCheckDuration": 30,
"disableProxy": false,
"resetBeforeDate": null,
"reset_id": "reset-123",
"totalDowntime": 3600,
"last_online": null,
"isOnline": true,
"latency": {
"curr": 100,
"avg": 150,
"max": 300
},
"deployment_id": "deployment-456",
"edge": false,
"edgeSyncBeforeExpiry": true,
"offlineCount": 5
}
"#;
let result: std::result::Result<BucketTarget, _> = serde_json::from_str(json);
assert!(result.is_ok(), "Failed to deserialize BucketTarget: {:?}", result.err());
let target = result.unwrap();
// Verify basic fields
assert_eq!(target.source_bucket, "source-bucket-name");
assert_eq!(target.endpoint, "s3.amazonaws.com");
assert_eq!(target.target_bucket, "target-bucket-name");
assert!(target.secure);
assert_eq!(target.path, "/api/v1");
assert_eq!(target.api, "s3v4");
assert_eq!(target.arn, "arn:aws:s3:::target-bucket-name");
assert_eq!(target.target_type, BucketTargetType::ReplicationService);
assert_eq!(target.region, "us-east-1");
assert_eq!(target.bandwidth_limit, 1000000);
assert!(target.replication_sync);
assert_eq!(target.storage_class, "STANDARD");
assert!(target.skip_tls_verify);
assert_eq!(target.ca_cert_pem, "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n");
assert_eq!(target.health_check_duration, Duration::from_secs(30));
assert!(!target.disable_proxy);
assert_eq!(target.reset_id, "reset-123");
assert_eq!(target.total_downtime, Duration::from_secs(3600));
assert!(target.online);
assert_eq!(target.deployment_id, "deployment-456");
assert!(!target.edge);
assert!(target.edge_sync_before_expiry);
assert_eq!(target.offline_count, 5);
// Verify credentials
assert!(target.credentials.is_some());
let credentials = target.credentials.unwrap();
assert_eq!(credentials.access_key, "test-access-key");
assert_eq!(credentials.secret_key, "test-secret-key");
assert_eq!(credentials.session_token, Some("test-session-token".to_string()));
assert_eq!(
serde_json::to_value(credentials.expiration.expect("expiration should parse"))
.expect("expiration should serialize to JSON"),
serde_json::json!("2024-12-31T23:59:59Z")
);
// Verify latency statistics
assert_eq!(target.latency.curr, Duration::from_millis(100));
assert_eq!(target.latency.avg, Duration::from_millis(150));
assert_eq!(target.latency.max, Duration::from_millis(300));
// Verify time fields
assert!(target.reset_before_date.is_none());
assert!(target.last_online.is_none());
}
#[test]
fn test_bucket_target_json_serialize_deserialize_roundtrip() {
let original = BucketTarget {
source_bucket: "test-source".to_string(),
endpoint: "rustfs.example.com".to_string(),
credentials: Some(Credentials {
access_key: "rustfsaccess".to_string(),
secret_key: "rustfssecret".to_string(),
session_token: None,
expiration: None,
}),
target_bucket: "test-target".to_string(),
secure: false,
path: "/".to_string(),
api: "s3v4".to_string(),
arn: "arn:rustfs:s3:::test-target".to_string(),
target_type: BucketTargetType::ReplicationService,
region: "us-west-2".to_string(),
bandwidth_limit: 500000,
replication_sync: false,
storage_class: "REDUCED_REDUNDANCY".to_string(),
skip_tls_verify: true,
ca_cert_pem: "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n".to_string(),
health_check_duration: Duration::from_secs(60),
disable_proxy: true,
reset_before_date: Some(OffsetDateTime::now_utc()),
reset_id: "reset-456".to_string(),
total_downtime: Duration::from_secs(1800),
last_online: Some(OffsetDateTime::now_utc()),
online: false,
latency: LatencyStat {
curr: Duration::from_millis(250),
avg: Duration::from_millis(200),
max: Duration::from_millis(500),
},
deployment_id: "deploy-789".to_string(),
edge: true,
edge_sync_before_expiry: false,
offline_count: 10,
};
// Serialize to JSON
let json = serde_json::to_string(&original).expect("Failed to serialize to JSON");
// Deserialize from JSON
let deserialized: BucketTarget = serde_json::from_str(&json).expect("Failed to deserialize from JSON");
// Verify key fields are equal
assert_eq!(original.source_bucket, deserialized.source_bucket);
assert_eq!(original.endpoint, deserialized.endpoint);
assert_eq!(original.target_bucket, deserialized.target_bucket);
assert_eq!(original.secure, deserialized.secure);
assert_eq!(original.target_type, deserialized.target_type);
assert_eq!(original.region, deserialized.region);
assert_eq!(original.bandwidth_limit, deserialized.bandwidth_limit);
assert_eq!(original.replication_sync, deserialized.replication_sync);
assert_eq!(original.skip_tls_verify, deserialized.skip_tls_verify);
assert_eq!(original.ca_cert_pem, deserialized.ca_cert_pem);
assert_eq!(original.health_check_duration, deserialized.health_check_duration);
assert_eq!(original.online, deserialized.online);
assert_eq!(original.edge, deserialized.edge);
assert_eq!(original.offline_count, deserialized.offline_count);
}
#[test]
fn bucket_target_reads_go_nanosecond_durations_defensively() {
// MinIO-written bucket-targets metadata and madmin clients encode
// these fields as Go `time.Duration` nanoseconds; RustFS has always
// persisted seconds. Both encodings must decode to the same interval.
let target: BucketTarget = serde_json::from_value(serde_json::json!({
"endpoint": "localhost:9000",
"targetbucket": "target",
"type": "replication",
"healthCheckDuration": 60_000_000_000u64,
"totalDowntime": 90_000_000_000u64
}))
.expect("nanosecond durations should deserialize");
assert_eq!(target.health_check_duration, Duration::from_secs(60));
assert_eq!(target.total_downtime, Duration::from_secs(90));
// The persisted wire format stays seconds for existing RustFS readers.
let value = serde_json::to_value(&target).expect("target should serialize");
assert_eq!(value["healthCheckDuration"], 60);
assert_eq!(value["totalDowntime"], 90);
}
#[test]
fn test_bucket_target_debug_redacts_credentials() {
let target = BucketTarget {
credentials: Some(Credentials {
access_key: "visible-access-key".to_string(),
secret_key: "must-not-leak-secret".to_string(),
session_token: Some("must-not-leak-token".to_string()),
expiration: None,
}),
..Default::default()
};
let debug = format!("{target:?}");
assert!(debug.contains("visible-access-key"));
assert!(!debug.contains("must-not-leak-secret"));
assert!(!debug.contains("must-not-leak-token"));
assert!(debug.contains(REDACTED_CREDENTIAL));
}
#[test]
fn test_bucket_targets_redacted_credentials_removes_secrets() {
let targets = BucketTargets {
targets: vec![BucketTarget {
credentials: Some(Credentials {
access_key: "visible-access-key".to_string(),
secret_key: "must-not-leak-secret".to_string(),
session_token: Some("must-not-leak-token".to_string()),
expiration: None,
}),
..Default::default()
}],
};
let raw_json = serde_json::to_string(&targets).expect("serialize raw targets");
let redacted_json = serde_json::to_string(&targets.redacted_credentials()).expect("serialize redacted targets");
assert!(raw_json.contains("must-not-leak-secret"));
assert!(raw_json.contains("must-not-leak-token"));
assert!(redacted_json.contains("visible-access-key"));
assert!(!redacted_json.contains("must-not-leak-secret"));
assert!(!redacted_json.contains("must-not-leak-token"));
assert!(redacted_json.contains(r#""secretKey":"""#));
assert!(redacted_json.contains(r#""session_token":null"#));
}
#[test]
fn historical_bucket_target_options_remain_readable() {
let target: BucketTarget = serde_json::from_value(serde_json::json!({
"endpoint": "legacy.example:9000",
"credentials": {
"accessKey": "legacy-access",
"secretKey": "legacy-secret",
"session_token": "legacy-session-token",
"expiration": "2024-12-31T23:59:59Z"
},
"targetbucket": "legacy-bucket",
"api": "s3v2",
"healthCheckDuration": 30,
"disableProxy": true,
"edge": true,
"edgeSyncBeforeExpiry": true,
"type": "replication"
}))
.expect("historical remote target should remain readable");
assert_eq!(target.api, "s3v2");
assert_eq!(target.health_check_duration, Duration::from_secs(30));
assert!(target.disable_proxy);
assert!(target.edge);
assert!(target.edge_sync_before_expiry);
assert_eq!(
target
.credentials
.as_ref()
.and_then(|credentials| credentials.session_token.as_deref()),
Some("legacy-session-token")
);
assert_eq!(
target
.credentials
.as_ref()
.and_then(|credentials| credentials.expiration)
.map(serde_json::to_value)
.transpose()
.expect("expiration should serialize to JSON"),
Some(serde_json::json!("2024-12-31T23:59:59Z"))
);
}
#[test]
fn test_bucket_target_type_json_deserialize() {
// Test BucketTargetType JSON deserialization
let replication_json = r#""replication""#;
let ilm_json = r#""ilm""#;
let replication_type: BucketTargetType =
serde_json::from_str(replication_json).expect("Failed to deserialize replication type");
let ilm_type: BucketTargetType = serde_json::from_str(ilm_json).expect("Failed to deserialize ilm type");
assert_eq!(replication_type, BucketTargetType::ReplicationService);
assert_eq!(ilm_type, BucketTargetType::IlmService);
// Verify type validity
assert!(replication_type.is_valid());
assert!(ilm_type.is_valid());
assert!(!BucketTargetType::None.is_valid());
}
#[test]
fn test_credentials_json_deserialize() {
let json = r#"
{
"accessKey": "AKIAIOSFODNN7EXAMPLE",
"secretKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"session_token": "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT",
"expiration": "2024-12-31T23:59:59Z"
}
"#;
let credentials: Credentials = serde_json::from_str(json).expect("Failed to deserialize credentials");
assert_eq!(credentials.access_key, "AKIAIOSFODNN7EXAMPLE");
assert_eq!(credentials.secret_key, "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
assert_eq!(
credentials.session_token,
Some("AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT".to_string())
);
assert_eq!(
serde_json::to_value(credentials.expiration.expect("expiration should parse"))
.expect("expiration should serialize to JSON"),
serde_json::json!("2024-12-31T23:59:59Z")
);
}
#[test]
fn test_latency_stat_json_deserialize() {
let json = r#"
{
"curr": 50,
"avg": 75,
"max": 200
}
"#;
let latency: LatencyStat = serde_json::from_str(json).expect("Failed to deserialize latency stat");
assert_eq!(latency.curr, Duration::from_millis(50));
assert_eq!(latency.avg, Duration::from_millis(75));
assert_eq!(latency.max, Duration::from_millis(200));
}
#[test]
fn test_bucket_targets_json_deserialize() {
let json = r#"
{
"targets": [
{
"sourcebucket": "bucket1",
"endpoint": "s3.amazonaws.com",
"targetbucket": "target1",
"secure": true,
"path": "/",
"api": "s3v4",
"arn": "arn:aws:s3:::target1",
"type": "replication",
"region": "us-east-1",
"bandwidth_limit": 0,
"replicationSync": false,
"storage_class": "",
"skipTlsVerify": false,
"caCertPem": "",
"healthCheckDuration": 0,
"disableProxy": false,
"resetBeforeDate": null,
"reset_id": "",
"totalDowntime": 0,
"lastOnline": null,
"isOnline": false,
"latency": {
"curr": 0,
"avg": 0,
"max": 0
},
"deployment_id": "",
"edge": false,
"edgeSyncBeforeExpiry": false,
"offlineCount": 0
}
]
}
"#;
let targets: BucketTargets = serde_json::from_str(json).expect("Failed to deserialize bucket targets");
assert_eq!(targets.targets.len(), 1);
assert_eq!(targets.targets[0].source_bucket, "bucket1");
assert_eq!(targets.targets[0].endpoint, "s3.amazonaws.com");
assert_eq!(targets.targets[0].target_bucket, "target1");
assert!(!targets.is_empty());
}
#[test]
fn test_user_provided_json_deserialize() {
// Test the specific JSON provided by the user with missing required fields added
let json = r#"
{
"sourcebucket": "mc-test-bucket-22139",
"endpoint": "localhost:8000",
"credentials": {
"accessKey": "rustfsadmin",
"secretKey": "rustfsadmin",
"expiration": "0001-01-01T00:00:00Z"
},
"targetbucket": "test",
"secure": false,
"path": "auto",
"api": "s3v4",
"type": "replication",
"replicationSync": false,
"skipTlsVerify": true,
"caCertPem": "-----BEGIN CERTIFICATE-----\nMC4x\n-----END CERTIFICATE-----\n",
"healthCheckDuration": 60,
"disableProxy": false,
"resetBeforeDate": "0001-01-01T00:00:00Z",
"totalDowntime": 0,
"lastOnline": "0001-01-01T00:00:00Z",
"isOnline": false,
"latency": {
"curr": 0,
"avg": 0,
"max": 0
},
"deployment_id": "",
"edge": false,
"edgeSyncBeforeExpiry": false,
"offlineCount": 0,
"bandwidth": 107374182400
}
"#;
let target: BucketTarget = serde_json::from_str(json).expect("Failed to deserialize user provided JSON to BucketTarget");
// Verify the deserialized values match the original JSON
assert_eq!(target.source_bucket, "mc-test-bucket-22139");
assert_eq!(target.endpoint, "localhost:8000");
assert_eq!(target.target_bucket, "test");
assert!(!target.secure);
assert_eq!(target.path, "auto");
assert_eq!(target.api, "s3v4");
assert_eq!(target.target_type, BucketTargetType::ReplicationService);
assert!(!target.replication_sync);
assert!(target.skip_tls_verify);
assert_eq!(target.ca_cert_pem, "-----BEGIN CERTIFICATE-----\nMC4x\n-----END CERTIFICATE-----\n");
assert_eq!(target.health_check_duration, Duration::from_secs(60));
assert!(!target.disable_proxy);
assert!(!target.online);
assert!(!target.edge);
assert!(!target.edge_sync_before_expiry);
assert_eq!(target.bandwidth_limit, 107374182400); // bandwidth field mapped to bandwidth_limit
// Verify credentials
assert!(target.credentials.is_some());
let credentials = target.credentials.unwrap();
assert_eq!(credentials.access_key, "rustfsadmin");
assert_eq!(credentials.secret_key, "rustfsadmin");
// Verify latency statistics
assert_eq!(target.latency.curr, Duration::from_millis(0));
assert_eq!(target.latency.avg, Duration::from_millis(0));
assert_eq!(target.latency.max, Duration::from_millis(0));
// Verify time fields parsing (should handle "0001-01-01T00:00:00Z" as None due to being the zero time)
assert!(target.reset_before_date.is_some());
assert!(target.last_online.is_some());
println!("✅ User provided JSON successfully deserialized to BucketTarget");
}
#[test]
fn test_user_provided_json_as_bucket_targets() {
// Test wrapping the user JSON in BucketTargets structure
let json = r#"
{
"targets": [
{
"sourcebucket": "mc-test-bucket-22139",
"endpoint": "localhost:8000",
"credentials": {
"accessKey": "rustfsadmin",
"secretKey": "rustfsadmin",
"expiration": "0001-01-01T00:00:00Z"
},
"targetbucket": "test",
"secure": false,
"path": "auto",
"api": "s3v4",
"arn": "",
"type": "replication",
"region": "",
"replicationSync": false,
"storage_class": "",
"skipTlsVerify": true,
"caCertPem": "-----BEGIN CERTIFICATE-----\nMC4x\n-----END CERTIFICATE-----\n",
"healthCheckDuration": 60,
"disableProxy": false,
"resetBeforeDate": "0001-01-01T00:00:00Z",
"reset_id": "",
"totalDowntime": 0,
"lastOnline": "0001-01-01T00:00:00Z",
"isOnline": false,
"latency": {
"curr": 0,
"avg": 0,
"max": 0
},
"deployment_id": "",
"edge": false,
"edgeSyncBeforeExpiry": false,
"offlineCount": 0,
"bandwidth": 107374182400
}
]
}
"#;
let bucket_targets: BucketTargets =
serde_json::from_str(json).expect("Failed to deserialize user provided JSON to BucketTargets");
assert_eq!(bucket_targets.targets.len(), 1);
assert!(!bucket_targets.is_empty());
let target = &bucket_targets.targets[0];
assert_eq!(target.source_bucket, "mc-test-bucket-22139");
assert_eq!(target.endpoint, "localhost:8000");
assert_eq!(target.target_bucket, "test");
assert_eq!(target.bandwidth_limit, 107374182400);
assert!(target.skip_tls_verify);
assert_eq!(target.ca_cert_pem, "-----BEGIN CERTIFICATE-----\nMC4x\n-----END CERTIFICATE-----\n");
println!("✅ User provided JSON successfully deserialized to BucketTargets");
}
#[test]
fn test_bucket_target_minimal_json_with_defaults() {
// Test that BucketTarget can be deserialized with minimal JSON using defaults
let minimal_json = r#"
{
"sourcebucket": "test-source",
"endpoint": "localhost:9000",
"targetbucket": "test-target"
}
"#;
let target: BucketTarget =
serde_json::from_str(minimal_json).expect("Failed to deserialize minimal JSON to BucketTarget");
// Verify required fields
assert_eq!(target.source_bucket, "test-source");
assert_eq!(target.endpoint, "localhost:9000");
assert_eq!(target.target_bucket, "test-target");
// Verify default values
assert!(!target.secure); // bool default is false
assert_eq!(target.path, ""); // String default is empty
assert_eq!(target.api, ""); // String default is empty
assert_eq!(target.arn, ""); // String default is empty
assert_eq!(target.target_type, BucketTargetType::None); // enum default
assert_eq!(target.region, ""); // String default is empty
assert_eq!(target.bandwidth_limit, 0); // i64 default is 0
assert!(!target.replication_sync); // bool default is false
assert_eq!(target.storage_class, ""); // String default is empty
assert!(!target.skip_tls_verify); // bool default is false
assert_eq!(target.ca_cert_pem, ""); // String default is empty
assert_eq!(target.health_check_duration, Duration::from_secs(0)); // Duration default
assert!(!target.disable_proxy); // bool default is false
assert!(target.reset_before_date.is_none()); // Option default is None
assert_eq!(target.reset_id, ""); // String default is empty
assert_eq!(target.total_downtime, Duration::from_secs(0)); // Duration default
assert!(target.last_online.is_none()); // Option default is None
assert!(!target.online); // bool default is false
assert_eq!(target.latency.curr, Duration::from_millis(0)); // LatencyStat default
assert_eq!(target.latency.avg, Duration::from_millis(0));
assert_eq!(target.latency.max, Duration::from_millis(0));
assert_eq!(target.deployment_id, ""); // String default is empty
assert!(!target.edge); // bool default is false
assert!(!target.edge_sync_before_expiry); // bool default is false
assert_eq!(target.offline_count, 0); // u64 default is 0
assert!(target.credentials.is_none()); // Option default is None
println!("✅ Minimal JSON with defaults successfully deserialized to BucketTarget");
}
#[test]
fn test_bucket_target_empty_json_with_defaults() {
// Test that BucketTarget can be deserialized with completely empty JSON using all defaults
let empty_json = r#"{}"#;
let target: BucketTarget = serde_json::from_str(empty_json).expect("Failed to deserialize empty JSON to BucketTarget");
// Verify all fields use default values
assert_eq!(target.source_bucket, "");
assert_eq!(target.endpoint, "");
assert_eq!(target.target_bucket, "");
assert!(!target.secure);
assert_eq!(target.path, "");
assert_eq!(target.api, "");
assert_eq!(target.arn, "");
assert_eq!(target.target_type, BucketTargetType::None);
assert_eq!(target.region, "");
assert_eq!(target.bandwidth_limit, 0);
assert!(!target.replication_sync);
assert_eq!(target.storage_class, "");
assert!(!target.skip_tls_verify);
assert_eq!(target.ca_cert_pem, "");
assert_eq!(target.health_check_duration, Duration::from_secs(0));
assert!(!target.disable_proxy);
assert!(target.reset_before_date.is_none());
assert_eq!(target.reset_id, "");
assert_eq!(target.total_downtime, Duration::from_secs(0));
assert!(target.last_online.is_none());
assert!(!target.online);
assert_eq!(target.latency.curr, Duration::from_millis(0));
assert_eq!(target.latency.avg, Duration::from_millis(0));
assert_eq!(target.latency.max, Duration::from_millis(0));
assert_eq!(target.deployment_id, "");
assert!(!target.edge);
assert!(!target.edge_sync_before_expiry);
assert_eq!(target.offline_count, 0);
assert!(target.credentials.is_none());
println!("✅ Empty JSON with all defaults successfully deserialized to BucketTarget");
}
#[test]
fn test_original_user_json_with_defaults() {
// Test the original user JSON without extra required fields
let json = r#"
{
"sourcebucket": "mc-test-bucket-22139",
"endpoint": "localhost:8000",
"credentials": {
"accessKey": "rustfsadmin",
"secretKey": "rustfsadmin",
"expiration": "0001-01-01T00:00:00Z"
},
"targetbucket": "test",
"secure": false,
"path": "auto",
"api": "s3v4",
"type": "replication",
"replicationSync": false,
"skipTlsVerify": true,
"caCertPem": "-----BEGIN CERTIFICATE-----\nMC4x\n-----END CERTIFICATE-----\n",
"healthCheckDuration": 60,
"disableProxy": false,
"resetBeforeDate": "0001-01-01T00:00:00Z",
"totalDowntime": 0,
"lastOnline": "0001-01-01T00:00:00Z",
"isOnline": false,
"latency": {
"curr": 0,
"avg": 0,
"max": 0
},
"edge": false,
"edgeSyncBeforeExpiry": false,
"bandwidth": 107374182400
}
"#;
let target: BucketTarget = serde_json::from_str(json).expect("Failed to deserialize original user JSON to BucketTarget");
// Verify the deserialized values
assert_eq!(target.source_bucket, "mc-test-bucket-22139");
assert_eq!(target.endpoint, "localhost:8000");
assert_eq!(target.target_bucket, "test");
assert!(!target.secure);
assert_eq!(target.path, "auto");
assert_eq!(target.api, "s3v4");
assert_eq!(target.target_type, BucketTargetType::ReplicationService);
assert!(!target.replication_sync);
assert!(target.skip_tls_verify);
assert_eq!(target.ca_cert_pem, "-----BEGIN CERTIFICATE-----\nMC4x\n-----END CERTIFICATE-----\n");
assert_eq!(target.health_check_duration, Duration::from_secs(60));
assert!(!target.disable_proxy);
assert!(!target.online);
assert!(!target.edge);
assert!(!target.edge_sync_before_expiry);
assert_eq!(target.bandwidth_limit, 107374182400);
// Fields not specified should use defaults
assert_eq!(target.arn, ""); // default empty string
assert_eq!(target.region, ""); // default empty string
assert_eq!(target.storage_class, ""); // default empty string
assert_eq!(target.reset_id, ""); // default empty string
assert_eq!(target.deployment_id, ""); // default empty string
assert_eq!(target.offline_count, 0); // default u64
// Verify credentials
assert!(target.credentials.is_some());
let credentials = target.credentials.unwrap();
assert_eq!(credentials.access_key, "rustfsadmin");
assert_eq!(credentials.secret_key, "rustfsadmin");
println!("✅ Original user JSON with defaults successfully deserialized to BucketTarget");
}
}