//! Integration tests for the Proxmox API client core. //! //! These tests exercise the generic `api_request` helper against a local HTTP //! mock server, covering auth header injection, response envelope unwrapping, //! error mapping, query parameters, and URL construction. use httpmock::prelude::*; use proxmox_desktop::{api_request, AuthContext, AuthMode, Error}; use reqwest::Client; use reqwest::Method as RMethod; const TICKET: &str = "PVE:root@pam:abc123"; const CSRF_TOKEN: &str = "csrf-token-123"; fn token_auth() -> AuthContext { AuthContext { mode: AuthMode::Token, token: Some("root@pam!test-token".to_string()), ticket: None, csrf_token: None, } } fn password_auth() -> AuthContext { AuthContext { mode: AuthMode::Password, token: None, ticket: Some(TICKET.to_string()), csrf_token: Some(CSRF_TOKEN.to_string()), } } #[tokio::test] async fn token_auth_sends_authorization_header_and_unwraps_envelope() { let server = MockServer::start(); let expected = serde_json::json!({ "version": "8.1.0", "release": "8.1", "repoid": "abc123", }); let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/version") .header("Authorization", "PVEAPIToken=root@pam!test-token"); then.status(200) .header("content-type", "application/json") .body(serde_json::json!({ "data": expected }).to_string()); }); let result = api_request( &Client::new(), &server.base_url(), RMethod::GET, "/version", &token_auth(), &[], None, ) .await .expect("request should succeed"); assert_eq!(result, expected); mock.assert(); } #[tokio::test] async fn password_auth_sends_cookie_header_on_get() { let server = MockServer::start(); let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/version") .header("Cookie", format!("PVEAuthCookie={}", TICKET)); then.status(200) .header("content-type", "application/json") .body(r#"{"data":{"version":"8.1.0"}}"#); }); let result = api_request( &Client::new(), &server.base_url(), RMethod::GET, "/version", &password_auth(), &[], None, ) .await .expect("request should succeed"); assert_eq!(result["version"], "8.1.0"); mock.assert(); } #[tokio::test] async fn password_auth_sends_cookie_and_csrf_headers_on_post() { let server = MockServer::start(); let mock = server.mock(|when, then| { when.method(POST) .path("/api2/json/nodes/pve/qemu/100/status/start") .header("Cookie", format!("PVEAuthCookie={}", TICKET)) .header("CSRFPreventionToken", CSRF_TOKEN); then.status(200) .header("content-type", "application/json") .body(r#"{"data":null}"#); }); let result = api_request( &Client::new(), &server.base_url(), RMethod::POST, "/nodes/pve/qemu/100/status/start", &password_auth(), &[], None, ) .await .expect("request should succeed"); assert_eq!(result, serde_json::Value::Null); mock.assert(); } #[tokio::test] async fn server_error_message_is_mapped_to_api_error() { let server = MockServer::start(); let mock = server.mock(|when, then| { when.method(GET).path("/api2/json/version"); then.status(500) .header("content-type", "application/json") .body(r#"{"message":"boom"}"#); }); let error = api_request( &Client::new(), &server.base_url(), RMethod::GET, "/version", &token_auth(), &[], None, ) .await .expect_err("request should fail"); assert!( matches!(error, Error::ApiError(ref message) if message == "boom"), "expected ApiError with message 'boom', got: {}", error ); mock.assert(); } #[tokio::test] async fn query_params_are_included_in_request() { let server = MockServer::start(); let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/nodes/pve/qemu") .query_param("node", "pve") .query_param("vmid", "100") .query_param("include-config", "true"); then.status(200) .header("content-type", "application/json") .body(r#"{"data":[]}"#); }); let query = [ ("node", "pve".to_string()), ("vmid", "100".to_string()), ("include-config", "true".to_string()), ]; let result = api_request( &Client::new(), &server.base_url(), RMethod::GET, "/nodes/pve/qemu", &token_auth(), &query, None, ) .await .expect("request should succeed"); assert_eq!(result, serde_json::json!([])); mock.assert(); } #[tokio::test] async fn base_url_trailing_slash_is_stripped() { let server = MockServer::start(); let mock = server.mock(|when, then| { when.method(GET).path("/api2/json/version"); then.status(200) .header("content-type", "application/json") .body(r#"{"data":{"version":"8.1.0"}}"#); }); let base_url = format!("{}/", server.base_url()); let result = api_request( &Client::new(), &base_url, RMethod::GET, "/version", &token_auth(), &[], None, ) .await .expect("request should succeed"); assert_eq!(result["version"], "8.1.0"); mock.assert(); }