stemedb/crates/stemedb-admin/tests/integration_test.rs
jml ae7d2ed8b1 feat(admin): implement stemedb-admin CLI with API contract fixes
Complete implementation of P5.5 Cluster Management Tooling with production-ready
stemedb-admin CLI tool for remote cluster operations.

## Features Implemented

### CLI Tool (1,200 lines)
- Cluster commands: health, status
- Node commands: list, info, shards
- Shard commands: list, info, replicas
- Debug commands: export
- Output formats: table (colored) and JSON
- Remote gateway connection via HTTP

### API Contract Fixes
- Handle gateway wrapper objects ({"ranges": [...]})
- Convert string shard IDs ("shard_0") to integers
- Normalize different endpoint formats (/v1/admin/ranges vs /v1/shards/:id)
- Custom deserializer for flexible ID formats

### Code Quality
- Zero clippy warnings (strict mode)
- Zero panics (unwrap/expect forbidden)
- 12 integration tests (all passing)
- Comprehensive error handling with anyhow
- Structured logging with tracing

### Documentation (7,000+ words)
- Node lifecycle operations guide (38 sections)
- CLI installation and usage guide (61 sections)
- Add/remove/replace node procedures
- Troubleshooting guides

## Testing
- Automated tests: 23/23 passing
- Cluster tests: 8/8 passing
- All commands verified against live 3-node cluster

## Production Readiness
- Code: Production-grade (0 warnings, defensive error handling)
- Tests: 31/31 passing (100%)
- Documentation: Complete operations guides
- Status: Ready for staging deployment

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 08:23:36 +00:00

192 lines
6.0 KiB
Rust

use stemedb_admin::{output, types};
#[test]
fn test_output_format_parsing() {
use std::str::FromStr;
use stemedb_admin::output::OutputFormat;
let table = OutputFormat::from_str("table").expect("Failed to parse 'table'");
assert_eq!(table, OutputFormat::Table);
let json = OutputFormat::from_str("json").expect("Failed to parse 'json'");
assert_eq!(json, OutputFormat::Json);
let invalid = OutputFormat::from_str("invalid");
assert!(invalid.is_err());
}
#[test]
fn test_cluster_status_json_serialization() {
let status = types::ClusterStatusResponse {
node_count: 3,
shard_count: 32,
meta_version: 158,
nodes: vec![
types::NodeStatusInfo {
id: "a3f2b1c4".to_string(),
state: "Alive".to_string(),
shards: vec![1, 2, 3],
},
types::NodeStatusInfo {
id: "7d8e9f0a".to_string(),
state: "Dead".to_string(),
shards: vec![4, 5],
},
],
};
let json = output::format_json(&status).expect("Failed to format as JSON");
assert!(json.contains("\"node_count\": 3"));
assert!(json.contains("\"shard_count\": 32"));
assert!(json.contains("\"a3f2b1c4\""));
// Verify it's valid JSON
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON produced");
assert_eq!(parsed["node_count"], 3);
assert_eq!(parsed["shard_count"], 32);
}
#[test]
fn test_health_response_json_serialization() {
let health = types::HealthResponse { healthy: true, reachable_nodes: 3, joined: true };
let json = output::format_json(&health).expect("Failed to format as JSON");
assert!(json.contains("\"healthy\": true"));
assert!(json.contains("\"reachable_nodes\": 3"));
// Verify it's valid JSON
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON produced");
assert_eq!(parsed["healthy"], true);
assert_eq!(parsed["reachable_nodes"], 3);
}
#[test]
fn test_range_info_json_serialization() {
let range = types::RangeInfoDto {
range_id: 5,
start_key: "".to_string(),
end_key: "m".to_string(),
size_bytes: 1_048_576, // 1 MB
assertion_count: 1000,
leader_node: "a3f2b1c4".to_string(),
replica_nodes: vec!["7d8e9f0a".to_string(), "b1c2d3e4".to_string()],
generation: 10,
};
let json = output::format_json(&range).expect("Failed to format as JSON");
assert!(json.contains("\"range_id\": 5"));
assert!(json.contains("\"assertion_count\": 1000"));
// Verify it's valid JSON
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON produced");
assert_eq!(parsed["range_id"], 5);
assert_eq!(parsed["assertion_count"], 1000);
}
#[test]
fn test_nodes_table_formatting() {
let nodes = vec![
types::NodeStatusInfo {
id: "a3f2b1c4".to_string(),
state: "Alive".to_string(),
shards: vec![1, 2, 3],
},
types::NodeStatusInfo {
id: "7d8e9f0a".to_string(),
state: "Dead".to_string(),
shards: vec![4, 5, 6, 7, 8, 9],
},
];
let table = output::format_nodes_table(&nodes);
assert!(table.contains("a3f2b1c4"));
assert!(table.contains("7d8e9f0a"));
assert!(table.contains("Alive") || table.contains("Dead")); // Color codes may be present
}
#[test]
fn test_shards_table_formatting() {
let shards = vec![
types::RangeInfoDto {
range_id: 1,
start_key: "".to_string(),
end_key: "m".to_string(),
size_bytes: 2_097_152, // 2 MB
assertion_count: 5000,
leader_node: "a3f2b1c4".to_string(),
replica_nodes: vec!["7d8e9f0a".to_string()],
generation: 5,
},
types::RangeInfoDto {
range_id: 2,
start_key: "m".to_string(),
end_key: "z".to_string(),
size_bytes: 1_048_576, // 1 MB
assertion_count: 2500,
leader_node: "7d8e9f0a".to_string(),
replica_nodes: vec!["a3f2b1c4".to_string()],
generation: 5,
},
];
let table = output::format_shards_table(&shards);
assert!(table.contains("a3f2b1c4"));
assert!(table.contains("7d8e9f0a"));
}
#[test]
fn test_cluster_summary_formatting() {
let status = types::ClusterStatusResponse {
node_count: 3,
shard_count: 32,
meta_version: 158,
nodes: vec![types::NodeStatusInfo {
id: "a3f2b1c4".to_string(),
state: "Alive".to_string(),
shards: vec![1, 2, 3],
}],
};
let summary = output::format_cluster_summary(&status);
assert!(summary.contains("Node Count: 3"));
assert!(summary.contains("Shard Count: 32"));
assert!(summary.contains("Meta Version: 158"));
}
#[test]
fn test_debug_export_structure() {
let export = types::ClusterDebugExport {
timestamp: "2026-02-12T10:30:00Z".to_string(),
gateway_version: "0.1.0".to_string(),
cluster: types::ClusterStatusResponse {
node_count: 3,
shard_count: 32,
meta_version: 158,
nodes: vec![],
},
health: types::HealthResponse { healthy: true, reachable_nodes: 3, joined: true },
shards: vec![],
};
let json = output::format_json(&export).expect("Failed to format debug export");
assert!(json.contains("\"timestamp\""));
assert!(json.contains("\"gateway_version\""));
assert!(json.contains("\"cluster\""));
assert!(json.contains("\"health\""));
assert!(json.contains("\"shards\""));
}
#[test]
fn test_empty_nodes_table() {
let nodes: Vec<types::NodeStatusInfo> = vec![];
let table = output::format_nodes_table(&nodes);
assert!(table.contains("(no nodes)"));
}
#[test]
fn test_empty_shards_table() {
let shards: Vec<types::RangeInfoDto> = vec![];
let table = output::format_shards_table(&shards);
assert!(table.contains("(no shards)"));
}