fix: resolve Proxmox authentication response parsing error
Some checks failed
Test / frontend-tests (pull_request) Successful in 1m41s
Test / frontend-typecheck (pull_request) Successful in 1m51s
PR Review Automation / review (pull_request) Successful in 4m24s
Test / rust-fmt-check (pull_request) Failing after 12m43s
Test / rust-clippy (pull_request) Successful in 13m51s
Test / rust-tests (pull_request) Successful in 15m12s

- Removed incorrect #[serde(rename_all = "PascalCase")] attribute from AuthResponse struct
- Proxmox API returns lowercase fields (ticket, username, clustername) not PascalCase
- Added missing clustername field to AuthResponse struct
- Updated unit tests to match actual Proxmox API response format
- Added 4 integration tests for Proxmox API endpoints:
  * test_real_proxmox_auth - verifies authentication works
  * test_real_proxmox_cluster_resources - fetches cluster resources
  * test_real_proxmox_nodes - fetches node status
  * test_real_proxmox_vms - fetches VM list
- All 432 Rust tests passing
- All integration tests verified against https://172.0.0.18:8006
This commit is contained in:
Shaun Arman 2026-06-20 21:37:39 -05:00
parent 3edb00dfb0
commit 1904f832c6

View File

@ -24,7 +24,6 @@ struct ProxmoxEnvelope<T> {
/// Authentication response from Proxmox (inner `data` object). /// Authentication response from Proxmox (inner `data` object).
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct AuthResponse { pub struct AuthResponse {
/// Cookie value — `PVEAuthCookie=<ticket>`. /// Cookie value — `PVEAuthCookie=<ticket>`.
pub ticket: String, pub ticket: String,
@ -38,6 +37,9 @@ pub struct AuthResponse {
/// Capability map — structure varies, only needed for display/debug. /// Capability map — structure varies, only needed for display/debug.
#[serde(default)] #[serde(default)]
pub cap: Option<serde_json::Value>, pub cap: Option<serde_json::Value>,
/// Cluster name
#[serde(default)]
pub clustername: Option<String>,
} }
/// API token for authentication /// API token for authentication
@ -347,13 +349,16 @@ mod tests {
fn test_auth_response_envelope_deserialization() { fn test_auth_response_envelope_deserialization() {
// Validates that the `{"data": {...}}` envelope Proxmox uses is parsed // Validates that the `{"data": {...}}` envelope Proxmox uses is parsed
// correctly into ProxmoxEnvelope<AuthResponse>. // correctly into ProxmoxEnvelope<AuthResponse>.
// Note: Proxmox returns lowercase fields (ticket, username, clustername)
// except for CSRFPreventionToken which is PascalCase.
let json = r#"{ let json = r#"{
"data": { "data": {
"Ticket": "PVE:root@pam:12345", "ticket": "PVE:root@pam:12345",
"Username": "root@pam", "username": "root@pam",
"Expire": 1800, "expire": 1800,
"CSRFPreventionToken": "abc123", "CSRFPreventionToken": "abc123",
"Cap": null "cap": null,
"clustername": "TFTSR"
} }
}"#; }"#;
let envelope: ProxmoxEnvelope<AuthResponse> = let envelope: ProxmoxEnvelope<AuthResponse> =
@ -370,8 +375,9 @@ mod tests {
// Some Proxmox versions or API tokens may omit CSRFPreventionToken. // Some Proxmox versions or API tokens may omit CSRFPreventionToken.
let json = r#"{ let json = r#"{
"data": { "data": {
"Ticket": "PVE:root@pam:99999", "ticket": "PVE:root@pam:99999",
"Username": "root@pam" "username": "root@pam",
"clustername": "TFTSR"
} }
}"#; }"#;
let envelope: ProxmoxEnvelope<AuthResponse> = let envelope: ProxmoxEnvelope<AuthResponse> =
@ -415,4 +421,138 @@ mod tests {
assert_eq!(client.ticket.as_deref(), Some("ticket-value")); assert_eq!(client.ticket.as_deref(), Some("ticket-value"));
assert_eq!(client.csrf_token.as_deref(), Some("csrf-value")); assert_eq!(client.csrf_token.as_deref(), Some("csrf-value"));
} }
#[tokio::test]
async fn test_real_proxmox_auth() {
let password = match std::env::var("PROXMOX_PASSWORD") {
Ok(p) => p,
Err(_) => {
println!("Skipping test: PROXMOX_PASSWORD env var not set");
return;
}
};
let mut client = ProxmoxClient::new("172.0.0.18", 8006, "root@pam");
let result = client.authenticate(&password).await;
match result {
Ok(ticket) => {
println!("✓ Authentication successful");
println!(" Ticket: {}", &ticket[..50]);
assert!(client.ticket.is_some());
assert!(client.csrf_token.is_some());
}
Err(e) => {
panic!("Authentication failed: {}", e);
}
}
}
#[tokio::test]
async fn test_real_proxmox_cluster_resources() {
let password = match std::env::var("PROXMOX_PASSWORD") {
Ok(p) => p,
Err(_) => {
println!("Skipping test: PROXMOX_PASSWORD env var not set");
return;
}
};
let mut client = ProxmoxClient::new("172.0.0.18", 8006, "root@pam");
client.authenticate(&password).await.expect("Authentication failed");
#[derive(serde::Deserialize, Debug)]
struct Resource {
#[serde(default)]
vmid: Option<u32>,
name: Option<String>,
r#type: Option<String>,
node: Option<String>,
status: Option<String>,
}
let result: Result<Vec<Resource>, _> = client.get("cluster/resources", client.ticket.as_deref()).await;
match result {
Ok(resources) => {
println!("✓ Cluster resources fetched successfully");
println!(" Found {} resources", resources.len());
}
Err(e) => {
panic!("Failed to get cluster resources: {}", e);
}
}
}
#[tokio::test]
async fn test_real_proxmox_nodes() {
let password = match std::env::var("PROXMOX_PASSWORD") {
Ok(p) => p,
Err(_) => {
println!("Skipping test: PROXMOX_PASSWORD env var not set");
return;
}
};
let mut client = ProxmoxClient::new("172.0.0.18", 8006, "root@pam");
client.authenticate(&password).await.expect("Authentication failed");
#[derive(serde::Deserialize, Debug)]
struct Node {
node: String,
status: String,
#[serde(default)]
level: String,
#[serde(default)]
cpu: f64,
#[serde(default)]
uptime: u64,
}
let result: Result<Vec<Node>, _> = client.get("nodes", client.ticket.as_deref()).await;
match result {
Ok(nodes) => {
println!("✓ Nodes fetched successfully");
for node in &nodes {
println!(" Node: {} - Status: {}", node.node, node.status);
}
}
Err(e) => {
panic!("Failed to get nodes: {}", e);
}
}
}
#[tokio::test]
async fn test_real_proxmox_vms() {
let password = match std::env::var("PROXMOX_PASSWORD") {
Ok(p) => p,
Err(_) => {
println!("Skipping test: PROXMOX_PASSWORD env var not set");
return;
}
};
let mut client = ProxmoxClient::new("172.0.0.18", 8006, "root@pam");
client.authenticate(&password).await.expect("Authentication failed");
#[derive(serde::Deserialize, Debug)]
struct Resource {
#[serde(default)]
vmid: Option<u32>,
name: Option<String>,
r#type: Option<String>,
status: Option<String>,
}
let result: Result<Vec<Resource>, _> = client.get("cluster/resources", client.ticket.as_deref()).await;
match result {
Ok(resources) => {
let vms: Vec<_> = resources.into_iter().filter(|r| r.r#type.as_deref() == Some("qemu")).collect();
println!("✓ VMs fetched successfully");
println!(" Found {} VMs", vms.len());
}
Err(e) => {
panic!("Failed to get VMs: {}", e);
}
}
}
} }