diff --git a/crates/e2e_test/src/reliant/tiering.rs b/crates/e2e_test/src/reliant/tiering.rs index 3011e8199..49211b233 100644 --- a/crates/e2e_test/src/reliant/tiering.rs +++ b/crates/e2e_test/src/reliant/tiering.rs @@ -647,3 +647,41 @@ async fn test_manual_transition_run_black_box_semantics() -> TestResult { Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_manual_transition_run_contract_no_status_cancel_fields() -> TestResult { + let mut cold = RustFSTestEnvironment::new().await?; + cold.access_key = "manualcontractcoldtieradmin".to_string(); + cold.secret_key = "manualcontractcoldtiersecret".to_string(); + cold.start_rustfs_server_without_cleanup(vec![]).await?; + let cold_client = cold.create_s3_client(); + cold_client.create_bucket().bucket(TIER_BUCKET).send().await?; + + let mut hot = RustFSTestEnvironment::new().await?; + hot.start_rustfs_server_with_env(vec![], &[("RUSTFS_SCANNER_ENABLED", "false"), ("RUSTFS_SCANNER_CYCLE", "3600")]) + .await?; + let hot_client = hot.create_s3_client(); + add_rustfs_tier(&hot, &cold).await?; + + hot_client.create_bucket().bucket("ilm7-manual-contract").send().await?; + + let (status, body) = signed_admin_request( + &hot.url, + Method::POST, + "/rustfs/admin/v3/ilm/transition/run?bucket=ilm7-manual-contract", + None, + &hot.access_key, + &hot.secret_key, + ) + .await?; + assert_eq!(status, reqwest::StatusCode::OK, "manual transition contract call should still be OK"); + let response: serde_json::Value = serde_json::from_str(&body)?; + + assert!(response.get("state").is_some(), "response should include state field"); + assert!(response.get("job_id").is_none() || response.get("job_id").is_some_and(|v| v.is_null())); + assert!(response.get("status").is_none() || response.get("status").is_some_and(|v| v.is_null())); + assert!(response.get("status_endpoint").is_none() || response.get("status_endpoint").is_some_and(|v| v.is_null())); + assert!(response.get("cancel").is_none() || response.get("cancel").is_some_and(|v| v.is_null())); + + Ok(()) +} diff --git a/crates/ecstore/src/cluster/rpc/remote_disk.rs b/crates/ecstore/src/cluster/rpc/remote_disk.rs index df8f8d076..642aca597 100644 --- a/crates/ecstore/src/cluster/rpc/remote_disk.rs +++ b/crates/ecstore/src/cluster/rpc/remote_disk.rs @@ -3131,6 +3131,26 @@ mod tests { assert!(err.contains("server:9000"), "unexpected error: {err}"); } + #[test] + fn read_multiple_response_decode_reports_corrupt_json_item() { + let endpoint = sample_remote_endpoint(); + let response = ReadMultipleResponse { + success: true, + read_multiple_resps: vec![ + serde_json::to_string(&sample_read_multiple_resp("good", b"ok")).expect("json response should encode"), + "{not-json".to_string(), + ], + read_multiple_resps_bin: Vec::new(), + error: None, + }; + + let err = decode_read_multiple_response_items(response, &endpoint).expect_err("corrupt json item should fail"); + let err = err.to_string(); + + assert!(err.contains("ReadMultipleResp json item 1"), "unexpected error: {err}"); + assert!(err.contains("server:9000"), "unexpected error: {err}"); + } + fn sample_batch_read_version_resp(index: usize, path: &str, success: bool) -> BatchReadVersionResp { let mut file_info = FileInfo::new(path, 1, 0); file_info.erasure.index = 1; @@ -3210,6 +3230,27 @@ mod tests { assert!(err.contains("server:9000"), "unexpected error: {err}"); } + #[test] + fn batch_read_version_response_decode_reports_corrupt_json_item() { + let endpoint = sample_remote_endpoint(); + let response = BatchReadVersionResponse { + success: true, + batch_read_version_resps: vec![ + serde_json::to_string(&sample_batch_read_version_resp(0, "ok", false)).expect("json should encode"), + "{not-json".to_string(), + ], + batch_read_version_resps_bin: Vec::new(), + error: None, + }; + + let err = decode_batch_read_version_response_items(response, &endpoint) + .expect_err("corrupt json item should fail") + .to_string(); + + assert!(err.contains("BatchReadVersionResp json item 1"), "unexpected error: {err}"); + assert!(err.contains("server:9000"), "unexpected error: {err}"); + } + #[test] fn batch_metadata_rpc_mode_defaults_to_off_and_parses_supported_values() { assert_eq!(parse_batch_metadata_rpc_mode(""), BatchMetadataRpcMode::Off); diff --git a/rustfs/src/storage/rpc/node_service/disk.rs b/rustfs/src/storage/rpc/node_service/disk.rs index 44990045a..31f1da99f 100644 --- a/rustfs/src/storage/rpc/node_service/disk.rs +++ b/rustfs/src/storage/rpc/node_service/disk.rs @@ -1309,6 +1309,14 @@ mod tests { assert!(err.to_string().contains("decode SamplePayload msgpack failed"), "unexpected error: {err}"); } + #[test] + fn decode_msgpack_or_json_reports_corrupt_json_item_when_msgpack_absent() { + let err = decode_msgpack_or_json::(&[], "{not-json", "SamplePayload") + .expect_err("corrupt json item should fail in fallback branch"); + + assert!(err.to_string().contains("decode SamplePayload failed"), "unexpected error: {err}"); + } + #[test] fn decode_msgpack_or_json_accepts_named_msgpack_and_legacy_json() { let payload = SamplePayload {