Skip to content

Commit 40ab6e0

Browse files
test: implement fixture-based validation for Enterprise API (#352) (#398)
* test: add fixture-based validation tests for Enterprise API - Created generate-enterprise-fixtures.sh script to capture real API responses - Generated 21 fixtures from live Docker Enterprise cluster - Added fixture_validation_tests.rs with 8 tests validating type definitions - All tests pass except 1 intentionally ignored (found type bug) Findings: - Discovered Module struct type mismatch: bigstore_version_2_support is bool in API but defined as map in struct - Stats endpoints return arrays not objects - All other core types (Cluster, Database, Node, User, License) deserialize correctly This fixture-based approach will catch type mismatches that manual mocks miss, addressing issues #347, #348, #351. Related to #352 * fix: correct Module struct field types for crdb and dependencies The fixture tests revealed that crdb and dependencies fields were incorrectly typed. The API returns empty objects {} when these features are not used, not boolean/array values. Changes: - crdb: Option<bool> → Option<Value> (handles {} empty objects) - dependencies: Option<Vec<Value>> → Option<Value> (handles {} empty objects) All 8 fixture validation tests now pass (previously 7/8 with 1 ignored). This fix was discovered by the fixture-based testing approach - exactly the type of bug it's designed to catch. * feat: add Cloud fixture generation script and documentation Added infrastructure for Cloud API fixture generation with important notes about costs and data sanitization. Changes: - Created scripts/generate-cloud-fixtures.sh for capturing Cloud API responses - Added crates/redis-cloud/tests/fixtures/README.md documenting approach - Explains why Cloud fixtures require careful handling (costs, PII) - Documents current wiremock-based testing approach Note: Cloud fixtures not generated yet as it requires: - Active Cloud account with resources - Manual data sanitization - Billable infrastructure Enterprise fixtures are complete and working (8/8 tests passing).
1 parent 6a78a0b commit 40ab6e0

25 files changed

+370
-715
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Redis Cloud API Fixtures
2+
3+
## Current Status
4+
5+
The Cloud API fixtures directory currently only contains the OpenAPI specification.
6+
7+
## Why No Real Fixtures Yet?
8+
9+
Unlike Enterprise API (which uses Docker for testing), Cloud API fixtures require:
10+
1. A real Cloud account with active resources
11+
2. Billable subscriptions and databases
12+
3. Careful sanitization of account data before committing
13+
14+
## Generating Cloud Fixtures
15+
16+
When you have a Cloud account with test resources, you can generate fixtures:
17+
18+
```bash
19+
export REDIS_CLOUD_API_KEY="your-key"
20+
export REDIS_CLOUD_SECRET_KEY="your-secret"
21+
./scripts/generate-cloud-fixtures.sh
22+
```
23+
24+
**Important**: Review all generated fixtures for sensitive data before committing!
25+
26+
## Current Testing Approach
27+
28+
Cloud API tests currently use wiremock with inline JSON mocks. This approach:
29+
- ✅ Works well for testing
30+
- ✅ No infrastructure required
31+
- ✅ No costs
32+
- ⚠️ Doesn't catch type mismatches from real API responses
33+
34+
## Future Work
35+
36+
To get the full benefits of fixture-based testing for Cloud:
37+
1. Use a test Cloud account with minimal resources
38+
2. Generate fixtures from real API responses
39+
3. Sanitize account/subscription IDs
40+
4. Add validation tests like Enterprise has

crates/redis-enterprise/src/modules.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@ pub struct Module {
3838
/// Redis command used to configure this module
3939
pub config_command: Option<String>,
4040

41-
/// Whether the module supports CRDB (Conflict-free Replicated Database)
42-
pub crdb: Option<bool>,
41+
/// CRDB (Conflict-free Replicated Database) configuration
42+
/// The API returns an empty object {} for modules without CRDB support
43+
pub crdb: Option<Value>,
4344

44-
/// List of other modules this module depends on
45-
pub dependencies: Option<Vec<Value>>,
45+
/// Module dependencies
46+
/// The API returns an empty object {} for modules without dependencies
47+
pub dependencies: Option<Value>,
4648

4749
/// Contact email address of the module author
4850
pub email: Option<String>,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//! Fixture-based validation tests
2+
//!
3+
//! These tests use real API responses captured from a Redis Enterprise cluster
4+
//! to validate that our Rust type definitions accurately match the actual API.
5+
//!
6+
//! ## Fixed Issues
7+
//!
8+
//! - Module struct had type mismatches: crdb and dependencies were incorrectly typed
9+
//! They should be Option<Value> to handle empty objects {} from API
10+
11+
use redis_enterprise::{ClusterInfo, Database, License, Module, Node, User};
12+
use serde_json::Value;
13+
14+
#[test]
15+
fn test_cluster_info_from_fixture() {
16+
let fixture = include_str!("fixtures/cluster.json");
17+
let cluster: ClusterInfo =
18+
serde_json::from_str(fixture).expect("Failed to deserialize cluster info");
19+
assert!(!cluster.name.is_empty());
20+
}
21+
22+
#[test]
23+
fn test_database_list_from_fixture() {
24+
let fixture = include_str!("fixtures/bdbs_list.json");
25+
let databases: Vec<Database> =
26+
serde_json::from_str(fixture).expect("Failed to deserialize database list");
27+
assert!(!databases.is_empty());
28+
}
29+
30+
#[test]
31+
fn test_single_database_from_fixture() {
32+
let fixture = include_str!("fixtures/bdb_single.json");
33+
let _database: Database =
34+
serde_json::from_str(fixture).expect("Failed to deserialize single database");
35+
}
36+
37+
#[test]
38+
fn test_nodes_list_from_fixture() {
39+
let fixture = include_str!("fixtures/nodes_list.json");
40+
let nodes: Vec<Node> = serde_json::from_str(fixture).expect("Failed to deserialize nodes list");
41+
assert!(!nodes.is_empty());
42+
}
43+
44+
#[test]
45+
fn test_users_list_from_fixture() {
46+
let fixture = include_str!("fixtures/users_list.json");
47+
let users: Vec<User> = serde_json::from_str(fixture).expect("Failed to deserialize users list");
48+
assert!(!users.is_empty());
49+
}
50+
51+
#[test]
52+
fn test_modules_list_from_fixture() {
53+
let fixture = include_str!("fixtures/modules_list.json");
54+
let modules: Vec<Module> =
55+
serde_json::from_str(fixture).expect("Failed to deserialize modules list");
56+
57+
assert!(!modules.is_empty(), "Should have modules");
58+
59+
let module = &modules[0];
60+
assert!(!module.uid.is_empty(), "Module should have UID");
61+
assert!(module.module_name.is_some(), "Module should have name");
62+
}
63+
64+
#[test]
65+
fn test_license_from_fixture() {
66+
let fixture = include_str!("fixtures/license.json");
67+
let _license: License = serde_json::from_str(fixture).expect("Failed to deserialize license");
68+
}
69+
70+
#[test]
71+
fn test_stats_from_fixtures() {
72+
// Test cluster stats
73+
let cluster_stats: Value = serde_json::from_str(include_str!("fixtures/cluster_stats.json"))
74+
.expect("Failed to deserialize cluster stats");
75+
assert!(cluster_stats.is_object());
76+
77+
// Test database stats - these are arrays not objects
78+
let db_stats: Value = serde_json::from_str(include_str!("fixtures/bdbs_stats.json"))
79+
.expect("Failed to deserialize database stats");
80+
assert!(db_stats.is_array());
81+
82+
// Test node stats - these are arrays not objects
83+
let node_stats: Value = serde_json::from_str(include_str!("fixtures/nodes_stats.json"))
84+
.expect("Failed to deserialize node stats");
85+
assert!(node_stats.is_array());
86+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"actions":[{"action_uid":"8fc32a11-4688-4057-9237-cd77fccd726f","creation_time":"1760400087","name":"retry_bdb","node_uid":"1","progress":"100","status":"completed","task_id":"8fc32a11-4688-4057-9237-cd77fccd726f"}],"state-machines":[{"action_uid":"04d7b6ea-f377-4d40-9b23-9a8f291988df","heartbeat":1760400082,"name":"SMCreateBDB","object_name":"bdb:1","status":"completed"}]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"bdb_groups":[]}
Lines changed: 1 addition & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -1,185 +1 @@
1-
{
2-
"acl": [],
3-
"active_defrag_cycle_max": 25,
4-
"active_defrag_cycle_min": 1,
5-
"active_defrag_ignore_bytes": "104857600",
6-
"active_defrag_max_scan_fields": 1000,
7-
"active_defrag_threshold_lower": 10,
8-
"active_defrag_threshold_upper": 100,
9-
"activedefrag": "no",
10-
"aof_policy": "appendfsync-every-sec",
11-
"authentication_admin_pass": "6KioqIaX0iEU4BbQ5sMjE471Q5wljEuVz4nwFts3x34KDkbT",
12-
"authentication_redis_pass": "",
13-
"authentication_sasl_pass": "",
14-
"authentication_sasl_uname": "",
15-
"authentication_ssl_client_certs": [],
16-
"authentication_ssl_crdt_certs": [],
17-
"authorized_subjects": [],
18-
"auto_upgrade": false,
19-
"background_op": [
20-
{
21-
"status": "idle"
22-
}
23-
],
24-
"backup": false,
25-
"backup_failure_reason": "",
26-
"backup_history": 0,
27-
"backup_interval": 86400,
28-
"backup_progress": 0.0,
29-
"backup_status": "",
30-
"bigstore": false,
31-
"bigstore_max_ram_ratio": 10,
32-
"bigstore_ram_size": 0,
33-
"bigstore_version": 1,
34-
"client_cert_subject_validation_type": "disabled",
35-
"compare_key_hslot": false,
36-
"conns": 5,
37-
"conns_type": "per-thread",
38-
"crdt": false,
39-
"crdt_causal_consistency": false,
40-
"crdt_config_version": 0,
41-
"crdt_ghost_replica_ids": "",
42-
"crdt_guid": "",
43-
"crdt_modules": "[]",
44-
"crdt_repl_backlog_size": "auto",
45-
"crdt_replica_id": 0,
46-
"crdt_replicas": "",
47-
"crdt_sources": [],
48-
"crdt_sync": "disabled",
49-
"crdt_sync_connection_alarm_timeout_seconds": 0,
50-
"crdt_sync_dist": true,
51-
"crdt_syncer_auto_oom_unlatch": true,
52-
"crdt_xadd_id_uniqueness_mode": "strict",
53-
"created_time": "2025-09-16T21:34:19Z",
54-
"data_internode_encryption": false,
55-
"data_persistence": "disabled",
56-
"dataset_import_sources": [],
57-
"db_conns_auditing": false,
58-
"default_user": true,
59-
"dns_address_master": "",
60-
"dns_suffixes": [],
61-
"email_alerts": false,
62-
"endpoints": [
63-
{
64-
"addr": [
65-
"192.168.32.2"
66-
],
67-
"addr_type": "external",
68-
"dns_name": "redis-11657.docker-cluster",
69-
"oss_cluster_api_preferred_endpoint_type": "ip",
70-
"oss_cluster_api_preferred_ip_type": "internal",
71-
"port": 11657,
72-
"proxy_policy": "single",
73-
"uid": "2:1"
74-
}
75-
],
76-
"eviction_policy": "volatile-lru",
77-
"export_failure_reason": "",
78-
"export_progress": 0.0,
79-
"export_status": "",
80-
"flush_on_fullsync": true,
81-
"generate_text_monitor": false,
82-
"gradual_src_max_sources": 1,
83-
"gradual_src_mode": "disabled",
84-
"gradual_sync_max_shards_per_source": 1,
85-
"gradual_sync_mode": "auto",
86-
"group_uid": 0,
87-
"hash_slots_policy": "16k",
88-
"implicit_shard_key": false,
89-
"import_failure_reason": "",
90-
"import_progress": 0.0,
91-
"import_status": "",
92-
"internal": false,
93-
"last_changed_time": "2025-09-16T21:34:19Z",
94-
"master_persistence": false,
95-
"max_aof_file_size": 322122547200,
96-
"max_aof_load_time": 3600,
97-
"max_client_pipeline": 200,
98-
"max_connections": 0,
99-
"max_pipelined": 2000,
100-
"maxclients": 10000,
101-
"memory_size": 107374182,
102-
"metrics_export_all": false,
103-
"mkms": true,
104-
"module_list": [
105-
{
106-
"module_args": "",
107-
"module_id": "4e6efe7fa848e35cbae142dd1e4564a2",
108-
"module_name": "search",
109-
"semantic_version": "7.99.91"
110-
},
111-
{
112-
"module_args": "",
113-
"module_id": "b14385b09778a335835ee3ae380bbc77",
114-
"module_name": "bf",
115-
"semantic_version": "8.0.1"
116-
},
117-
{
118-
"module_args": "",
119-
"module_id": "259bc961ae0ed766d6294bcdb7af9039",
120-
"module_name": "ReJSON",
121-
"semantic_version": "7.99.3"
122-
},
123-
{
124-
"module_args": "",
125-
"module_id": "891142de155df09c6739543b422178f4",
126-
"module_name": "timeseries",
127-
"semantic_version": "8.0.2"
128-
}
129-
],
130-
"mtls_allow_outdated_certs": false,
131-
"mtls_allow_weak_hashing": false,
132-
"multi_commands_opt": "auto",
133-
"name": "test-db",
134-
"oss_cluster": false,
135-
"oss_cluster_api_preferred_endpoint_type": "ip",
136-
"oss_cluster_api_preferred_ip_type": "internal",
137-
"oss_sharding": false,
138-
"port": 0,
139-
"proxy_policy": "single",
140-
"rack_aware": false,
141-
"recovery_wait_time": -1,
142-
"redis_cluster_enabled": false,
143-
"redis_version": "8.0",
144-
"repl_backlog_size": "auto",
145-
"replica_read_only": false,
146-
"replica_sources": [],
147-
"replica_sync": "disabled",
148-
"replica_sync_connection_alarm_timeout_seconds": 0,
149-
"replica_sync_dist": true,
150-
"replication": false,
151-
"resp3": true,
152-
"roles_permissions": [],
153-
"sched_policy": "cmp",
154-
"shard_block_crossslot_keys": false,
155-
"shard_block_foreign_keys": true,
156-
"shard_key_regex": [],
157-
"shard_list": [
158-
1
159-
],
160-
"sharding": false,
161-
"shards_count": 1,
162-
"shards_placement": "dense",
163-
"skip_import_analyze": "disabled",
164-
"slave_buffer": "auto",
165-
"slave_ha": false,
166-
"slave_ha_priority": 0,
167-
"snapshot_policy": [],
168-
"ssl": false,
169-
"status": "active",
170-
"support_syncer_reconf": true,
171-
"sync": "disabled",
172-
"sync_dedicated_threads": 5,
173-
"sync_sources": [],
174-
"syncer_log_level": "info",
175-
"syncer_mode": "distributed",
176-
"throughput_ingress": 0,
177-
"tls_mode": "disabled",
178-
"topology_epoch": 1,
179-
"tracking_table_max_keys": 1000000,
180-
"type": "redis",
181-
"uid": 2,
182-
"use_selective_flush": true,
183-
"version": "8.0.0",
184-
"wait_command": true
185-
}
1+
{"acl":[],"active_defrag_cycle_max":25,"active_defrag_cycle_min":1,"active_defrag_ignore_bytes":"104857600","active_defrag_max_scan_fields":1000,"active_defrag_threshold_lower":10,"active_defrag_threshold_upper":100,"activedefrag":"no","aof_policy":"appendfsync-every-sec","authentication_admin_pass":"MTRp3pu6VGSojFkfw5v1iiHFnU3NYFvUHtoMDybsZAufxDfL","authentication_redis_pass":"","authentication_sasl_pass":"","authentication_sasl_uname":"","authentication_ssl_client_certs":[],"authentication_ssl_crdt_certs":[],"authorized_subjects":[],"auto_upgrade":false,"background_op":[{"status":"idle"}],"backup":false,"backup_failure_reason":"","backup_history":0,"backup_interval":86400,"backup_progress":0.0,"backup_status":"","bigstore":false,"bigstore_max_ram_ratio":10,"bigstore_ram_size":0,"bigstore_version":1,"client_cert_subject_validation_type":"disabled","compare_key_hslot":false,"conns":5,"conns_type":"per-thread","crdt":false,"crdt_causal_consistency":false,"crdt_config_version":0,"crdt_ghost_replica_ids":"","crdt_guid":"","crdt_modules":"[]","crdt_repl_backlog_size":"auto","crdt_replica_id":0,"crdt_replicas":"","crdt_sources":[],"crdt_sync":"disabled","crdt_sync_connection_alarm_timeout_seconds":0,"crdt_sync_dist":true,"crdt_syncer_auto_oom_unlatch":true,"crdt_xadd_id_uniqueness_mode":"strict","created_time":"2025-10-14T00:01:22Z","data_internode_encryption":false,"data_persistence":"disabled","dataset_import_sources":[],"db_conns_auditing":false,"default_user":true,"dns_address_master":"","dns_suffixes":[],"email_alerts":false,"endpoints":[{"addr":["192.168.16.2"],"addr_type":"external","dns_name":"redis-14968.docker-cluster","oss_cluster_api_preferred_endpoint_type":"ip","oss_cluster_api_preferred_ip_type":"internal","port":14968,"proxy_policy":"single","uid":"1:1"}],"eviction_policy":"volatile-lru","export_failure_reason":"","export_progress":0.0,"export_status":"","flush_on_fullsync":true,"generate_text_monitor":false,"gradual_src_max_sources":1,"gradual_src_mode":"disabled","gradual_sync_max_shards_per_source":1,"gradual_sync_mode":"auto","group_uid":0,"hash_slots_policy":"16k","implicit_shard_key":false,"import_failure_reason":"","import_progress":0.0,"import_status":"","internal":false,"last_changed_time":"2025-10-14T00:01:22Z","master_persistence":false,"max_aof_file_size":322122547200,"max_aof_load_time":3600,"max_client_pipeline":200,"max_connections":0,"max_pipelined":2000,"maxclients":10000,"memory_size":1073741824,"metrics_export_all":false,"mkms":true,"module_list":[{"module_args":"","module_id":"891142de155df09c6739543b422178f4","module_name":"timeseries","semantic_version":"8.0.2"},{"module_args":"","module_id":"4e6efe7fa848e35cbae142dd1e4564a2","module_name":"search","semantic_version":"7.99.91"},{"module_args":"","module_id":"b14385b09778a335835ee3ae380bbc77","module_name":"bf","semantic_version":"8.0.1"},{"module_args":"","module_id":"259bc961ae0ed766d6294bcdb7af9039","module_name":"ReJSON","semantic_version":"7.99.3"}],"mtls_allow_outdated_certs":false,"mtls_allow_weak_hashing":false,"multi_commands_opt":"auto","name":"default-db","oss_cluster":false,"oss_cluster_api_preferred_endpoint_type":"ip","oss_cluster_api_preferred_ip_type":"internal","oss_sharding":false,"port":0,"proxy_policy":"single","rack_aware":false,"recovery_wait_time":-1,"redis_cluster_enabled":false,"redis_version":"8.0","repl_backlog_size":"auto","replica_read_only":false,"replica_sources":[],"replica_sync":"disabled","replica_sync_connection_alarm_timeout_seconds":0,"replica_sync_dist":true,"replication":false,"resp3":true,"roles_permissions":[],"sched_policy":"cmp","shard_block_crossslot_keys":false,"shard_block_foreign_keys":true,"shard_key_regex":[],"shard_list":[1],"sharding":false,"shards_count":1,"shards_placement":"dense","skip_import_analyze":"disabled","slave_buffer":"auto","slave_ha":false,"slave_ha_priority":0,"snapshot_policy":[],"ssl":false,"status":"active","support_syncer_reconf":true,"sync":"disabled","sync_dedicated_threads":5,"sync_sources":[],"syncer_log_level":"info","syncer_mode":"distributed","throughput_ingress":0,"tls_mode":"disabled","topology_epoch":1,"tracking_table_max_keys":1000000,"type":"redis","uid":1,"use_selective_flush":true,"version":"8.0.0","wait_command":true}

0 commit comments

Comments
 (0)