//! Integration tests for the real Proxmox data-read API calls and the //! VM/container lifecycle operations. //! //! Every request goes through `Connection::request`, so these tests cover URL //! construction, query parameters, auth header injection, response envelope //! unwrapping, and mapping into the public serde structs. Token-mode //! connections resolve the token from the config, so the read methods can be //! called directly on an added connection without `connect()`. use httpmock::prelude::*; use clustri::{ConnectionConfig, ConnectionManager, EndpointConfig, Error, UpdateVMConfig}; /// Builds a `ConnectionManager` with a single token-mode connection whose /// primary endpoint points at the mock server. `node` pins the storage node. async fn setup_manager( url: &str, token: &str, node: Option<&str>, ) -> (ConnectionManager, tempfile::TempDir) { let dir = tempfile::tempdir().expect("temp dir should be created"); let path = dir.path().join("connections.json"); let mut manager = ConnectionManager::new(); let config = ConnectionConfig { id: "conn".to_string(), name: "conn".to_string(), primary: EndpointConfig { url: url.to_string(), node: node.map(str::to_string), token: Some(token.to_string()), }, fallbacks: vec![], cert_fingerprint: None, trusted: false, accept_untrusted: true, status: "disconnected".to_string(), cluster_name: None, is_cluster: false, auth_mode: "token".to_string(), username: None, nodes: vec![], cluster_id: None, server_type: "pve".to_string(), }; manager .add_connection(config, &path) .await .expect("connection should be added"); (manager, dir) } #[tokio::test] async fn get_nodes_maps_cluster_resources() { let server = MockServer::start(); let token = "root@pam!nodes-token"; let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/nodes") .header("Authorization", format!("PVEAPIToken={}", token)); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [ {"node": "pve1", "status": "online", "cpu": 0.12, "maxcpu": 16, "mem": 8589934592u64, "maxmem": 68719476736u64, "disk": 214748364800u64, "maxdisk": 858993459200u64, "uptime": 123456, "level": "", "id": "node/pve1", "type": "node"}, {"node": "pve2", "status": "offline", "cpu": 0.0, "maxcpu": 8, "mem": 0, "maxmem": 34359738368u64, "disk": 0, "maxdisk": 536870912000u64, "uptime": 0, "level": "", "id": "node/pve2", "type": "node"} ] }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), token, None).await; let nodes = manager .get_nodes("conn") .await .expect("nodes should be fetched"); assert_eq!(nodes.len(), 2); assert_eq!(nodes[0].node, "pve1"); assert_eq!(nodes[0].status, "online"); assert!((nodes[0].cpu - 0.12).abs() < 1e-9); assert_eq!(nodes[0].maxcpu, 16); assert_eq!(nodes[0].mem, 8589934592u64); assert_eq!(nodes[0].maxmem, 68719476736u64); assert_eq!(nodes[0].disk, 214748364800u64); assert_eq!(nodes[0].maxdisk, 858993459200u64); assert_eq!(nodes[0].uptime, 123456); assert_eq!(nodes[0].id, "node/pve1"); assert_eq!(nodes[0].r#type, "node"); assert_eq!(nodes[1].node, "pve2"); assert_eq!(nodes[1].status, "offline"); mock.assert(); } #[tokio::test] async fn get_vms_uses_cluster_resources() { let server = MockServer::start(); let token = "root@pam!vms-token"; let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/cluster/resources") .query_param("type", "vm") .header("Authorization", format!("PVEAPIToken={}", token)); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [ {"vmid": 100, "name": "web01", "type": "qemu", "status": "running", "node": "pve1", "cpu": 0.03, "cpus": 2, "maxcpu": 2, "mem": 2147483648u64, "maxmem": 4294967296u64, "disk": 107374182400u64, "maxdisk": 34359738368u64, "uptime": 99999, "netin": 123, "netout": 456, "diskread": 789, "diskwrite": 101112, "template": 0, "tags": "prod", "pid": 1234}, // A stopped container: the API omits runtime stats. {"vmid": 201, "name": "ct01", "type": "lxc", "status": "stopped", "node": "pve2", "template": 1, "tags": ""} ] }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), token, None).await; let vms = manager .get_vms("conn") .await .expect("VMs should be fetched"); assert_eq!(vms.len(), 2); assert_eq!(vms[0].vmid, 100); assert_eq!(vms[0].name.as_deref(), Some("web01")); assert_eq!(vms[0].r#type, "qemu"); assert_eq!(vms[0].status, "running"); assert_eq!(vms[0].node, "pve1"); assert_eq!(vms[0].uptime, 99999); assert_eq!(vms[0].netin, 123); assert_eq!(vms[0].netout, 456); assert_eq!(vms[0].diskread, 789); assert_eq!(vms[0].diskwrite, 101112); assert_eq!(vms[0].tags.as_deref(), Some("prod")); assert_eq!(vms[0].pid, Some(1234)); // Tolerance: fields omitted for the stopped LXC default instead of failing. assert_eq!(vms[1].r#type, "lxc"); assert_eq!(vms[1].status, "stopped"); assert_eq!(vms[1].uptime, 0); assert_eq!(vms[1].cpu, 0.0); assert_eq!(vms[1].netin, 0); assert_eq!(vms[1].pid, None); mock.assert(); } #[tokio::test] async fn get_nodes_accepts_string_and_float_maxcpu() { // Some Proxmox versions report maxcpu as a numeric string or an integral // float instead of an integer. let server = MockServer::start(); let token = "root@pam!nodes-token"; let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/nodes") .header("Authorization", format!("PVEAPIToken={}", token)); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [ {"node": "pve1", "status": "online", "cpu": 0.12, "maxcpu": "16", "mem": 8589934592u64, "maxmem": 68719476736u64, "disk": 214748364800u64, "maxdisk": 858993459200u64, "uptime": 123456, "level": "", "id": "node/pve1", "type": "node"}, {"node": "pve2", "status": "online", "cpu": 0.0, "maxcpu": 8.0, "mem": 0, "maxmem": 34359738368u64, "disk": 0, "maxdisk": 536870912000u64, "uptime": 0, "level": "", "id": "node/pve2", "type": "node"} ] }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), token, None).await; let nodes = manager .get_nodes("conn") .await .expect("nodes should be fetched"); assert_eq!(nodes[0].maxcpu, 16); assert_eq!(nodes[1].maxcpu, 8); mock.assert(); } #[tokio::test] async fn get_vms_accepts_string_and_float_cpus() { // Some Proxmox versions report cpus as a numeric string or an integral // float instead of an integer. let server = MockServer::start(); let token = "root@pam!vms-token"; let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/cluster/resources") .query_param("type", "vm") .header("Authorization", format!("PVEAPIToken={}", token)); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [ {"vmid": 100, "name": "web01", "type": "qemu", "status": "running", "node": "pve1", "cpu": 0.03, "cpus": "2", "maxcpu": "2", "mem": 2147483648u64, "maxmem": 4294967296u64, "disk": 107374182400u64, "maxdisk": 34359738368u64, "uptime": 99999, "netin": 123, "netout": 456, "diskread": 789, "diskwrite": 101112, "template": 0, "tags": "prod", "pid": 1234}, {"vmid": 201, "name": "ct01", "type": "lxc", "status": "running", "node": "pve2", "cpu": 0.01, "cpus": 4.0, "maxcpu": 4.0, "mem": 1073741824u64, "maxmem": 2147483648u64, "disk": 107374182400u64, "maxdisk": 34359738368u64, "uptime": 123, "netin": 1, "netout": 2, "diskread": 3, "diskwrite": 4, "template": 0, "tags": "", "pid": 5678} ] }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), token, None).await; let vms = manager .get_vms("conn") .await .expect("VMs should be fetched"); assert_eq!(vms[0].cpus, 2); assert_eq!(vms[1].cpus, 4); mock.assert(); } #[tokio::test] async fn get_vms_rejects_non_numeric_cpus() { let server = MockServer::start(); let token = "root@pam!vms-token"; let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/cluster/resources") .query_param("type", "vm") .header("Authorization", format!("PVEAPIToken={}", token)); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [ {"vmid": 100, "name": "web01", "type": "qemu", "status": "running", "node": "pve1", "cpu": 0.03, "cpus": "many", "maxcpu": 2, "mem": 2147483648u64, "maxmem": 4294967296u64, "disk": 107374182400u64, "maxdisk": 34359738368u64, "uptime": 99999, "netin": 0, "netout": 0, "diskread": 0, "diskwrite": 0, "template": 0, "tags": "prod"} ] }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), token, None).await; let error = manager .get_vms("conn") .await .expect_err("non-numeric cpus must be rejected"); assert!( matches!(error, Error::SerializationError(ref message) if message.contains("many")), "expected SerializationError mentioning the cpus value, got: {}", error ); mock.assert(); } #[tokio::test] async fn get_storage_maps_cluster_resources() { let server = MockServer::start(); let token = "root@pam!storage-token"; let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/cluster/resources") .query_param("type", "storage"); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [ // The real cluster-resource shape: usage is `disk`/`maxdisk` // and liveness is the `status` string. There are no // `used`/`total`/`avail`/`enabled`/`active` keys. {"storage": "local", "node": "pve1", "type": "dir", "content": "iso,vztmpl", "shared": 0, "plugintype": "dir", "disk": 429496729600u64, "maxdisk": 858993459200u64, "status": "available"}, {"storage": "backup", "node": "pve2", "type": "nfs", "content": "backup", "shared": 1, "plugintype": "nfs", "disk": 644245094400u64, "maxdisk": 1717986918400u64, "status": "unavailable"} ] }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), token, None).await; let storages = manager .get_storage("conn") .await .expect("storage should be fetched"); assert_eq!(storages.len(), 2); assert_eq!(storages[0].storage, "local"); assert_eq!(storages[0].r#type, "dir"); assert_eq!(storages[0].content, "iso,vztmpl"); // `disk`/`maxdisk`/`status` map onto used/total/avail/enabled/active. assert_eq!(storages[0].used, 429496729600u64); assert_eq!(storages[0].total, 858993459200u64); assert_eq!(storages[0].avail, 429496729600u64); assert_eq!(storages[0].enabled, 1); assert_eq!(storages[0].active, 1); assert_eq!(storages[0].shared, 0); assert_eq!(storages[0].node, "pve1"); // A storage whose status is not "available" reports enabled/active 0. assert_eq!(storages[1].enabled, 0); assert_eq!(storages[1].active, 0); assert_eq!(storages[1].used, 644245094400u64); assert_eq!(storages[1].total, 1717986918400u64); assert_eq!(storages[1].avail, 1073741824000u64); assert_eq!(storages[1].shared, 1); assert_eq!(storages[1].node, "pve2"); mock.assert(); } #[tokio::test] async fn get_storage_content_uses_configured_node() { let server = MockServer::start(); let token = "root@pam!content-token"; let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/nodes/pve1/storage/local/content"); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [ {"volid": "local:iso/debian-12.iso", "content": "iso", "ctime": 1700000000, "format": "iso", "size": 68719476736u64}, {"volid": "local:vztmpl/ubuntu-22.tar.xz", "content": "vztmpl", "ctime": 1700000001} ] }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), token, Some("pve1")).await; let contents = manager .get_storage_content("conn", "local", Some("pve1")) .await .expect("content should be fetched"); assert_eq!(contents.len(), 2); assert_eq!(contents[0].volid, "local:iso/debian-12.iso"); assert_eq!(contents[0].content, "iso"); assert_eq!(contents[0].format.as_deref(), Some("iso")); assert_eq!(contents[0].size, Some(68719476736u64)); // Tolerance: optional metadata is omitted for the second entry. assert_eq!(contents[1].format, None); assert_eq!(contents[1].subtype, None); mock.assert(); } #[tokio::test] async fn get_storage_content_accepts_string_metadata() { // Some Proxmox versions report ctime/size as numeric strings instead of // integers; the list must still parse. let server = MockServer::start(); let token = "root@pam!content-token"; let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/nodes/pve1/storage/local/content"); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [ {"volid": "local:iso/debian-12.iso", "content": "iso", "ctime": "1700000000", "format": "iso", "size": "68719476736"}, {"volid": "local:vztmpl/ubuntu-22.tar.xz", "content": "vztmpl", "ctime": 1700000001.0, "format": "tgz"} ] }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), token, Some("pve1")).await; let contents = manager .get_storage_content("conn", "local", Some("pve1")) .await .expect("content should be fetched"); assert_eq!(contents.len(), 2); assert_eq!(contents[0].ctime, 1700000000); assert_eq!(contents[0].size, Some(68719476736u64)); // Integral floats are accepted too; missing size stays None. assert_eq!(contents[1].ctime, 1700000001); assert_eq!(contents[1].size, None); mock.assert(); } #[tokio::test] async fn get_storage_content_falls_back_to_online_node() { let server = MockServer::start(); let token = "root@pam!fallback-token"; let nodes_mock = server.mock(|when, then| { when.method(GET).path("/api2/json/nodes"); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [ {"node": "pve1", "status": "online", "cpu": 0.0, "maxcpu": 8, "mem": 0, "maxmem": 34359738368u64, "disk": 0, "maxdisk": 536870912000u64, "uptime": 0, "level": "", "id": "node/pve1", "type": "node"}, {"node": "pve2", "status": "offline", "cpu": 0.0, "maxcpu": 8, "mem": 0, "maxmem": 34359738368u64, "disk": 0, "maxdisk": 536870912000u64, "uptime": 0, "level": "", "id": "node/pve2", "type": "node"} ] }) .to_string(), ); }); let content_mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/nodes/pve1/storage/local/content"); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [{"volid": "local:iso/debian.iso", "content": "iso", "ctime": 1700000000}] }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), token, None).await; let contents = manager .get_storage_content("conn", "local", None) .await .expect("content should be fetched"); assert_eq!(contents.len(), 1); assert_eq!(contents[0].volid, "local:iso/debian.iso"); nodes_mock.assert(); content_mock.assert(); } #[tokio::test] async fn get_storage_content_uses_explicit_node() { let server = MockServer::start(); let token = "root@pam!explicit-node-token"; let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/nodes/pve2/storage/local/content"); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [{"volid": "local:iso/explicit.iso", "content": "iso", "ctime": 1700000000}] }) .to_string(), ); }); // The connection pins `pve1`, but an explicitly requested node must win. let (manager, _dir) = setup_manager(&server.base_url(), token, Some("pve1")).await; let contents = manager .get_storage_content("conn", "local", Some("pve2")) .await .expect("content should be fetched"); assert_eq!(contents.len(), 1); assert_eq!(contents[0].volid, "local:iso/explicit.iso"); mock.assert(); } #[tokio::test] async fn get_storage_detail_maps_status() { let server = MockServer::start(); let token = "root@pam!detail-token"; let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/nodes/pve1/storage/local/status"); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ // The real status response has no `storage` or `node` keys; // both fields must default to empty strings. "data": {"type": "dir", "content": "iso,vztmpl", "active": 1, "enabled": 1, "shared": 0, "used": 429496729600u64, "total": 858993459200u64, "avail": 429496729600u64} }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), token, None).await; let detail = manager .get_storage_detail("conn", "pve1", "local") .await .expect("detail should be fetched"); // The storage and node are not part of the status response, so they // default to empty strings. assert_eq!(detail.storage, ""); assert_eq!(detail.node, ""); assert_eq!(detail.r#type, "dir"); assert_eq!(detail.content, "iso,vztmpl"); assert_eq!(detail.active, 1); assert_eq!(detail.enabled, 1); assert_eq!(detail.used, 429496729600u64); assert_eq!(detail.total, 858993459200u64); assert_eq!(detail.avail, 429496729600u64); mock.assert(); } #[tokio::test] async fn get_tasks_maps_cluster_tasks() { let server = MockServer::start(); let token = "root@pam!tasks-token"; let mock = server.mock(|when, then| { when.method(GET) .path("/api2/json/cluster/tasks"); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [ {"upid": "UPID:pve1:00000001:00000001:5F000000:qmpstart:100:root@pam:", "node": "pve1", "pid": 1000, "pstart": 100, "starttime": 1700000000, "type": "qmpstart", "id": "100", "user": "root@pam", "status": "stopped", "endtime": 1700000100, "exitstatus": "OK"}, {"upid": "UPID:pve2:00000002:00000002:5F000001:vzstart:201:root@pam:", "node": "pve2", "pid": 2000, "pstart": 200, "starttime": 1700000001, "type": "vzstart", "id": "201", "user": "root@pam"} ] }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), token, None).await; let tasks = manager .get_tasks("conn") .await .expect("tasks should be fetched"); assert_eq!(tasks.len(), 2); assert_eq!( tasks[0].upid, "UPID:pve1:00000001:00000001:5F000000:qmpstart:100:root@pam:" ); assert_eq!(tasks[0].node, "pve1"); assert_eq!(tasks[0].pid, 1000); assert_eq!(tasks[0].pstart, 100); assert_eq!(tasks[0].starttime, 1700000000); assert_eq!(tasks[0].r#type, "qmpstart"); assert_eq!(tasks[0].id, "100"); assert_eq!(tasks[0].user, "root@pam"); assert_eq!(tasks[0].endtime, Some(1700000100)); assert_eq!(tasks[0].status.as_deref(), Some("stopped")); assert_eq!(tasks[0].exitstatus.as_deref(), Some("OK")); // Tolerance: the second task has not finished yet. assert_eq!(tasks[1].endtime, None); assert_eq!(tasks[1].status, None); assert_eq!(tasks[1].exitstatus, None); mock.assert(); } #[tokio::test] async fn get_cluster_status_builds_cluster() { let server = MockServer::start(); let token = "root@pam!cluster-token"; let mock = server.mock(|when, then| { when.method(GET).path("/api2/json/cluster/status"); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [ {"type": "cluster", "id": "cluster", "name": "proxmox-cluster", "nodes": 2}, {"type": "node", "id": "node/pve1", "nodeid": 1, "online": 1, "local": 1, "ip": "10.0.0.1"}, {"type": "node", "id": "node/pve2", "nodeid": 2, "online": 0, "ip": "10.0.0.2"} ] }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), token, None).await; let status = manager .get_cluster_status("conn") .await .expect("cluster status should be fetched"); assert_eq!(status.r#type, "cluster"); assert_eq!(status.name, "proxmox-cluster"); assert_eq!(status.id, "cluster"); let nodes = status.nodes.expect("nodes should be present"); assert_eq!(nodes.len(), 2); assert_eq!(nodes[0].name, "pve1"); assert_eq!(nodes[0].nodeid, 1); assert_eq!(nodes[0].online, 1); assert_eq!(nodes[0].local, Some(1)); assert_eq!(nodes[0].ip.as_deref(), Some("10.0.0.1")); assert_eq!(nodes[1].name, "pve2"); assert_eq!(nodes[1].online, 0); assert_eq!(nodes[1].local, None); mock.assert(); } #[tokio::test] async fn get_cluster_status_falls_back_to_default_name() { let server = MockServer::start(); let mock = server.mock(|when, then| { when.method(GET).path("/api2/json/cluster/status"); then.status(200) .header("content-type", "application/json") .body( serde_json::json!({ "data": [{"type": "node", "id": "node/pve1", "nodeid": 1, "online": 1}] }) .to_string(), ); }); let (manager, _dir) = setup_manager(&server.base_url(), "root@pam!default-token", None).await; let status = manager .get_cluster_status("conn") .await .expect("status should be fetched"); assert_eq!(status.name, "default"); assert_eq!(status.id, ""); let nodes = status.nodes.expect("nodes should be present"); assert_eq!(nodes.len(), 1); assert_eq!(nodes[0].name, "pve1"); mock.assert(); } #[tokio::test] async fn lifecycle_uses_qemu_or_lxc_path() { let server = MockServer::start(); let token = "root@pam!lifecycle-token"; let start_qemu = server.mock(|when, then| { when.method(POST) .path("/api2/json/nodes/pve1/qemu/100/status/start") .header("Authorization", format!("PVEAPIToken={}", token)); then.status(200) .header("content-type", "application/json") .body(r#"{"data":null}"#); }); let start_lxc = server.mock(|when, then| { when.method(POST) .path("/api2/json/nodes/pve1/lxc/201/status/start"); then.status(200) .header("content-type", "application/json") .body(r#"{"data":null}"#); }); let stop_qemu = server.mock(|when, then| { when.method(POST) .path("/api2/json/nodes/pve1/qemu/100/status/stop"); then.status(200) .header("content-type", "application/json") .body(r#"{"data":null}"#); }); let (manager, _dir) = setup_manager(&server.base_url(), token, None).await; manager .start_vm("conn", "pve1", 100, "qemu") .await .expect("qemu start should succeed"); manager .start_vm("conn", "pve1", 201, "lxc") .await .expect("lxc start should succeed"); manager .stop_vm("conn", "pve1", 100, "qemu") .await .expect("qemu stop should succeed"); start_qemu.assert(); start_lxc.assert(); stop_qemu.assert(); } #[tokio::test] async fn lifecycle_with_invalid_type_errors() { let server = MockServer::start(); let probe = server.mock(|when, then| { when.method(POST) .path("/api2/json/nodes/pve1/kvm/100/status/start"); then.status(200) .header("content-type", "application/json") .body(r#"{"data":null}"#); }); let (manager, _dir) = setup_manager(&server.base_url(), "root@pam!bad-type-token", None).await; let error = manager .start_vm("conn", "pve1", 100, "kvm") .await .expect_err("invalid vm type must be rejected"); assert!( matches!(error, Error::InvalidUrl(ref message) if message.contains("kvm")), "expected InvalidUrl mentioning 'kvm', got: {}", error ); assert_eq!( probe.calls(), 0, "no HTTP request should be made for an invalid vm type" ); } #[tokio::test] async fn update_vm_config_posts_only_present_fields() { let server = MockServer::start(); let token = "root@pam!update-config-token"; let mock = server.mock(|when, then| { when.method(POST) .path("/api2/json/nodes/pve1/qemu/100/config") .header("Authorization", format!("PVEAPIToken={}", token)) // The form body carries exactly the present fields. .body_includes("name=web01") .body_includes("cores=4") .body_includes("memory=8192") .body_excludes("description"); then.status(200) .header("content-type", "application/json") .body(r#"{"data":null}"#); }); let (manager, _dir) = setup_manager(&server.base_url(), token, None).await; manager .update_vm_config( "conn", "pve1", 100, "qemu", UpdateVMConfig { name: Some("web01".to_string()), cores: Some(4), memory: Some(8192), description: None, }, ) .await .expect("config update should succeed"); mock.assert(); } #[tokio::test] async fn update_vm_config_with_all_none_errors_without_request() { let server = MockServer::start(); let probe = server.mock(|when, then| { when.method(POST) .path("/api2/json/nodes/pve1/qemu/100/config"); then.status(200) .header("content-type", "application/json") .body(r#"{"data":null}"#); }); let (manager, _dir) = setup_manager(&server.base_url(), "root@pam!empty-config-token", None).await; let error = manager .update_vm_config( "conn", "pve1", 100, "qemu", UpdateVMConfig { name: None, cores: None, memory: None, description: None, }, ) .await .expect_err("an empty config must be rejected"); assert!( matches!(error, Error::ApiError(ref message) if message == "Nothing to update"), "expected ApiError 'Nothing to update', got: {}", error ); assert_eq!( probe.calls(), 0, "no HTTP request should be made for an empty config" ); } #[tokio::test] async fn read_methods_error_with_connection_not_found() { let server = MockServer::start(); let (manager, _dir) = setup_manager(&server.base_url(), "root@pam!missing-token", None).await; let error = manager .get_nodes("does-not-exist") .await .expect_err("unknown connection must error"); assert!( matches!(error, Error::ConnectionNotFound(ref id) if id == "does-not-exist"), "expected ConnectionNotFound, got: {}", error ); let error = manager .get_vms("does-not-exist") .await .expect_err("unknown connection must error"); assert!( matches!(error, Error::ConnectionNotFound(ref id) if id == "does-not-exist"), "expected ConnectionNotFound, got: {}", error ); let error = manager .start_vm("does-not-exist", "pve1", 100, "qemu") .await .expect_err("unknown connection must error"); assert!( matches!(error, Error::ConnectionNotFound(ref id) if id == "does-not-exist"), "expected ConnectionNotFound, got: {}", error ); }