From e1c1d8edcb6478054232c6f8fc05a12068c66679 Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Mon, 29 Jun 2026 07:56:37 +0800 Subject: [PATCH] test: expand snapshot coverage (#4024) --- Cargo.lock | 4 + crates/concurrency/Cargo.toml | 1 + ..._workload_admission_registry_snapshot.snap | 24 ++++ ...d__tests__workload_admission_snapshot.snap | 12 ++ crates/concurrency/src/workload.rs | 27 +++- crates/kms/Cargo.toml | 1 + crates/kms/src/api_types.rs | 120 ++++++++++++++++++ ...sts__kms_cancel_key_deletion_response.snap | 9 ++ ..._types__tests__kms_configure_response.snap | 9 ++ ...types__tests__kms_delete_key_response.snap | 10 ++ ...ts__kms_describe_key_response_missing.snap | 9 ++ ..._types__tests__kms_list_keys_response.snap | 14 ++ ..._api_types__tests__kms_start_response.snap | 9 ++ ...api_types__tests__kms_status_response.snap | 10 ++ ...__api_types__tests__kms_stop_response.snap | 9 ++ ...pi_types__tests__kms_tag_key_response.snap | 9 ++ ..._types__tests__kms_untag_key_response.snap | 9 ++ ...__kms_update_key_description_response.snap | 9 ++ ...sts__kms_vault_transit_config_summary.snap | 27 ++++ crates/storage-api/Cargo.toml | 1 + crates/storage-api/src/capability.rs | 25 +++- ...ty__tests__capability_status_contract.snap | 13 ++ ...gy__tests__topology_snapshot_contract.snap | 83 ++++++++++++ crates/storage-api/src/topology.rs | 23 ++++ rustfs/Cargo.toml | 1 + rustfs/src/admin/plugin_contract.rs | 31 ++++- ...tract__tests__plugin_catalog_contract.snap | 45 +++++++ ..._plugin_catalog_distribution_contract.snap | 36 ++++++ ...ract__tests__plugin_instance_contract.snap | 34 +++++ ...ests__plugin_instance_detail_contract.snap | 27 ++++ 30 files changed, 633 insertions(+), 8 deletions(-) create mode 100644 crates/concurrency/src/snapshots/rustfs_concurrency__workload__tests__workload_admission_registry_snapshot.snap create mode 100644 crates/concurrency/src/snapshots/rustfs_concurrency__workload__tests__workload_admission_snapshot.snap create mode 100644 crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_cancel_key_deletion_response.snap create mode 100644 crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_configure_response.snap create mode 100644 crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_delete_key_response.snap create mode 100644 crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_describe_key_response_missing.snap create mode 100644 crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_list_keys_response.snap create mode 100644 crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_start_response.snap create mode 100644 crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_status_response.snap create mode 100644 crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_stop_response.snap create mode 100644 crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_tag_key_response.snap create mode 100644 crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_untag_key_response.snap create mode 100644 crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_update_key_description_response.snap create mode 100644 crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_vault_transit_config_summary.snap create mode 100644 crates/storage-api/src/snapshots/rustfs_storage_api__capability__tests__capability_status_contract.snap create mode 100644 crates/storage-api/src/snapshots/rustfs_storage_api__topology__tests__topology_snapshot_contract.snap create mode 100644 rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_catalog_contract.snap create mode 100644 rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_catalog_distribution_contract.snap create mode 100644 rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_instance_contract.snap create mode 100644 rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_instance_detail_contract.snap diff --git a/Cargo.lock b/Cargo.lock index 8e35082c1..a16f60ad3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9063,6 +9063,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", + "insta", "jemalloc_pprof", "jiff", "libc", @@ -9213,6 +9214,7 @@ dependencies = [ name = "rustfs-concurrency" version = "1.0.0-beta.8" dependencies = [ + "insta", "rustfs-io-core", "rustfs-io-metrics", "serde", @@ -9612,6 +9614,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "chacha20poly1305", + "insta", "jiff", "md5", "moka", @@ -10048,6 +10051,7 @@ name = "rustfs-storage-api" version = "1.0.0-beta.8" dependencies = [ "async-trait", + "insta", "rustfs-filemeta", "serde", "serde_json", diff --git a/crates/concurrency/Cargo.toml b/crates/concurrency/Cargo.toml index 13bdf1392..0b4e329c8 100644 --- a/crates/concurrency/Cargo.toml +++ b/crates/concurrency/Cargo.toml @@ -27,6 +27,7 @@ thiserror = { workspace = true } tracing = { workspace = true } [dev-dependencies] +insta = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = ["test-util","macros","rt-multi-thread"] } diff --git a/crates/concurrency/src/snapshots/rustfs_concurrency__workload__tests__workload_admission_registry_snapshot.snap b/crates/concurrency/src/snapshots/rustfs_concurrency__workload__tests__workload_admission_registry_snapshot.snap new file mode 100644 index 000000000..27bbc1431 --- /dev/null +++ b/crates/concurrency/src/snapshots/rustfs_concurrency__workload__tests__workload_admission_registry_snapshot.snap @@ -0,0 +1,24 @@ +--- +source: crates/concurrency/src/workload.rs +expression: "serde_json::to_value(®istry).expect(\"workload admission registry should serialize\")" +--- +{ + "entries": [ + { + "active": null, + "class": "scanner", + "limit": null, + "queued": null, + "reason": null, + "state": "unknown" + }, + { + "active": 3, + "class": "foreground_read", + "limit": 8, + "queued": 1, + "reason": null, + "state": "open" + } + ] +} diff --git a/crates/concurrency/src/snapshots/rustfs_concurrency__workload__tests__workload_admission_snapshot.snap b/crates/concurrency/src/snapshots/rustfs_concurrency__workload__tests__workload_admission_snapshot.snap new file mode 100644 index 000000000..8ef60792e --- /dev/null +++ b/crates/concurrency/src/snapshots/rustfs_concurrency__workload__tests__workload_admission_snapshot.snap @@ -0,0 +1,12 @@ +--- +source: crates/concurrency/src/workload.rs +expression: encoded +--- +{ + "active": 0, + "class": "foreground_write", + "limit": 0, + "queued": 0, + "reason": "foreground write admission not exposed", + "state": "disabled" +} diff --git a/crates/concurrency/src/workload.rs b/crates/concurrency/src/workload.rs index b12b66096..f7e2128ea 100644 --- a/crates/concurrency/src/workload.rs +++ b/crates/concurrency/src/workload.rs @@ -181,7 +181,25 @@ pub trait WorkloadAdmissionSnapshotProvider { #[cfg(test)] mod tests { use super::*; - use serde_json::json; + use serde_json::{Value, json}; + + fn sorted_json_value(value: Value) -> Value { + match value { + Value::Array(values) => Value::Array(values.into_iter().map(sorted_json_value).collect()), + Value::Object(object) => { + let mut entries: Vec<_> = object.into_iter().collect(); + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + + Value::Object( + entries + .into_iter() + .map(|(key, value)| (key, sorted_json_value(value))) + .collect(), + ) + } + value => value, + } + } #[test] fn workload_contract_covers_required_classes() { @@ -211,6 +229,10 @@ mod tests { ); let registry = WorkloadAdmissionRegistrySnapshot::new(vec![unknown.clone(), open.clone()]); + insta::assert_json_snapshot!( + "workload_admission_registry_snapshot", + sorted_json_value(serde_json::to_value(®istry).expect("workload admission registry should serialize")) + ); assert_eq!(registry.entries(), &[unknown, open]); assert_eq!( registry.get(WorkloadClass::Scanner).map(|snapshot| snapshot.state), @@ -262,8 +284,9 @@ mod tests { let snapshot = WorkloadAdmissionSnapshot::new(WorkloadClass::ForegroundWrite, AdmissionState::Disabled) .with_counts(Some(0), Some(0), Some(0)) .with_reason("foreground write admission not exposed"); - let encoded = serde_json::to_value(&snapshot).expect("serialize workload admission snapshot"); + let encoded = sorted_json_value(serde_json::to_value(&snapshot).expect("serialize workload admission snapshot")); + insta::assert_json_snapshot!("workload_admission_snapshot", encoded); assert_eq!(encoded["class"], json!("foreground_write")); assert_eq!(encoded["state"], json!("disabled")); diff --git a/crates/kms/Cargo.toml b/crates/kms/Cargo.toml index 59810d014..1a8eb8497 100644 --- a/crates/kms/Cargo.toml +++ b/crates/kms/Cargo.toml @@ -64,6 +64,7 @@ reqwest = { workspace = true } vaultrs = { workspace = true } [dev-dependencies] +insta = { workspace = true } tempfile = { workspace = true } temp-env = { workspace = true } diff --git a/crates/kms/src/api_types.rs b/crates/kms/src/api_types.rs index 5a5ac8531..8fe13827a 100644 --- a/crates/kms/src/api_types.rs +++ b/crates/kms/src/api_types.rs @@ -487,6 +487,29 @@ impl ConfigureKmsRequest { mod tests { use super::*; use crate::config::REDACTED_SECRET; + use serde_json::Value; + + fn stable_json_value(value: impl serde::Serialize) -> Value { + sorted_json_value(serde_json::to_value(value).expect("KMS snapshot value should serialize")) + } + + fn sorted_json_value(value: Value) -> Value { + match value { + Value::Array(values) => Value::Array(values.into_iter().map(sorted_json_value).collect()), + Value::Object(object) => { + let mut entries: Vec<_> = object.into_iter().collect(); + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + + Value::Object( + entries + .into_iter() + .map(|(key, value)| (key, sorted_json_value(value))) + .collect(), + ) + } + value => value, + } + } #[test] fn test_deserialize_vault_kv2_configure_request_accepts_type_aliases() { @@ -650,6 +673,7 @@ mod tests { }; let summary = KmsConfigSummary::from(&config); + insta::assert_json_snapshot!("kms_vault_transit_config_summary", stable_json_value(&summary)); assert_eq!(summary.backend_type, KmsBackend::VaultTransit); assert_eq!(summary.timeout_seconds, 30); assert_eq!(summary.retry_attempts, 3); @@ -777,6 +801,102 @@ mod tests { assert!(rendered.contains("has_master_key") || rendered.contains("has_stored_credentials")); } } + + #[test] + fn kms_management_responses_have_stable_json_shapes() { + insta::assert_json_snapshot!( + "kms_configure_response", + stable_json_value(ConfigureKmsResponse { + success: true, + message: "kms configured".to_string(), + status: KmsServiceStatus::Configured, + }) + ); + insta::assert_json_snapshot!( + "kms_start_response", + stable_json_value(StartKmsResponse { + success: true, + message: "kms started".to_string(), + status: KmsServiceStatus::Running, + }) + ); + insta::assert_json_snapshot!( + "kms_stop_response", + stable_json_value(StopKmsResponse { + success: true, + message: "kms stopped".to_string(), + status: KmsServiceStatus::Configured, + }) + ); + insta::assert_json_snapshot!( + "kms_status_response", + stable_json_value(KmsStatusResponse { + status: KmsServiceStatus::Running, + backend_type: Some(KmsBackend::VaultTransit), + healthy: Some(true), + config_summary: None, + }) + ); + insta::assert_json_snapshot!( + "kms_delete_key_response", + stable_json_value(DeleteKeyResponse { + success: true, + message: "key scheduled for deletion".to_string(), + key_id: "key-a".to_string(), + deletion_date: Some("2026-07-01T00:00:00Z".to_string()), + }) + ); + insta::assert_json_snapshot!( + "kms_list_keys_response", + stable_json_value(ListKeysResponse { + success: true, + message: "keys listed".to_string(), + keys: vec!["key-a".to_string(), "key-b".to_string()], + truncated: true, + next_marker: Some("key-b".to_string()), + }) + ); + insta::assert_json_snapshot!( + "kms_describe_key_response_missing", + stable_json_value(DescribeKeyResponse { + success: false, + message: "key not found".to_string(), + key_metadata: None, + }) + ); + insta::assert_json_snapshot!( + "kms_cancel_key_deletion_response", + stable_json_value(CancelKeyDeletionResponse { + success: true, + message: "key deletion canceled".to_string(), + key_id: "key-a".to_string(), + }) + ); + insta::assert_json_snapshot!( + "kms_update_key_description_response", + stable_json_value(UpdateKeyDescriptionResponse { + success: true, + message: "key description updated".to_string(), + key_id: "key-a".to_string(), + }) + ); + insta::assert_json_snapshot!( + "kms_tag_key_response", + stable_json_value(TagKeyResponse { + success: true, + message: "key tags updated".to_string(), + key_id: "key-a".to_string(), + }) + ); + insta::assert_json_snapshot!( + "kms_untag_key_response", + stable_json_value(UntagKeyResponse { + success: true, + message: "key tags removed".to_string(), + key_id: "key-a".to_string(), + }) + ); + } } // ======================================== diff --git a/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_cancel_key_deletion_response.snap b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_cancel_key_deletion_response.snap new file mode 100644 index 000000000..5daac3fb8 --- /dev/null +++ b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_cancel_key_deletion_response.snap @@ -0,0 +1,9 @@ +--- +source: crates/kms/src/api_types.rs +expression: "serde_json::to_value(CancelKeyDeletionResponse\n{\n success: true, message: \"key deletion canceled\".to_string(), key_id:\n \"key-a\".to_string(),\n}).expect(\"cancel deletion response should serialize\")" +--- +{ + "key_id": "key-a", + "message": "key deletion canceled", + "success": true +} diff --git a/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_configure_response.snap b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_configure_response.snap new file mode 100644 index 000000000..0abd89eb8 --- /dev/null +++ b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_configure_response.snap @@ -0,0 +1,9 @@ +--- +source: crates/kms/src/api_types.rs +expression: "serde_json::to_value(ConfigureKmsResponse\n{\n success: true, message: \"kms configured\".to_string(), status:\n KmsServiceStatus::Configured,\n}).expect(\"configure response should serialize\")" +--- +{ + "message": "kms configured", + "status": "Configured", + "success": true +} diff --git a/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_delete_key_response.snap b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_delete_key_response.snap new file mode 100644 index 000000000..c5862e213 --- /dev/null +++ b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_delete_key_response.snap @@ -0,0 +1,10 @@ +--- +source: crates/kms/src/api_types.rs +expression: "serde_json::to_value(DeleteKeyResponse\n{\n success: true, message: \"key scheduled for deletion\".to_string(), key_id:\n \"key-a\".to_string(), deletion_date:\n Some(\"2026-07-01T00:00:00Z\".to_string()),\n}).expect(\"delete key response should serialize\")" +--- +{ + "deletion_date": "2026-07-01T00:00:00Z", + "key_id": "key-a", + "message": "key scheduled for deletion", + "success": true +} diff --git a/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_describe_key_response_missing.snap b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_describe_key_response_missing.snap new file mode 100644 index 000000000..d8dc89abd --- /dev/null +++ b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_describe_key_response_missing.snap @@ -0,0 +1,9 @@ +--- +source: crates/kms/src/api_types.rs +expression: "serde_json::to_value(DescribeKeyResponse\n{\n success: false, message: \"key not found\".to_string(), key_metadata: None,\n}).expect(\"describe key response should serialize\")" +--- +{ + "key_metadata": null, + "message": "key not found", + "success": false +} diff --git a/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_list_keys_response.snap b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_list_keys_response.snap new file mode 100644 index 000000000..c6afb89a7 --- /dev/null +++ b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_list_keys_response.snap @@ -0,0 +1,14 @@ +--- +source: crates/kms/src/api_types.rs +expression: "serde_json::to_value(ListKeysResponse\n{\n success: true, message: \"keys listed\".to_string(), keys:\n vec![\"key-a\".to_string(), \"key-b\".to_string()], truncated: true,\n next_marker: Some(\"key-b\".to_string()),\n}).expect(\"list keys response should serialize\")" +--- +{ + "keys": [ + "key-a", + "key-b" + ], + "message": "keys listed", + "next_marker": "key-b", + "success": true, + "truncated": true +} diff --git a/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_start_response.snap b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_start_response.snap new file mode 100644 index 000000000..1c0aa9f92 --- /dev/null +++ b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_start_response.snap @@ -0,0 +1,9 @@ +--- +source: crates/kms/src/api_types.rs +expression: "serde_json::to_value(StartKmsResponse\n{\n success: true, message: \"kms started\".to_string(), status:\n KmsServiceStatus::Running,\n}).expect(\"start response should serialize\")" +--- +{ + "message": "kms started", + "status": "Running", + "success": true +} diff --git a/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_status_response.snap b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_status_response.snap new file mode 100644 index 000000000..2052ba155 --- /dev/null +++ b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_status_response.snap @@ -0,0 +1,10 @@ +--- +source: crates/kms/src/api_types.rs +expression: "serde_json::to_value(KmsStatusResponse\n{\n status: KmsServiceStatus::Running, backend_type:\n Some(KmsBackend::VaultTransit), healthy: Some(true), config_summary: None,\n}).expect(\"status response should serialize\")" +--- +{ + "backend_type": "VaultTransit", + "config_summary": null, + "healthy": true, + "status": "Running" +} diff --git a/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_stop_response.snap b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_stop_response.snap new file mode 100644 index 000000000..189fc53bd --- /dev/null +++ b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_stop_response.snap @@ -0,0 +1,9 @@ +--- +source: crates/kms/src/api_types.rs +expression: "serde_json::to_value(StopKmsResponse\n{\n success: true, message: \"kms stopped\".to_string(), status:\n KmsServiceStatus::Configured,\n}).expect(\"stop response should serialize\")" +--- +{ + "message": "kms stopped", + "status": "Configured", + "success": true +} diff --git a/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_tag_key_response.snap b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_tag_key_response.snap new file mode 100644 index 000000000..e0ca44288 --- /dev/null +++ b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_tag_key_response.snap @@ -0,0 +1,9 @@ +--- +source: crates/kms/src/api_types.rs +expression: "serde_json::to_value(TagKeyResponse\n{\n success: true, message: \"key tags updated\".to_string(), key_id:\n \"key-a\".to_string(),\n}).expect(\"tag key response should serialize\")" +--- +{ + "key_id": "key-a", + "message": "key tags updated", + "success": true +} diff --git a/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_untag_key_response.snap b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_untag_key_response.snap new file mode 100644 index 000000000..4c991d515 --- /dev/null +++ b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_untag_key_response.snap @@ -0,0 +1,9 @@ +--- +source: crates/kms/src/api_types.rs +expression: "serde_json::to_value(UntagKeyResponse\n{\n success: true, message: \"key tags removed\".to_string(), key_id:\n \"key-a\".to_string(),\n}).expect(\"untag key response should serialize\")" +--- +{ + "key_id": "key-a", + "message": "key tags removed", + "success": true +} diff --git a/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_update_key_description_response.snap b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_update_key_description_response.snap new file mode 100644 index 000000000..bfd9a2259 --- /dev/null +++ b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_update_key_description_response.snap @@ -0,0 +1,9 @@ +--- +source: crates/kms/src/api_types.rs +expression: "serde_json::to_value(UpdateKeyDescriptionResponse\n{\n success: true, message: \"key description updated\".to_string(), key_id:\n \"key-a\".to_string(),\n}).expect(\"update description response should serialize\")" +--- +{ + "key_id": "key-a", + "message": "key description updated", + "success": true +} diff --git a/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_vault_transit_config_summary.snap b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_vault_transit_config_summary.snap new file mode 100644 index 000000000..7167562a6 --- /dev/null +++ b/crates/kms/src/snapshots/rustfs_kms__api_types__tests__kms_vault_transit_config_summary.snap @@ -0,0 +1,27 @@ +--- +source: crates/kms/src/api_types.rs +expression: "serde_json::to_value(&summary).expect(\"KMS config summary should serialize\")" +--- +{ + "backend_summary": { + "address": "http://127.0.0.1:8200", + "auth_method_type": "token", + "backend_type": "vault-transit", + "has_stored_credentials": true, + "mount_path": "transit", + "namespace": "tenant-a", + "skip_tls_verify": false + }, + "backend_type": "VaultTransit", + "cache_summary": { + "enable_metrics": true, + "max_keys": 1000, + "ttl_seconds": 3600 + }, + "cache_ttl_seconds": 3600, + "default_key_id": "rustfs-master-key", + "enable_cache": true, + "max_cached_keys": 1000, + "retry_attempts": 3, + "timeout_seconds": 30 +} diff --git a/crates/storage-api/Cargo.toml b/crates/storage-api/Cargo.toml index 346e226e2..2218d302a 100644 --- a/crates/storage-api/Cargo.toml +++ b/crates/storage-api/Cargo.toml @@ -38,6 +38,7 @@ time.workspace = true uuid.workspace = true [dev-dependencies] +insta = { workspace = true } serde_json.workspace = true tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/crates/storage-api/src/capability.rs b/crates/storage-api/src/capability.rs index 436a83fee..5ce361ce0 100644 --- a/crates/storage-api/src/capability.rs +++ b/crates/storage-api/src/capability.rs @@ -95,13 +95,36 @@ impl std::error::Error for CapabilitySnapshotError {} #[cfg(test)] mod tests { use super::*; + use serde_json::Value; + + fn sorted_json_value(value: Value) -> Value { + match value { + Value::Array(values) => Value::Array(values.into_iter().map(sorted_json_value).collect()), + Value::Object(object) => { + let mut entries: Vec<_> = object.into_iter().collect(); + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + + Value::Object( + entries + .into_iter() + .map(|(key, value)| (key, sorted_json_value(value))) + .collect(), + ) + } + value => value, + } + } #[test] fn capability_status_serializes_unknown_and_unsupported_states() { let unknown = CapabilityStatus::unknown(); let unsupported = CapabilityStatus::unsupported().with_reason("target does not expose profiler"); - let encoded = serde_json::to_string(&(unknown, unsupported)).expect("serialize capability statuses"); + let encoded = serde_json::to_string(&(&unknown, &unsupported)).expect("serialize capability statuses"); + insta::assert_json_snapshot!( + "capability_status_contract", + sorted_json_value(serde_json::to_value((&unknown, &unsupported)).expect("capability statuses should serialize")) + ); let decoded: (CapabilityStatus, CapabilityStatus) = serde_json::from_str(&encoded).expect("deserialize capability statuses"); diff --git a/crates/storage-api/src/snapshots/rustfs_storage_api__capability__tests__capability_status_contract.snap b/crates/storage-api/src/snapshots/rustfs_storage_api__capability__tests__capability_status_contract.snap new file mode 100644 index 000000000..1bd2b5952 --- /dev/null +++ b/crates/storage-api/src/snapshots/rustfs_storage_api__capability__tests__capability_status_contract.snap @@ -0,0 +1,13 @@ +--- +source: crates/storage-api/src/capability.rs +expression: "serde_json::to_value((&unknown,\n&unsupported)).expect(\"capability statuses should serialize\")" +--- +[ + { + "state": "unknown" + }, + { + "reason": "target does not expose profiler", + "state": "unsupported" + } +] diff --git a/crates/storage-api/src/snapshots/rustfs_storage_api__topology__tests__topology_snapshot_contract.snap b/crates/storage-api/src/snapshots/rustfs_storage_api__topology__tests__topology_snapshot_contract.snap new file mode 100644 index 000000000..643671d18 --- /dev/null +++ b/crates/storage-api/src/snapshots/rustfs_storage_api__topology__tests__topology_snapshot_contract.snap @@ -0,0 +1,83 @@ +--- +source: crates/storage-api/src/topology.rs +expression: "serde_json::to_value(&snapshot).expect(\"topology snapshot should serialize\")" +--- +{ + "capabilities": { + "failure_domain_labels": { + "state": "supported" + }, + "media_labels": { + "state": "supported" + }, + "numa": { + "state": "unknown" + }, + "profiling": { + "state": "supported" + } + }, + "pools": [ + { + "labels": { + "additional": { + "room": "a" + }, + "media": null, + "node": null, + "numa_node": null, + "rack": null, + "zone": null + }, + "pool_id": null, + "pool_index": 0, + "sets": [ + { + "disks": [ + { + "capabilities": { + "failure_domain": { + "state": "unknown" + }, + "media_type": { + "state": "supported" + }, + "numa": { + "reason": "not reported", + "state": "unsupported" + }, + "profiling": { + "state": "disabled" + } + }, + "disk_id": "disk-2", + "disk_index": 2, + "labels": { + "additional": { + "slot": "nvme0" + }, + "media": "ssd", + "node": null, + "numa_node": null, + "rack": null, + "zone": null + }, + "pool_index": 0, + "set_index": 1 + } + ], + "labels": { + "media": null, + "node": null, + "numa_node": null, + "rack": null, + "zone": null + }, + "pool_index": 0, + "set_id": null, + "set_index": 1 + } + ] + } + ] +} diff --git a/crates/storage-api/src/topology.rs b/crates/storage-api/src/topology.rs index ac4d0a7e9..c2e54b598 100644 --- a/crates/storage-api/src/topology.rs +++ b/crates/storage-api/src/topology.rs @@ -95,6 +95,25 @@ pub trait TopologySnapshotProvider: Send + Sync + Debug { mod tests { use super::*; use crate::CapabilityState; + use serde_json::Value; + + fn sorted_json_value(value: Value) -> Value { + match value { + Value::Array(values) => Value::Array(values.into_iter().map(sorted_json_value).collect()), + Value::Object(object) => { + let mut entries: Vec<_> = object.into_iter().collect(); + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + + Value::Object( + entries + .into_iter() + .map(|(key, value)| (key, sorted_json_value(value))) + .collect(), + ) + } + value => value, + } + } #[test] fn topology_snapshot_allows_missing_and_extra_labels() { @@ -141,6 +160,10 @@ mod tests { }"#; let snapshot: TopologySnapshot = serde_json::from_str(raw).expect("deserialize topology snapshot"); + insta::assert_json_snapshot!( + "topology_snapshot_contract", + sorted_json_value(serde_json::to_value(&snapshot).expect("topology snapshot should serialize")) + ); let disk = &snapshot.pools[0].sets[0].disks[0]; assert_eq!(snapshot.pools[0].labels.zone, None); diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index 79906f332..9a3da8c86 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -210,6 +210,7 @@ serial_test = { workspace = true } tempfile = { workspace = true } aws-config = { workspace = true } anyhow = { workspace = true } +insta = { workspace = true } proptest = "1" tokio = { workspace = true, features = ["test-util"] } temp-env = { workspace = true, features = ["async_closure"] } diff --git a/rustfs/src/admin/plugin_contract.rs b/rustfs/src/admin/plugin_contract.rs index b308e260a..fad9d2df1 100644 --- a/rustfs/src/admin/plugin_contract.rs +++ b/rustfs/src/admin/plugin_contract.rs @@ -382,9 +382,26 @@ mod tests { PluginInstanceDetail, PluginInstanceDiagnostic, PluginInstanceDiagnosticCode, PluginInstanceDiagnosticCount, PluginInstanceEntry, PluginInstanceSource, PluginInstancesResponse, PluginRuntimeContract, PluginRuntimeTransport, }; - use serde_json::json; + use serde_json::{Value, json}; use std::collections::HashMap; + fn sorted_json_value(value: Value) -> Value { + match value { + Value::Array(values) => Value::Array(values.into_iter().map(sorted_json_value).collect()), + Value::Object(map) => { + let mut entries = map.into_iter().collect::>(); + entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + Value::Object( + entries + .into_iter() + .map(|(key, value)| (key, sorted_json_value(value))) + .collect(), + ) + } + value => value, + } + } + #[test] fn plugin_catalog_contract_serializes_stable_json_shape() { let response = PluginCatalogResponse { @@ -418,7 +435,8 @@ mod tests { }, }; - let value = serde_json::to_value(response).expect("catalog response should serialize"); + let value = sorted_json_value(serde_json::to_value(response).expect("catalog response should serialize")); + insta::assert_json_snapshot!("plugin_catalog_contract", value); assert_eq!( value, json!({ @@ -481,7 +499,8 @@ mod tests { next_marker: None, }; - let value = serde_json::to_value(response).expect("instance response should serialize"); + let value = sorted_json_value(serde_json::to_value(response).expect("instance response should serialize")); + insta::assert_json_snapshot!("plugin_instance_contract", value); assert_eq!( value, json!({ @@ -534,7 +553,8 @@ mod tests { }], }; - let value = serde_json::to_value(detail).expect("instance detail should serialize"); + let value = sorted_json_value(serde_json::to_value(detail).expect("instance detail should serialize")); + insta::assert_json_snapshot!("plugin_instance_detail_contract", value); assert_eq!( value, json!({ @@ -591,7 +611,8 @@ mod tests { installation: None, }; - let value = serde_json::to_value(entry).expect("catalog entry should serialize"); + let value = sorted_json_value(serde_json::to_value(entry).expect("catalog entry should serialize")); + insta::assert_json_snapshot!("plugin_catalog_distribution_contract", value); assert_eq!(value["distribution"]["artifacts"][0]["artifact_id"], "sidecar-linux-amd64"); assert_eq!(value["distribution"]["artifacts"][0]["target_triple"], "x86_64-unknown-linux-gnu"); assert_eq!( diff --git a/rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_catalog_contract.snap b/rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_catalog_contract.snap new file mode 100644 index 000000000..1f4806c65 --- /dev/null +++ b/rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_catalog_contract.snap @@ -0,0 +1,45 @@ +--- +source: rustfs/src/admin/plugin_contract.rs +expression: value +--- +{ + "admin_discovery": { + "clusterSnapshot": "/rustfs/admin/v4/cluster/snapshot", + "extensionsCatalog": "/rustfs/admin/v4/extensions/catalog", + "runtimeCapabilities": "/rustfs/admin/v4/runtime/capabilities" + }, + "plugins": [ + { + "api_compatibility_version": "rustfs.target-plugin.v1", + "display_name": "Webhook", + "distribution": null, + "domain_configs": [ + { + "domain": "notify", + "subsystem": "notify_webhook", + "valid_fields": [ + "endpoint", + "auth_token" + ] + } + ], + "entrypoint_kind": "builtin", + "packaging": "builtin", + "plugin_id": "builtin:webhook", + "provider": "rustfs", + "runtime_contract": { + "protocol_version": "rustfs.target-runtime.v1", + "transport": "in_process" + }, + "secret_fields": [ + "auth_token" + ], + "supported_domains": [ + "audit", + "notify" + ], + "target_type": "webhook", + "version": "1.0.0" + } + ] +} diff --git a/rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_catalog_distribution_contract.snap b/rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_catalog_distribution_contract.snap new file mode 100644 index 000000000..1cc17038f --- /dev/null +++ b/rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_catalog_distribution_contract.snap @@ -0,0 +1,36 @@ +--- +source: rustfs/src/admin/plugin_contract.rs +expression: value +--- +{ + "api_compatibility_version": "rustfs.target-plugin.v1", + "display_name": "Webhook+", + "distribution": { + "artifacts": [ + { + "artifact_id": "sidecar-linux-amd64", + "digest_sha256": "0123456789abcdef", + "download_uri": "https://plugins.example.test/webhook.tar.zst", + "provenance_uri": "https://plugins.example.test/webhook.tar.zst.intoto.jsonl", + "signature_uri": "https://plugins.example.test/webhook.tar.zst.sig", + "size_bytes": 4096, + "target_triple": "x86_64-unknown-linux-gnu" + } + ] + }, + "domain_configs": [], + "entrypoint_kind": "sidecar", + "packaging": "builtin", + "plugin_id": "external:webhook", + "provider": "example", + "runtime_contract": { + "protocol_version": "rustfs.target-runtime.v1", + "transport": "grpc" + }, + "secret_fields": [], + "supported_domains": [ + "notify" + ], + "target_type": "webhook", + "version": "1.2.3" +} diff --git a/rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_instance_contract.snap b/rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_instance_contract.snap new file mode 100644 index 000000000..a7990e03e --- /dev/null +++ b/rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_instance_contract.snap @@ -0,0 +1,34 @@ +--- +source: rustfs/src/admin/plugin_contract.rs +expression: value +--- +{ + "diagnostic_counts": [ + { + "code": "not_loaded_in_runtime", + "count": 1 + } + ], + "instances": [ + { + "account_id": "primary", + "config": { + "enable": "on", + "endpoint": "https://example.com/hook" + }, + "diagnostic_codes": [ + "not_loaded_in_runtime" + ], + "domain": "notify", + "enabled": true, + "id": "builtin:webhook:notify:primary", + "plugin_id": "builtin:webhook", + "service": "webhook", + "source": "config", + "status": "offline", + "subsystem": "notify_webhook" + } + ], + "next_marker": null, + "truncated": false +} diff --git a/rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_instance_detail_contract.snap b/rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_instance_detail_contract.snap new file mode 100644 index 000000000..0b66abf80 --- /dev/null +++ b/rustfs/src/admin/snapshots/rustfs__admin__plugin_contract__tests__plugin_instance_detail_contract.snap @@ -0,0 +1,27 @@ +--- +source: rustfs/src/admin/plugin_contract.rs +expression: value +--- +{ + "account_id": "primary", + "config": { + "endpoint": "https://example.com/hook" + }, + "diagnostic_codes": [ + "not_loaded_in_runtime" + ], + "diagnostics": [ + { + "code": "not_loaded_in_runtime", + "message": "plugin instance is enabled in config but not currently loaded in runtime" + } + ], + "domain": "notify", + "enabled": true, + "id": "builtin:webhook:notify:primary", + "plugin_id": "builtin:webhook", + "service": "webhook", + "source": "config", + "status": "offline", + "subsystem": "notify_webhook" +}