Skip to content

Commit 62056c8

Browse files
fix(enterprise): fix deserialization errors in cluster, user, and license structs (#327)
* fix: remove non-existent workflow command from docker-compose.yml The workflow commands are not yet implemented in the CLI (they're commented out). Updated docker-compose.yml to remove the auto-init container and added manual initialization instructions instead. * fix: use v0.5.0 tag instead of latest in docker-compose.yml The workflow init-cluster command IS implemented and working. The issue was using :latest tag which might not have the latest code. Using explicit v0.5.0 tag ensures the workflow command is available. * fix: init-cluster workflow to work without authentication for bootstrap - Bootstrap API doesn't require authentication initially - Modified init-cluster workflow to create unauthenticated client for bootstrap - Added ARM64 Redis Enterprise image requirement to CLAUDE.md - Fixed docker-compose.yml to build from local Dockerfile for testing - Verified workflow successfully initializes cluster with admin user and database * fix: disable init container until v0.5.1 release The init-cluster workflow fix for unauthenticated bootstrap is not in v0.5.0, so the docker-compose workflow won't work until the next release. Commented out the init container with instructions for manual initialization and notes to re-enable after v0.5.1 is published. * fix(enterprise): fix deserialization errors in cluster, user, and license structs - Fix ClusterInfo struct field types to match actual API responses: - sentinel_cipher_suites: String -> Vec<String> - sentinel_cipher_suites_tls_1_3: Vec<Value> -> String - password_complexity: Value -> bool - mtls_certificate_authentication: String -> bool - upgrade_mode: String -> bool - Fix User struct to match actual API: - Remove non-existent 'username' field - Make 'email' required (it's the user identifier) - Add 'name' field for display name - Add missing fields: auth_method, certificate_subject_line, role_uids - Fix License struct: - Remove required 'license_key' field (doesn't exist) - Keep 'key' and 'license' as optional fields - Update mock tests to use realistic API responses - Add comprehensive cluster info test with actual API data - Fix rbac_impl.rs to use email instead of username Fixes #316, #317, #323, #324, #325
1 parent c911cb8 commit 62056c8

File tree

8 files changed

+161
-31
lines changed

8 files changed

+161
-31
lines changed

crates/redis-enterprise/src/cluster.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ pub struct ClusterInfo {
254254
pub mtls_authorized_subjects: Option<Vec<String>>,
255255

256256
/// Certificate authentication mode for mutual TLS
257-
pub mtls_certificate_authentication: Option<String>,
257+
pub mtls_certificate_authentication: Option<bool>,
258258

259259
/// Validation type for MTLS client certificate subjects
260260
pub mtls_client_cert_subject_validation_type: Option<String>,
@@ -266,7 +266,7 @@ pub struct ClusterInfo {
266266
pub options_method_forbidden: Option<bool>,
267267

268268
/// Requirements for password complexity
269-
pub password_complexity: Option<Value>,
269+
pub password_complexity: Option<bool>,
270270

271271
/// Duration in seconds before passwords expire
272272
pub password_expiration_duration: Option<u32>,
@@ -290,10 +290,10 @@ pub struct ClusterInfo {
290290
pub s3_certificate_verification: Option<bool>,
291291

292292
/// Cipher suites for sentinel TLS connections
293-
pub sentinel_cipher_suites: Option<String>,
293+
pub sentinel_cipher_suites: Option<Vec<String>>,
294294

295295
/// Cipher suites for sentinel TLS 1.3 connections
296-
pub sentinel_cipher_suites_tls_1_3: Option<Vec<Value>>,
296+
pub sentinel_cipher_suites_tls_1_3: Option<String>,
297297

298298
/// TLS mode for sentinel connections
299299
pub sentinel_tls_mode: Option<String>,
@@ -329,7 +329,7 @@ pub struct ClusterInfo {
329329
pub upgrade_in_progress: Option<bool>,
330330

331331
/// Current upgrade mode for the cluster
332-
pub upgrade_mode: Option<String>,
332+
pub upgrade_mode: Option<bool>,
333333

334334
/// Use external IPv6
335335
pub use_external_ipv6: Option<bool>,

crates/redis-enterprise/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@
194194
//! // List all users
195195
//! let users = handler.list().await?;
196196
//! for user in users {
197-
//! println!("User: {} ({})", user.username, user.role);
197+
//! println!("User: {} ({})", user.email, user.role);
198198
//! }
199199
//!
200200
//! // Create a new user

crates/redis-enterprise/src/license.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ use typed_builder::TypedBuilder;
1414
/// License information
1515
#[derive(Debug, Clone, Serialize, Deserialize)]
1616
pub struct License {
17-
/// License key string
18-
pub license_key: String,
19-
20-
/// Key field (some endpoints may return this instead of license_key)
17+
/// License key field
2118
#[serde(skip_serializing_if = "Option::is_none")]
2219
pub key: Option<String>,
2320

crates/redis-enterprise/src/users.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,25 @@ use typed_builder::TypedBuilder;
1919
#[derive(Debug, Clone, Serialize, Deserialize)]
2020
pub struct User {
2121
pub uid: u32,
22-
pub username: String,
23-
pub email: Option<String>,
22+
/// User's email address (used as login identifier)
23+
pub email: String,
24+
/// User's display name
25+
pub name: Option<String>,
26+
/// User's role
2427
pub role: String,
28+
/// User status (e.g., "active")
2529
pub status: Option<String>,
30+
/// Authentication method (e.g., "regular")
31+
pub auth_method: Option<String>,
32+
/// Certificate subject line for certificate auth
33+
pub certificate_subject_line: Option<String>,
34+
/// Password issue date
2635
pub password_issue_date: Option<String>,
36+
/// Whether user receives email alerts
2737
pub email_alerts: Option<bool>,
38+
/// List of role UIDs
39+
pub role_uids: Option<Vec<u32>>,
40+
/// Database IDs for alerts
2841
pub bdbs: Option<Vec<u32>>,
2942
/// Alert for audit database connections
3043
pub alert_audit_db_conns: Option<bool>,
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//! Test for cluster info endpoint with realistic data
2+
3+
use redis_enterprise::{ClusterHandler, EnterpriseClient};
4+
use serde_json::json;
5+
use wiremock::matchers::{basic_auth, method, path};
6+
use wiremock::{Mock, MockServer, ResponseTemplate};
7+
8+
fn success_response(body: serde_json::Value) -> ResponseTemplate {
9+
ResponseTemplate::new(200).set_body_json(body)
10+
}
11+
12+
#[tokio::test]
13+
async fn test_cluster_info() {
14+
let mock_server = MockServer::start().await;
15+
16+
// Mock response focusing on fields that were causing deserialization issues
17+
let cluster_response = json!({
18+
"name": "cluster.local",
19+
"created_time": "2025-09-15T21:22:00Z",
20+
"cnm_http_port": 8080,
21+
"cnm_https_port": 9443,
22+
"email_alerts": false,
23+
"rack_aware": false,
24+
"bigstore_driver": "speedb",
25+
// Fields that had type mismatches
26+
"password_complexity": false, // Was Option<Value>, now Option<bool>
27+
"upgrade_mode": false, // Was Option<String>, now Option<bool>
28+
"mtls_certificate_authentication": false, // Was Option<String>, now Option<bool>
29+
"sentinel_cipher_suites": [], // Was Option<String>, now Option<Vec<String>>
30+
"sentinel_cipher_suites_tls_1_3": "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256", // Was Option<Vec<Value>>, now Option<String>
31+
"data_cipher_suites_tls_1_3": [] // Array type
32+
});
33+
34+
Mock::given(method("GET"))
35+
.and(path("/v1/cluster"))
36+
.and(basic_auth("admin", "password"))
37+
.respond_with(success_response(cluster_response))
38+
.mount(&mock_server)
39+
.await;
40+
41+
let client = EnterpriseClient::builder()
42+
.base_url(mock_server.uri())
43+
.username("admin")
44+
.password("password")
45+
.build()
46+
.unwrap();
47+
48+
let handler = ClusterHandler::new(client);
49+
let result = handler.info().await;
50+
51+
assert!(result.is_ok());
52+
let info = result.unwrap();
53+
54+
// Verify key fields
55+
assert_eq!(info.name, "cluster.local");
56+
assert_eq!(info.cnm_http_port, Some(8080));
57+
assert_eq!(info.cnm_https_port, Some(9443));
58+
assert_eq!(info.email_alerts, Some(false));
59+
assert_eq!(info.rack_aware, Some(false));
60+
assert_eq!(info.bigstore_driver, Some("speedb".to_string()));
61+
62+
// Verify fields that were causing deserialization issues
63+
assert_eq!(info.password_complexity, Some(false));
64+
assert_eq!(info.upgrade_mode, Some(false));
65+
assert_eq!(info.mtls_certificate_authentication, Some(false));
66+
67+
// Verify array fields
68+
assert_eq!(info.sentinel_cipher_suites, Some(vec![]));
69+
assert_eq!(
70+
info.sentinel_cipher_suites_tls_1_3,
71+
Some(
72+
"TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"
73+
.to_string()
74+
)
75+
);
76+
assert_eq!(info.data_cipher_suites_tls_1_3, Some(vec![]));
77+
}
78+
79+
#[tokio::test]
80+
async fn test_cluster_info_minimal() {
81+
let mock_server = MockServer::start().await;
82+
83+
// Test with minimal required fields
84+
Mock::given(method("GET"))
85+
.and(path("/v1/cluster"))
86+
.and(basic_auth("admin", "password"))
87+
.respond_with(success_response(json!({
88+
"name": "minimal-cluster"
89+
})))
90+
.mount(&mock_server)
91+
.await;
92+
93+
let client = EnterpriseClient::builder()
94+
.base_url(mock_server.uri())
95+
.username("admin")
96+
.password("password")
97+
.build()
98+
.unwrap();
99+
100+
let handler = ClusterHandler::new(client);
101+
let result = handler.info().await;
102+
103+
assert!(result.is_ok());
104+
let info = result.unwrap();
105+
assert_eq!(info.name, "minimal-cluster");
106+
107+
// All optional fields should be None
108+
assert!(info.cnm_http_port.is_none());
109+
assert!(info.password_complexity.is_none());
110+
assert!(info.sentinel_cipher_suites.is_none());
111+
}

crates/redis-enterprise/tests/license_tests.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ fn success_response(body: serde_json::Value) -> ResponseTemplate {
1212

1313
fn valid_license() -> serde_json::Value {
1414
json!({
15-
"license_key": "lic-123-456-789",
15+
"key": "lic-123-456-789",
1616
"type": "production",
1717
"expired": false,
1818
"expiration_date": "2025-12-31T23:59:59Z",
@@ -25,7 +25,7 @@ fn valid_license() -> serde_json::Value {
2525

2626
fn expired_license() -> serde_json::Value {
2727
json!({
28-
"license_key": "lic-expired-123",
28+
"key": "lic-expired-123",
2929
"type": "trial",
3030
"expired": true,
3131
"expiration_date": "2023-01-01T00:00:00Z",
@@ -49,7 +49,7 @@ fn license_usage() -> serde_json::Value {
4949

5050
fn minimal_license() -> serde_json::Value {
5151
json!({
52-
"license_key": "lic-minimal-789",
52+
"key": "lic-minimal-789",
5353
"type": "dev",
5454
"expired": false
5555
})
@@ -78,7 +78,7 @@ async fn test_license_get() {
7878

7979
assert!(result.is_ok());
8080
let license = result.unwrap();
81-
assert_eq!(license.license_key, "lic-123-456-789");
81+
assert_eq!(license.key.unwrap(), "lic-123-456-789");
8282
assert_eq!(license.type_, Some("production".to_string()));
8383
assert!(!license.expired);
8484
assert_eq!(
@@ -121,7 +121,7 @@ async fn test_license_get_expired() {
121121

122122
assert!(result.is_ok());
123123
let license = result.unwrap();
124-
assert_eq!(license.license_key, "lic-expired-123");
124+
assert_eq!(license.key.unwrap(), "lic-expired-123");
125125
assert_eq!(license.type_, Some("trial".to_string()));
126126
assert!(license.expired);
127127
assert_eq!(
@@ -155,7 +155,7 @@ async fn test_license_get_minimal() {
155155

156156
assert!(result.is_ok());
157157
let license = result.unwrap();
158-
assert_eq!(license.license_key, "lic-minimal-789");
158+
assert_eq!(license.key.unwrap(), "lic-minimal-789");
159159
assert_eq!(license.type_, Some("dev".to_string()));
160160
assert!(!license.expired);
161161
assert!(license.expiration_date.is_none());
@@ -178,7 +178,7 @@ async fn test_license_update() {
178178
.and(basic_auth("admin", "password"))
179179
.and(body_json(&update_request))
180180
.respond_with(success_response(json!({
181-
"license_key": "new-license-key-12345",
181+
"key": "new-license-key-12345",
182182
"type": "production",
183183
"expired": false,
184184
"expiration_date": "2026-12-31T23:59:59Z",
@@ -202,7 +202,7 @@ async fn test_license_update() {
202202

203203
assert!(result.is_ok());
204204
let license = result.unwrap();
205-
assert_eq!(license.license_key, "new-license-key-12345");
205+
assert_eq!(license.key.unwrap(), "new-license-key-12345");
206206
assert_eq!(license.type_, Some("production".to_string()));
207207
assert!(!license.expired);
208208
assert_eq!(license.shards_limit, Some(200));
@@ -298,7 +298,7 @@ async fn test_license_validate_valid() {
298298
.and(basic_auth("admin", "password"))
299299
.and(body_json(&validate_request))
300300
.respond_with(success_response(json!({
301-
"license_key": "valid-license-to-validate",
301+
"key": "valid-license-to-validate",
302302
"type": "production",
303303
"expired": false,
304304
"expiration_date": "2025-06-30T23:59:59Z",
@@ -322,7 +322,7 @@ async fn test_license_validate_valid() {
322322

323323
assert!(result.is_ok());
324324
let license = result.unwrap();
325-
assert_eq!(license.license_key, "valid-license-to-validate");
325+
assert_eq!(license.key.unwrap(), "valid-license-to-validate");
326326
assert_eq!(license.type_, Some("production".to_string()));
327327
assert!(!license.expired);
328328
assert_eq!(license.shards_limit, Some(50));
@@ -369,7 +369,7 @@ async fn test_license_cluster_license() {
369369
.and(path("/v1/cluster/license"))
370370
.and(basic_auth("admin", "password"))
371371
.respond_with(success_response(json!({
372-
"license_key": "cluster-license-789",
372+
"key": "cluster-license-789",
373373
"type": "enterprise",
374374
"expired": false,
375375
"expiration_date": "2024-12-31T23:59:59Z",
@@ -393,7 +393,7 @@ async fn test_license_cluster_license() {
393393

394394
assert!(result.is_ok());
395395
let license = result.unwrap();
396-
assert_eq!(license.license_key, "cluster-license-789");
396+
assert_eq!(license.key.unwrap(), "cluster-license-789");
397397
assert_eq!(license.type_, Some("enterprise".to_string()));
398398
assert!(!license.expired);
399399
assert_eq!(license.shards_limit, Some(1000));

crates/redis-enterprise/tests/user_tests.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@ fn no_content_response() -> ResponseTemplate {
2121
fn test_user() -> serde_json::Value {
2222
json!({
2323
"uid": 1,
24-
"username": "test-user",
2524
"email": "[email protected]",
25+
"name": "Test User",
2626
"role": "admin",
27-
"status": "active"
27+
"status": "active",
28+
"auth_method": "regular",
29+
"certificate_subject_line": "",
30+
"email_alerts": true,
31+
"password_issue_date": "2025-01-01T00:00:00Z",
32+
"role_uids": [1]
2833
})
2934
}
3035

@@ -39,10 +44,14 @@ async fn test_user_list() {
3944
test_user(),
4045
{
4146
"uid": 2,
42-
"username": "user-2",
4347
"email": "[email protected]",
48+
"name": "User 2",
4449
"role": "viewer",
45-
"status": "active"
50+
"status": "active",
51+
"auth_method": "regular",
52+
"certificate_subject_line": "",
53+
"email_alerts": false,
54+
"role_uids": [2]
4655
}
4756
])))
4857
.mount(&mock_server)
@@ -87,7 +96,7 @@ async fn test_user_get() {
8796
assert!(result.is_ok());
8897
let user = result.unwrap();
8998
assert_eq!(user.uid, 1);
90-
assert_eq!(user.username, "test-user");
99+
assert_eq!(user.email, "test@example.com");
91100
}
92101

93102
#[tokio::test]
@@ -119,7 +128,7 @@ async fn test_user_create() {
119128
assert!(result.is_ok());
120129
let user = result.unwrap();
121130
assert_eq!(user.uid, 1);
122-
assert_eq!(user.username, "test-user");
131+
assert_eq!(user.email, "test@example.com");
123132
}
124133

125134
#[tokio::test]

crates/redisctl/src/commands/enterprise/rbac_impl.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ pub async fn reset_user_password(
129129

130130
// Get the user to get their email
131131
let user = handler.get(id).await?;
132-
let email = user.email.unwrap_or(user.username);
132+
let email = user.email.clone();
133133

134134
let new_password = if let Some(pwd) = password {
135135
pwd.to_string()

0 commit comments

Comments
 (0)