diff --git a/tests/PSProxmoxVE.Core.Tests/Authentication/PveAuthenticatorTests.cs b/tests/PSProxmoxVE.Core.Tests/Authentication/PveAuthenticatorTests.cs
new file mode 100644
index 0000000..654714d
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Authentication/PveAuthenticatorTests.cs
@@ -0,0 +1,85 @@
+using System;
+using Xunit;
+using PSProxmoxVE.Core.Authentication;
+
+namespace PSProxmoxVE.Core.Tests.Authentication
+{
+ ///
+ /// Tests for PveAuthenticator input validation. These tests exercise the
+ /// argument-validation logic without making actual HTTP calls.
+ ///
+ public class PveAuthenticatorTests
+ {
+ private const string ValidHostname = "pve.example.com";
+ private const int ValidPort = 8006;
+
+ [Fact]
+ public void ValidateUsername_MissingAtSign_Throws()
+ {
+ var ex = Assert.Throws(() =>
+ PveAuthenticator.AuthenticateWithCredentials(ValidHostname, ValidPort, false, "rootpam", "password"));
+
+ Assert.Contains("realm", ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void ValidateApiToken_InvalidFormat_NoTokenId_Throws()
+ {
+ // Missing the !TOKENID part — no exclamation mark
+ var ex = Assert.Throws(() =>
+ PveAuthenticator.AuthenticateWithApiToken(ValidHostname, ValidPort, false, "root@pam=someuuid"));
+
+ Assert.Contains("token", ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void ValidateApiToken_InvalidFormat_NoEquals_Throws()
+ {
+ // Has ! but no = sign after TOKENID
+ var ex = Assert.Throws(() =>
+ PveAuthenticator.AuthenticateWithApiToken(ValidHostname, ValidPort, false, "root@pam!mytoken"));
+
+ Assert.Contains("token", ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void ValidateApiToken_InvalidFormat_NoRealm_Throws()
+ {
+ // Missing the @realm portion
+ var ex = Assert.Throws(() =>
+ PveAuthenticator.AuthenticateWithApiToken(ValidHostname, ValidPort, false, "root!mytoken=someuuid"));
+
+ Assert.Contains("token", ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void ValidateHostname_NullOrEmpty_Throws_ForCredentials(string hostname)
+ {
+ Assert.Throws(() =>
+ PveAuthenticator.AuthenticateWithCredentials(hostname, ValidPort, false, "root@pam", "password"));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void ValidateHostname_NullOrEmpty_Throws_ForApiToken(string hostname)
+ {
+ Assert.Throws(() =>
+ PveAuthenticator.AuthenticateWithApiToken(hostname, ValidPort, false, "root@pam!mytoken=uuid"));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void ValidateApiToken_NullOrEmpty_Throws(string token)
+ {
+ Assert.Throws(() =>
+ PveAuthenticator.AuthenticateWithApiToken(ValidHostname, ValidPort, false, token));
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/Authentication/PveSessionTests.cs b/tests/PSProxmoxVE.Core.Tests/Authentication/PveSessionTests.cs
new file mode 100644
index 0000000..af9d753
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Authentication/PveSessionTests.cs
@@ -0,0 +1,104 @@
+using System;
+using Xunit;
+using PSProxmoxVE.Core.Authentication;
+
+namespace PSProxmoxVE.Core.Tests.Authentication
+{
+ public class PveSessionTests
+ {
+ private const string TestHostname = "pve.example.com";
+ private const int TestPort = 8006;
+
+ [Fact]
+ public void TicketSession_NotExpired_OnCreation()
+ {
+ var expiry = DateTime.UtcNow.AddHours(2);
+ var session = new PveSession(TestHostname, TestPort, false, "PVE:root@pam:TICKET", "CSRFTOKEN", expiry);
+
+ Assert.False(session.IsExpired);
+ }
+
+ [Fact]
+ public void TicketSession_IsExpired_After2Hours()
+ {
+ // Simulate a ticket that already expired an hour ago
+ var expiry = DateTime.UtcNow.AddHours(-1);
+ var session = new PveSession(TestHostname, TestPort, false, "PVE:root@pam:OLDTICKET", "CSRFTOKEN", expiry);
+
+ Assert.True(session.IsExpired);
+ }
+
+ [Fact]
+ public void ApiTokenSession_NeverExpires()
+ {
+ var session = new PveSession(TestHostname, TestPort, false, "root@pam!mytoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
+
+ Assert.False(session.IsExpired);
+ Assert.Equal(PveAuthMode.ApiToken, session.AuthMode);
+ }
+
+ [Fact]
+ public void BaseUrl_CorrectFormat()
+ {
+ var session = new PveSession(TestHostname, TestPort, false, "root@pam!mytoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
+
+ Assert.Equal($"https://{TestHostname}:{TestPort}/api2/json", session.BaseUrl);
+ }
+
+ [Fact]
+ public void TicketSession_AuthMode_IsTicket()
+ {
+ var expiry = DateTime.UtcNow.AddHours(2);
+ var session = new PveSession(TestHostname, TestPort, false, "PVE:root@pam:TICKET", "CSRFTOKEN", expiry);
+
+ Assert.Equal(PveAuthMode.Ticket, session.AuthMode);
+ }
+
+ [Fact]
+ public void SeparateSessions_DoNotShareState()
+ {
+ var expiry1 = DateTime.UtcNow.AddHours(2);
+ var session1 = new PveSession("host1", 8006, false, "PVE:root@pam:TICKET1", "CSRF1", expiry1);
+ var session2 = new PveSession("host2", 8006, false, "root@pam!token=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
+
+ Assert.NotEqual(session1.Hostname, session2.Hostname);
+ Assert.Equal(PveAuthMode.Ticket, session1.AuthMode);
+ Assert.Equal(PveAuthMode.ApiToken, session2.AuthMode);
+ Assert.Equal("PVE:root@pam:TICKET1", session1.Ticket);
+ Assert.Null(session1.ApiToken);
+ Assert.Null(session2.Ticket);
+ Assert.NotNull(session2.ApiToken);
+ }
+
+ [Fact]
+ public void TicketSession_StoresTicketAndCsrf()
+ {
+ var expiry = DateTime.UtcNow.AddHours(2);
+ const string ticket = "PVE:root@pam:ABCD1234";
+ const string csrf = "CSRFPREVENTION";
+
+ var session = new PveSession(TestHostname, TestPort, false, ticket, csrf, expiry);
+
+ Assert.Equal(ticket, session.Ticket);
+ Assert.Equal(csrf, session.CsrfToken);
+ }
+
+ [Fact]
+ public void ApiTokenSession_StoresApiToken()
+ {
+ const string token = "root@pam!mytoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
+ var session = new PveSession(TestHostname, TestPort, false, token);
+
+ Assert.Equal(token, session.ApiToken);
+ }
+
+ [Fact]
+ public void SkipCertificateCheck_IsStored()
+ {
+ var session = new PveSession(TestHostname, TestPort, skipCertificateCheck: true,
+ "root@pam!mytoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
+
+ Assert.True(session.SkipCertificateCheck);
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/Authentication/PveVersionTests.cs b/tests/PSProxmoxVE.Core.Tests/Authentication/PveVersionTests.cs
new file mode 100644
index 0000000..6e85816
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Authentication/PveVersionTests.cs
@@ -0,0 +1,91 @@
+using System;
+using Xunit;
+using PSProxmoxVE.Core.Authentication;
+
+namespace PSProxmoxVE.Core.Tests.Authentication
+{
+ public class PveVersionTests
+ {
+ [Fact]
+ public void Parse_ValidVersion_9_1()
+ {
+ var version = PveVersion.Parse("9.1-1");
+ Assert.Equal(9, version.Major);
+ Assert.Equal(1, version.Minor);
+ }
+
+ [Fact]
+ public void Parse_ValidVersion_8_3()
+ {
+ var version = PveVersion.Parse("8.3-2");
+ Assert.Equal(8, version.Major);
+ Assert.Equal(3, version.Minor);
+ }
+
+ [Fact]
+ public void Parse_ValidVersion_8_0()
+ {
+ var version = PveVersion.Parse("8.0-1");
+ Assert.Equal(8, version.Major);
+ Assert.Equal(0, version.Minor);
+ }
+
+ [Fact]
+ public void Parse_NoPatchLevel()
+ {
+ var version = PveVersion.Parse("9.1");
+ Assert.Equal(9, version.Major);
+ Assert.Equal(1, version.Minor);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void Parse_NullOrEmpty_Throws(string input)
+ {
+ Assert.Throws(() => PveVersion.Parse(input));
+ }
+
+ [Fact]
+ public void Parse_InvalidFormat_Throws()
+ {
+ Assert.Throws(() => PveVersion.Parse("invalid"));
+ }
+
+ [Fact]
+ public void IsAtLeast_True_SameMajorLowerMinor()
+ {
+ var version = PveVersion.Parse("9.1-1");
+ Assert.True(version.IsAtLeast(9, 0));
+ }
+
+ [Fact]
+ public void IsAtLeast_True_LowerMajor()
+ {
+ var version = PveVersion.Parse("9.1-1");
+ Assert.True(version.IsAtLeast(8, 0));
+ }
+
+ [Fact]
+ public void IsAtLeast_False()
+ {
+ var version = PveVersion.Parse("8.3-2");
+ Assert.False(version.IsAtLeast(9, 0));
+ }
+
+ [Fact]
+ public void IsAtLeast_ExactMatch()
+ {
+ var version = PveVersion.Parse("8.0-1");
+ Assert.True(version.IsAtLeast(8, 0));
+ }
+
+ [Fact]
+ public void ToString_Format()
+ {
+ var version = PveVersion.Parse("9.1-1");
+ Assert.Equal("9.1", version.ToString());
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve8_nodes.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve8_nodes.json
new file mode 100644
index 0000000..405ce8e
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve8_nodes.json
@@ -0,0 +1 @@
+{"data":[{"node":"pve-old","status":"online","maxcpu":4,"maxmem":16106127360,"mem":4294967296,"uptime":172800,"pveversion":"pve-manager/8.3-2/12345678","kversion":"6.5.13-6-pve","loadavg":["0.10","0.15","0.12"]}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve8_version.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve8_version.json
new file mode 100644
index 0000000..3497b61
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve8_version.json
@@ -0,0 +1 @@
+{"data":{"version":"8.3-2","release":"8.3","repoid":"12345678","console":"xtermjs","keyboard":"en-us"}}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_cluster_status.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_cluster_status.json
new file mode 100644
index 0000000..155ca2e
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_cluster_status.json
@@ -0,0 +1 @@
+{"data":[{"type":"cluster","name":"testcluster","nodes":3,"quorate":1,"version":3},{"type":"node","name":"pve1","online":1,"local":1,"nodeid":1,"ip":"192.168.1.10","level":""},{"type":"node","name":"pve2","online":1,"local":0,"nodeid":2,"ip":"192.168.1.11","level":""},{"type":"node","name":"pve3","online":0,"local":0,"nodeid":3,"ip":"192.168.1.12","level":""}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_containers.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_containers.json
new file mode 100644
index 0000000..1396463
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_containers.json
@@ -0,0 +1 @@
+{"data":[{"vmid":200,"name":"ct-web","status":"running","node":"pve1","cpus":2,"maxmem":2147483648,"maxswap":536870912,"maxdisk":8589934592,"ostype":"ubuntu","unprivileged":1,"uptime":43200,"tags":"web"},{"vmid":201,"name":"ct-db","status":"stopped","node":"pve1","cpus":1,"maxmem":1073741824,"maxswap":268435456,"maxdisk":4294967296,"ostype":"debian","unprivileged":1,"uptime":0}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_networks.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_networks.json
new file mode 100644
index 0000000..a559447
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_networks.json
@@ -0,0 +1 @@
+{"data":[{"iface":"vmbr0","type":"bridge","address":"192.168.1.10","netmask":"255.255.255.0","gateway":"192.168.1.1","bridge_ports":"enp0s31f6","autostart":1,"active":1,"cidr":"192.168.1.10/24"},{"iface":"vmbr1","type":"bridge","address":"10.0.0.1","netmask":"255.255.255.0","bridge_ports":"enp2s0","autostart":1,"active":1,"cidr":"10.0.0.1/24","bridge_vlan_aware":1}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_node_status.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_node_status.json
new file mode 100644
index 0000000..10b1f60
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_node_status.json
@@ -0,0 +1 @@
+{"data":{"node":"pve1","status":"online","maxcpu":16,"maxmem":68719476736,"mem":17179869184,"uptime":864000,"cpu":0.125,"rootfs":{"total":107374182400,"used":21474836480,"avail":85899345920,"free":85899345920},"swap":{"total":8589934592,"used":1073741824,"free":7516192768},"netin":549755813888,"netout":274877906944,"loadavg":["0.45","0.32","0.28"],"pveversion":"pve-manager/9.1-1/abcdef12","kversion":"6.8.12-5-pve"}}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_nodes.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_nodes.json
new file mode 100644
index 0000000..69790e2
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_nodes.json
@@ -0,0 +1 @@
+{"data":[{"node":"pve1","status":"online","maxcpu":16,"maxmem":68719476736,"mem":17179869184,"uptime":864000,"pveversion":"pve-manager/9.1-1/abcdef12","kversion":"6.8.12-5-pve","loadavg":["0.45","0.32","0.28"]},{"node":"pve2","status":"online","maxcpu":8,"maxmem":34359738368,"mem":8589934592,"uptime":432000,"pveversion":"pve-manager/9.1-1/abcdef12","kversion":"6.8.12-5-pve","loadavg":["0.12","0.08","0.06"]}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_roles.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_roles.json
new file mode 100644
index 0000000..01372bc
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_roles.json
@@ -0,0 +1 @@
+{"data":[{"roleid":"Administrator","privs":"Datastore.Allocate,Datastore.AllocateSpace,Datastore.AllocateTemplate,Datastore.Audit,Group.Allocate,Mapping.Audit,Mapping.Modify,Mapping.Use,Permissions.Modify,Pool.Allocate,Pool.Audit,Realm.Allocate,Realm.AllocateUser,SDN.Allocate,SDN.Audit,SDN.Use,Sys.Audit,Sys.Console,Sys.Incoming,Sys.Modify,Sys.PowerMgmt,Sys.Syslog,User.Modify,VM.Allocate,VM.Audit,VM.Backup,VM.Clone,VM.Config.CDROM,VM.Config.CPU,VM.Config.Cloudinit,VM.Config.Disk,VM.Config.HWType,VM.Config.Memory,VM.Config.Net,VM.Config.Options,VM.Console,VM.Migrate,VM.Monitor,VM.PowerMgmt,VM.Snapshot,VM.Snapshot.Rollback","special":1},{"roleid":"PVEAuditor","privs":"Datastore.Audit,Mapping.Audit,Pool.Audit,SDN.Audit,Sys.Audit,VM.Audit","special":1}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_sdn_vnets.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_sdn_vnets.json
new file mode 100644
index 0000000..4c75e91
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_sdn_vnets.json
@@ -0,0 +1 @@
+{"data":[{"vnet":"myvnet","zone":"localzone","tag":100,"alias":"My VNET"},{"vnet":"prodnet","zone":"vxlanzone","tag":200,"alias":"Production Network","vlanaware":1}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_sdn_zones.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_sdn_zones.json
new file mode 100644
index 0000000..3010e84
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_sdn_zones.json
@@ -0,0 +1 @@
+{"data":[{"zone":"localzone","type":"simple","dns":"","reversedns":"","dnszone":"","mtu":1500,"pending":0},{"zone":"vxlanzone","type":"vxlan","dns":"","reversedns":"","dnszone":"","mtu":1450,"nodes":"pve1,pve2","pending":0}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_snapshots.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_snapshots.json
new file mode 100644
index 0000000..360fec5
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_snapshots.json
@@ -0,0 +1 @@
+{"data":[{"name":"current","description":"You are here!","snaptime":0,"parent":"snap1"},{"name":"snap1","description":"Before upgrade","snaptime":1710000000,"vmstate":1,"parent":"base"},{"name":"base","description":"Initial setup","snaptime":1709000000,"vmstate":0}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_storage.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_storage.json
new file mode 100644
index 0000000..a438908
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_storage.json
@@ -0,0 +1 @@
+{"data":[{"storage":"local","type":"dir","content":"iso,vztmpl,backup","total":107374182400,"used":21474836480,"avail":85899345920,"enabled":1,"shared":0,"active":1},{"storage":"local-lvm","type":"lvmthin","content":"images,rootdir","total":536870912000,"used":107374182400,"avail":429496729600,"enabled":1,"shared":0,"active":1},{"storage":"ceph-pool","type":"rbd","content":"images","total":1099511627776,"used":214748364800,"avail":884763262976,"enabled":1,"shared":1,"active":1}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_storage_content.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_storage_content.json
new file mode 100644
index 0000000..21097a9
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_storage_content.json
@@ -0,0 +1 @@
+{"data":[{"volid":"local:iso/ubuntu-24.04-live-server-amd64.iso","content":"iso","format":"iso","size":2774532096,"ctime":1710000000},{"volid":"local:iso/debian-12.5.0-amd64-netinst.iso","content":"iso","format":"iso","size":658505728,"ctime":1709000000}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_task_running.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_task_running.json
new file mode 100644
index 0000000..b15ab79
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_task_running.json
@@ -0,0 +1 @@
+{"data":{"upid":"UPID:pve1:00001234:00ABCDEF:65F00000:qmstart:100:root@pam:","type":"qmstart","status":"running","node":"pve1","starttime":1710000000,"user":"root@pam","id":"100"}}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_tasks.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_tasks.json
new file mode 100644
index 0000000..0c21de8
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_tasks.json
@@ -0,0 +1 @@
+{"data":{"upid":"UPID:pve1:00001234:00ABCDEF:65F00000:qmstart:100:root@pam:","type":"qmstart","status":"stopped","exitstatus":"OK","node":"pve1","starttime":1710000000,"endtime":1710000010,"user":"root@pam","id":"100"}}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_ticket_response.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_ticket_response.json
new file mode 100644
index 0000000..dafb08a
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_ticket_response.json
@@ -0,0 +1 @@
+{"data":{"username":"root@pam","ticket":"PVE:root@pam:65F00000::abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890","CSRFPreventionToken":"65F00000:abcdef1234567890abcdef12"}}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_users.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_users.json
new file mode 100644
index 0000000..8d6e46d
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_users.json
@@ -0,0 +1 @@
+{"data":[{"userid":"root@pam","firstname":"Root","lastname":"User","email":"root@example.com","enable":1,"realm":"pam","expire":0},{"userid":"admin@pve","firstname":"Admin","lastname":"User","email":"admin@example.com","enable":1,"realm":"pve","groups":"admins","expire":0},{"userid":"readonly@pve","firstname":"Read","lastname":"Only","email":"readonly@example.com","enable":1,"realm":"pve","groups":"audit","comment":"Read-only audit user","expire":0}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_version.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_version.json
new file mode 100644
index 0000000..2ccbe5a
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_version.json
@@ -0,0 +1 @@
+{"data":{"version":"9.1-1","release":"9.1","repoid":"abcdef12","console":"xtermjs","keyboard":"en-us"}}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_vm_config.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_vm_config.json
new file mode 100644
index 0000000..eb41209
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_vm_config.json
@@ -0,0 +1 @@
+{"data":{"cores":4,"sockets":1,"memory":8192,"cpu":"host","bios":"ovmf","machine":"q35","boot":"order=scsi0;net0","description":"Test VM","tags":"prod;web","protection":0,"numa":0,"balloon":0,"ostype":"l26","virtio0":"local-lvm:vm-100-disk-0,size=100G","net0":"virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0,firewall=1","ide2":"local:iso/ubuntu-24.04-live-server-amd64.iso,media=cdrom","scsi0":"local-lvm:vm-100-disk-0,size=100G","scsihw":"virtio-scsi-single","ciuser":"admin","nameserver":"8.8.8.8","searchdomain":"example.com","ipconfig0":"ip=dhcp"}}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_vms.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_vms.json
new file mode 100644
index 0000000..300ebb7
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve9_vms.json
@@ -0,0 +1 @@
+{"data":[{"vmid":100,"name":"test-vm-1","status":"running","node":"pve1","cpus":4,"maxmem":8589934592,"maxdisk":107374182400,"uptime":86400,"tags":"prod;web","template":0,"pid":12345,"qmpstatus":"running","agent":1},{"vmid":101,"name":"test-vm-2","status":"stopped","node":"pve1","cpus":2,"maxmem":4294967296,"maxdisk":53687091200,"uptime":0,"tags":"dev","template":0},{"vmid":9000,"name":"ubuntu-template","status":"stopped","node":"pve1","cpus":2,"maxmem":4294967296,"maxdisk":53687091200,"uptime":0,"template":1}]}
diff --git a/tests/PSProxmoxVE.Core.Tests/Fixtures/pve_api_error.json b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve_api_error.json
new file mode 100644
index 0000000..dcb5492
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Fixtures/pve_api_error.json
@@ -0,0 +1 @@
+{"errors":{"vmid":"property is missing and it is not optional"},"data":null}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/ClusterModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/ClusterModelTests.cs
new file mode 100644
index 0000000..d1a7ca3
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Models/ClusterModelTests.cs
@@ -0,0 +1,135 @@
+using Xunit;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Models.Cluster;
+
+namespace PSProxmoxVE.Core.Tests.Models
+{
+ public class ClusterModelTests
+ {
+ [Fact]
+ public void PveClusterStatus_Deserialize_Pve9_ReturnsCorrectCount()
+ {
+ var json = TestHelper.LoadFixture("pve9_cluster_status.json");
+ var data = JObject.Parse(json)["data"];
+ var entries = data.ToObject();
+ Assert.NotNull(entries);
+ Assert.Equal(4, entries.Length);
+ }
+
+ [Fact]
+ public void PveClusterStatus_Deserialize_Pve9_FirstEntry_IsClusterType()
+ {
+ var json = TestHelper.LoadFixture("pve9_cluster_status.json");
+ var data = JObject.Parse(json)["data"];
+ var entries = data.ToObject();
+ Assert.NotNull(entries);
+ Assert.Equal("cluster", entries[0].Type);
+ }
+
+ [Fact]
+ public void PveClusterStatus_Deserialize_Pve9_ClusterEntry_HasCorrectName()
+ {
+ var json = TestHelper.LoadFixture("pve9_cluster_status.json");
+ var data = JObject.Parse(json)["data"];
+ var entries = data.ToObject();
+ Assert.NotNull(entries);
+ Assert.Equal("testcluster", entries[0].Name);
+ }
+
+ [Fact]
+ public void PveClusterStatus_Deserialize_Pve9_ClusterEntry_HasNodeCount()
+ {
+ var json = TestHelper.LoadFixture("pve9_cluster_status.json");
+ var data = JObject.Parse(json)["data"];
+ var entries = data.ToObject();
+ Assert.NotNull(entries);
+ Assert.Equal(3, entries[0].Nodes);
+ }
+
+ [Fact]
+ public void PveClusterStatus_Deserialize_Pve9_ClusterEntry_IsQuorate()
+ {
+ var json = TestHelper.LoadFixture("pve9_cluster_status.json");
+ var data = JObject.Parse(json)["data"];
+ var entries = data.ToObject();
+ Assert.NotNull(entries);
+ Assert.Equal(1, entries[0].Quorate);
+ }
+
+ [Fact]
+ public void PveClusterStatus_Deserialize_Pve9_SecondEntry_IsNodeType()
+ {
+ var json = TestHelper.LoadFixture("pve9_cluster_status.json");
+ var data = JObject.Parse(json)["data"];
+ var entries = data.ToObject();
+ Assert.NotNull(entries);
+ Assert.Equal("node", entries[1].Type);
+ }
+
+ [Fact]
+ public void PveClusterStatus_Deserialize_Pve9_FirstNodeEntry_HasCorrectIp()
+ {
+ var json = TestHelper.LoadFixture("pve9_cluster_status.json");
+ var data = JObject.Parse(json)["data"];
+ var entries = data.ToObject();
+ Assert.NotNull(entries);
+ Assert.Equal("192.168.1.10", entries[1].Ip);
+ }
+
+ [Fact]
+ public void PveClusterStatus_Deserialize_Pve9_FirstNodeEntry_IsOnline()
+ {
+ var json = TestHelper.LoadFixture("pve9_cluster_status.json");
+ var data = JObject.Parse(json)["data"];
+ var entries = data.ToObject();
+ Assert.NotNull(entries);
+ Assert.Equal(1, entries[1].Online);
+ }
+
+ [Fact]
+ public void PveClusterStatus_Deserialize_Pve9_FirstNodeEntry_IsLocal()
+ {
+ var json = TestHelper.LoadFixture("pve9_cluster_status.json");
+ var data = JObject.Parse(json)["data"];
+ var entries = data.ToObject();
+ Assert.NotNull(entries);
+ Assert.Equal(1, entries[1].Local);
+ }
+
+ [Fact]
+ public void PveClusterStatus_Deserialize_Pve9_ThirdNode_IsOffline()
+ {
+ var json = TestHelper.LoadFixture("pve9_cluster_status.json");
+ var data = JObject.Parse(json)["data"];
+ var entries = data.ToObject();
+ Assert.NotNull(entries);
+ // Fourth entry (index 3) is pve3 which is offline
+ Assert.Equal("node", entries[3].Type);
+ Assert.Equal(0, entries[3].Online);
+ }
+
+ [Fact]
+ public void PveClusterStatus_Deserialize_Pve9_NodeFields_NullForClusterEntry()
+ {
+ var json = TestHelper.LoadFixture("pve9_cluster_status.json");
+ var data = JObject.Parse(json)["data"];
+ var entries = data.ToObject();
+ Assert.NotNull(entries);
+ // Cluster-type entry should not have node-specific fields
+ Assert.Null(entries[0].Ip);
+ Assert.Null(entries[0].Online);
+ }
+
+ [Fact]
+ public void PveClusterStatus_Deserialize_Pve9_ClusterFields_NullForNodeEntry()
+ {
+ var json = TestHelper.LoadFixture("pve9_cluster_status.json");
+ var data = JObject.Parse(json)["data"];
+ var entries = data.ToObject();
+ Assert.NotNull(entries);
+ // Node-type entries should not have cluster-specific fields
+ Assert.Null(entries[1].Nodes);
+ Assert.Null(entries[1].Quorate);
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/ContainerModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/ContainerModelTests.cs
new file mode 100644
index 0000000..a95d11a
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Models/ContainerModelTests.cs
@@ -0,0 +1,125 @@
+using Xunit;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Models.Containers;
+
+namespace PSProxmoxVE.Core.Tests.Models
+{
+ public class ContainerModelTests
+ {
+ [Fact]
+ public void PveContainer_Deserialize_Pve9_ReturnsCorrectCount()
+ {
+ var json = TestHelper.LoadFixture("pve9_containers.json");
+ var data = JObject.Parse(json)["data"];
+ var containers = data.ToObject();
+ Assert.NotNull(containers);
+ Assert.Equal(2, containers.Length);
+ }
+
+ [Fact]
+ public void PveContainer_Deserialize_Pve9_FirstContainer_HasCorrectVmId()
+ {
+ var json = TestHelper.LoadFixture("pve9_containers.json");
+ var data = JObject.Parse(json)["data"];
+ var containers = data.ToObject();
+ Assert.NotNull(containers);
+ Assert.Equal(200, containers[0].VmId);
+ }
+
+ [Fact]
+ public void PveContainer_Deserialize_Pve9_FirstContainer_HasCorrectName()
+ {
+ var json = TestHelper.LoadFixture("pve9_containers.json");
+ var data = JObject.Parse(json)["data"];
+ var containers = data.ToObject();
+ Assert.NotNull(containers);
+ Assert.Equal("ct-web", containers[0].Name);
+ }
+
+ [Fact]
+ public void PveContainer_Deserialize_Pve9_FirstContainer_HasCorrectStatus()
+ {
+ var json = TestHelper.LoadFixture("pve9_containers.json");
+ var data = JObject.Parse(json)["data"];
+ var containers = data.ToObject();
+ Assert.NotNull(containers);
+ Assert.Equal("running", containers[0].Status);
+ }
+
+ [Fact]
+ public void PveContainer_Deserialize_Pve9_FirstContainer_HasCorrectNode()
+ {
+ var json = TestHelper.LoadFixture("pve9_containers.json");
+ var data = JObject.Parse(json)["data"];
+ var containers = data.ToObject();
+ Assert.NotNull(containers);
+ Assert.Equal("pve1", containers[0].Node);
+ }
+
+ [Fact]
+ public void PveContainer_Deserialize_Pve9_FirstContainer_HasCorrectCpuAndMem()
+ {
+ var json = TestHelper.LoadFixture("pve9_containers.json");
+ var data = JObject.Parse(json)["data"];
+ var containers = data.ToObject();
+ Assert.NotNull(containers);
+ Assert.Equal(2, containers[0].CpuCount);
+ Assert.Equal(2147483648L, containers[0].MaxMem);
+ }
+
+ [Fact]
+ public void PveContainer_Deserialize_Pve9_FirstContainer_HasSwapAndDisk()
+ {
+ var json = TestHelper.LoadFixture("pve9_containers.json");
+ var data = JObject.Parse(json)["data"];
+ var containers = data.ToObject();
+ Assert.NotNull(containers);
+ Assert.Equal(536870912L, containers[0].MaxSwap);
+ Assert.Equal(8589934592L, containers[0].RootFsSize);
+ }
+
+ [Fact]
+ public void PveContainer_Deserialize_Pve9_FirstContainer_IsUnprivileged()
+ {
+ var json = TestHelper.LoadFixture("pve9_containers.json");
+ var data = JObject.Parse(json)["data"];
+ var containers = data.ToObject();
+ Assert.NotNull(containers);
+ Assert.Equal(1, containers[0].Unprivileged);
+ }
+
+ [Fact]
+ public void PveContainer_Deserialize_Pve9_FirstContainer_HasOsType()
+ {
+ var json = TestHelper.LoadFixture("pve9_containers.json");
+ var data = JObject.Parse(json)["data"];
+ var containers = data.ToObject();
+ Assert.NotNull(containers);
+ Assert.Equal("ubuntu", containers[0].OsType);
+ }
+
+ [Fact]
+ public void PveContainer_Deserialize_Pve9_SecondContainer_IsStopped()
+ {
+ var json = TestHelper.LoadFixture("pve9_containers.json");
+ var data = JObject.Parse(json)["data"];
+ var containers = data.ToObject();
+ Assert.NotNull(containers);
+ Assert.Equal(201, containers[1].VmId);
+ Assert.Equal("stopped", containers[1].Status);
+ Assert.Equal(0L, containers[1].Uptime);
+ }
+
+ [Fact]
+ public void PveContainer_OptionalFields_NullWhenMissing()
+ {
+ var json = TestHelper.LoadFixture("pve9_containers.json");
+ var data = JObject.Parse(json)["data"];
+ var containers = data.ToObject();
+ Assert.NotNull(containers);
+ // Tags field is only in the first container
+ Assert.Equal("web", containers[0].Tags);
+ Assert.Null(containers[1].Tags);
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/NetworkModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/NetworkModelTests.cs
new file mode 100644
index 0000000..b76fd37
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Models/NetworkModelTests.cs
@@ -0,0 +1,104 @@
+using Xunit;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Models.Network;
+
+namespace PSProxmoxVE.Core.Tests.Models
+{
+ public class NetworkModelTests
+ {
+ [Fact]
+ public void PveNetwork_Deserialize_Pve9_ReturnsCorrectCount()
+ {
+ var json = TestHelper.LoadFixture("pve9_networks.json");
+ var data = JObject.Parse(json)["data"];
+ var networks = data.ToObject();
+ Assert.NotNull(networks);
+ Assert.Equal(2, networks.Length);
+ }
+
+ [Fact]
+ public void PveNetwork_Deserialize_Pve9_FirstInterface_HasCorrectName()
+ {
+ var json = TestHelper.LoadFixture("pve9_networks.json");
+ var data = JObject.Parse(json)["data"];
+ var networks = data.ToObject();
+ Assert.NotNull(networks);
+ Assert.Equal("vmbr0", networks[0].Iface);
+ }
+
+ [Fact]
+ public void PveNetwork_Deserialize_Pve9_FirstInterface_HasCorrectType()
+ {
+ var json = TestHelper.LoadFixture("pve9_networks.json");
+ var data = JObject.Parse(json)["data"];
+ var networks = data.ToObject();
+ Assert.NotNull(networks);
+ Assert.Equal("bridge", networks[0].Type);
+ }
+
+ [Fact]
+ public void PveNetwork_Deserialize_Pve9_FirstInterface_HasCorrectAddress()
+ {
+ var json = TestHelper.LoadFixture("pve9_networks.json");
+ var data = JObject.Parse(json)["data"];
+ var networks = data.ToObject();
+ Assert.NotNull(networks);
+ Assert.Equal("192.168.1.10", networks[0].Address);
+ Assert.Equal("255.255.255.0", networks[0].Netmask);
+ Assert.Equal("192.168.1.1", networks[0].Gateway);
+ }
+
+ [Fact]
+ public void PveNetwork_Deserialize_Pve9_FirstInterface_HasCidr()
+ {
+ var json = TestHelper.LoadFixture("pve9_networks.json");
+ var data = JObject.Parse(json)["data"];
+ var networks = data.ToObject();
+ Assert.NotNull(networks);
+ Assert.Equal("192.168.1.10/24", networks[0].Cidr);
+ }
+
+ [Fact]
+ public void PveNetwork_Deserialize_Pve9_FirstInterface_HasBridgePorts()
+ {
+ var json = TestHelper.LoadFixture("pve9_networks.json");
+ var data = JObject.Parse(json)["data"];
+ var networks = data.ToObject();
+ Assert.NotNull(networks);
+ Assert.Equal("enp0s31f6", networks[0].BridgePorts);
+ }
+
+ [Fact]
+ public void PveNetwork_Deserialize_Pve9_FirstInterface_Autostart_IsEnabled()
+ {
+ var json = TestHelper.LoadFixture("pve9_networks.json");
+ var data = JObject.Parse(json)["data"];
+ var networks = data.ToObject();
+ Assert.NotNull(networks);
+ Assert.Equal(1, networks[0].Autostart);
+ }
+
+ [Fact]
+ public void PveNetwork_Deserialize_Pve9_OptionalFields_NullWhenMissing()
+ {
+ var json = TestHelper.LoadFixture("pve9_networks.json");
+ var data = JObject.Parse(json)["data"];
+ var networks = data.ToObject();
+ Assert.NotNull(networks);
+ // No VLAN on primary bridge
+ Assert.Null(networks[0].VlanId);
+ Assert.Null(networks[0].Comments);
+ }
+
+ [Fact]
+ public void PveNetwork_Deserialize_Pve9_SecondInterface_HasCorrectAddress()
+ {
+ var json = TestHelper.LoadFixture("pve9_networks.json");
+ var data = JObject.Parse(json)["data"];
+ var networks = data.ToObject();
+ Assert.NotNull(networks);
+ Assert.Equal("vmbr1", networks[1].Iface);
+ Assert.Equal("10.0.0.1/24", networks[1].Cidr);
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/NodeModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/NodeModelTests.cs
new file mode 100644
index 0000000..8f5b53e
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Models/NodeModelTests.cs
@@ -0,0 +1,104 @@
+using Xunit;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Models.Nodes;
+
+namespace PSProxmoxVE.Core.Tests.Models
+{
+ public class NodeModelTests
+ {
+ [Fact]
+ public void PveNode_Deserialize_Pve9()
+ {
+ var json = TestHelper.LoadFixture("pve9_nodes.json");
+ var data = JObject.Parse(json)["data"];
+ var nodes = data.ToObject();
+ Assert.NotNull(nodes);
+ Assert.Equal(2, nodes.Length);
+ Assert.Equal("pve1", nodes[0].Name);
+ Assert.Equal("online", nodes[0].Status);
+ Assert.Equal(16, nodes[0].CpuCount);
+ Assert.Equal(68719476736L, nodes[0].MemoryTotal);
+ }
+
+ [Fact]
+ public void PveNode_Deserialize_Pve9_SecondNode()
+ {
+ var json = TestHelper.LoadFixture("pve9_nodes.json");
+ var data = JObject.Parse(json)["data"];
+ var nodes = data.ToObject();
+ Assert.NotNull(nodes);
+ Assert.Equal("pve2", nodes[1].Name);
+ Assert.Equal("online", nodes[1].Status);
+ Assert.Equal(8, nodes[1].CpuCount);
+ Assert.Equal(34359738368L, nodes[1].MemoryTotal);
+ }
+
+ [Fact]
+ public void PveNode_Deserialize_Pve8()
+ {
+ var json = TestHelper.LoadFixture("pve8_nodes.json");
+ var data = JObject.Parse(json)["data"];
+ var nodes = data.ToObject();
+ Assert.NotNull(nodes);
+ Assert.Single(nodes);
+ Assert.Equal("pve-old", nodes[0].Name);
+ }
+
+ [Fact]
+ public void PveNode_Deserialize_Pve8_HasExpectedCpuAndMemory()
+ {
+ var json = TestHelper.LoadFixture("pve8_nodes.json");
+ var data = JObject.Parse(json)["data"];
+ var nodes = data.ToObject();
+ Assert.NotNull(nodes);
+ Assert.Equal(4, nodes[0].CpuCount);
+ Assert.Equal(16106127360L, nodes[0].MemoryTotal);
+ }
+
+ [Fact]
+ public void PveNodeStatus_Deserialize_Pve9()
+ {
+ var json = TestHelper.LoadFixture("pve9_node_status.json");
+ var data = JObject.Parse(json)["data"];
+ var status = data.ToObject();
+ Assert.NotNull(status);
+ Assert.Equal(0.125, status.CpuUsage);
+ }
+
+ [Fact]
+ public void PveNodeStatus_Deserialize_Pve9_HasMemoryAndUptime()
+ {
+ var json = TestHelper.LoadFixture("pve9_node_status.json");
+ var data = JObject.Parse(json)["data"];
+ var status = data.ToObject();
+ Assert.NotNull(status);
+ Assert.Equal("pve1", status.Name);
+ Assert.Equal(68719476736L, status.MemoryTotal);
+ Assert.Equal(17179869184L, status.MemoryUsed);
+ Assert.Equal(864000L, status.Uptime);
+ }
+
+ [Fact]
+ public void PveNodeStatus_MemoryUsage_IsCalculated()
+ {
+ var json = TestHelper.LoadFixture("pve9_node_status.json");
+ var data = JObject.Parse(json)["data"];
+ var status = data.ToObject();
+ Assert.NotNull(status);
+ Assert.NotNull(status.MemoryUsage);
+ // 17179869184 / 68719476736 * 100 = 25.0
+ Assert.Equal(25.0, status.MemoryUsage!.Value, precision: 5);
+ }
+
+ [Fact]
+ public void PveNode_LoadAverage_IsDeserializedAsArray()
+ {
+ var json = TestHelper.LoadFixture("pve9_nodes.json");
+ var data = JObject.Parse(json)["data"];
+ var nodes = data.ToObject();
+ Assert.NotNull(nodes);
+ Assert.NotNull(nodes[0].LoadAverage);
+ Assert.Equal(3, nodes[0].LoadAverage!.Length);
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/SdnModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/SdnModelTests.cs
new file mode 100644
index 0000000..846272a
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Models/SdnModelTests.cs
@@ -0,0 +1,134 @@
+using Xunit;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Models.Network;
+
+namespace PSProxmoxVE.Core.Tests.Models
+{
+ public class SdnModelTests
+ {
+ [Fact]
+ public void PveSdnZone_Deserialize_Pve9_ReturnsCorrectCount()
+ {
+ var json = TestHelper.LoadFixture("pve9_sdn_zones.json");
+ var data = JObject.Parse(json)["data"];
+ var zones = data.ToObject();
+ Assert.NotNull(zones);
+ Assert.Equal(2, zones.Length);
+ }
+
+ [Fact]
+ public void PveSdnZone_Deserialize_Pve9_FirstZone_HasCorrectId()
+ {
+ var json = TestHelper.LoadFixture("pve9_sdn_zones.json");
+ var data = JObject.Parse(json)["data"];
+ var zones = data.ToObject();
+ Assert.NotNull(zones);
+ Assert.Equal("localzone", zones[0].Zone);
+ }
+
+ [Fact]
+ public void PveSdnZone_Deserialize_Pve9_FirstZone_HasCorrectType()
+ {
+ var json = TestHelper.LoadFixture("pve9_sdn_zones.json");
+ var data = JObject.Parse(json)["data"];
+ var zones = data.ToObject();
+ Assert.NotNull(zones);
+ Assert.Equal("simple", zones[0].Type);
+ }
+
+ [Fact]
+ public void PveSdnZone_Deserialize_Pve9_SecondZone_HasCorrectType()
+ {
+ var json = TestHelper.LoadFixture("pve9_sdn_zones.json");
+ var data = JObject.Parse(json)["data"];
+ var zones = data.ToObject();
+ Assert.NotNull(zones);
+ Assert.Equal("vxlanzone", zones[1].Zone);
+ Assert.Equal("vxlan", zones[1].Type);
+ }
+
+ [Fact]
+ public void PveSdnZone_Deserialize_Pve9_OptionalFields_NullWhenMissing()
+ {
+ var json = TestHelper.LoadFixture("pve9_sdn_zones.json");
+ var data = JObject.Parse(json)["data"];
+ var zones = data.ToObject();
+ Assert.NotNull(zones);
+ // Comments field is not present in the fixture
+ Assert.Null(zones[0].Comments);
+ }
+
+ [Fact]
+ public void PveSdnVnet_Deserialize_Pve9_ReturnsCorrectCount()
+ {
+ var json = TestHelper.LoadFixture("pve9_sdn_vnets.json");
+ var data = JObject.Parse(json)["data"];
+ var vnets = data.ToObject();
+ Assert.NotNull(vnets);
+ Assert.Equal(2, vnets.Length);
+ }
+
+ [Fact]
+ public void PveSdnVnet_Deserialize_Pve9_FirstVnet_HasCorrectId()
+ {
+ var json = TestHelper.LoadFixture("pve9_sdn_vnets.json");
+ var data = JObject.Parse(json)["data"];
+ var vnets = data.ToObject();
+ Assert.NotNull(vnets);
+ Assert.Equal("myvnet", vnets[0].Vnet);
+ }
+
+ [Fact]
+ public void PveSdnVnet_Deserialize_Pve9_FirstVnet_HasCorrectZone()
+ {
+ var json = TestHelper.LoadFixture("pve9_sdn_vnets.json");
+ var data = JObject.Parse(json)["data"];
+ var vnets = data.ToObject();
+ Assert.NotNull(vnets);
+ Assert.Equal("localzone", vnets[0].Zone);
+ }
+
+ [Fact]
+ public void PveSdnVnet_Deserialize_Pve9_FirstVnet_HasTag()
+ {
+ var json = TestHelper.LoadFixture("pve9_sdn_vnets.json");
+ var data = JObject.Parse(json)["data"];
+ var vnets = data.ToObject();
+ Assert.NotNull(vnets);
+ Assert.Equal(100, vnets[0].Tag);
+ }
+
+ [Fact]
+ public void PveSdnVnet_Deserialize_Pve9_FirstVnet_HasAlias()
+ {
+ var json = TestHelper.LoadFixture("pve9_sdn_vnets.json");
+ var data = JObject.Parse(json)["data"];
+ var vnets = data.ToObject();
+ Assert.NotNull(vnets);
+ Assert.Equal("My VNET", vnets[0].Alias);
+ }
+
+ [Fact]
+ public void PveSdnVnet_Deserialize_Pve9_SecondVnet_HasCorrectZoneAndTag()
+ {
+ var json = TestHelper.LoadFixture("pve9_sdn_vnets.json");
+ var data = JObject.Parse(json)["data"];
+ var vnets = data.ToObject();
+ Assert.NotNull(vnets);
+ Assert.Equal("prodnet", vnets[1].Vnet);
+ Assert.Equal("vxlanzone", vnets[1].Zone);
+ Assert.Equal(200, vnets[1].Tag);
+ }
+
+ [Fact]
+ public void PveSdnVnet_Deserialize_Pve9_OptionalFields_NullWhenMissing()
+ {
+ var json = TestHelper.LoadFixture("pve9_sdn_vnets.json");
+ var data = JObject.Parse(json)["data"];
+ var vnets = data.ToObject();
+ Assert.NotNull(vnets);
+ // Comments not present in fixture entries
+ Assert.Null(vnets[0].Comments);
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/SnapshotModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/SnapshotModelTests.cs
new file mode 100644
index 0000000..c48b29d
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Models/SnapshotModelTests.cs
@@ -0,0 +1,101 @@
+using Xunit;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Models.Vms;
+
+namespace PSProxmoxVE.Core.Tests.Models
+{
+ public class SnapshotModelTests
+ {
+ [Fact]
+ public void PveSnapshot_Deserialize_Pve9_ReturnsCorrectCount()
+ {
+ var json = TestHelper.LoadFixture("pve9_snapshots.json");
+ var data = JObject.Parse(json)["data"];
+ var snapshots = data.ToObject();
+ Assert.NotNull(snapshots);
+ Assert.Equal(3, snapshots.Length);
+ }
+
+ [Fact]
+ public void PveSnapshot_Deserialize_Pve9_Current_HasCorrectName()
+ {
+ var json = TestHelper.LoadFixture("pve9_snapshots.json");
+ var data = JObject.Parse(json)["data"];
+ var snapshots = data.ToObject();
+ Assert.NotNull(snapshots);
+ Assert.Equal("current", snapshots[0].Name);
+ }
+
+ [Fact]
+ public void PveSnapshot_Deserialize_Pve9_Current_HasDescription()
+ {
+ var json = TestHelper.LoadFixture("pve9_snapshots.json");
+ var data = JObject.Parse(json)["data"];
+ var snapshots = data.ToObject();
+ Assert.NotNull(snapshots);
+ Assert.Equal("You are here!", snapshots[0].Description);
+ }
+
+ [Fact]
+ public void PveSnapshot_Deserialize_Pve9_Snap1_HasSnapTime()
+ {
+ var json = TestHelper.LoadFixture("pve9_snapshots.json");
+ var data = JObject.Parse(json)["data"];
+ var snapshots = data.ToObject();
+ Assert.NotNull(snapshots);
+ Assert.Equal("snap1", snapshots[1].Name);
+ Assert.Equal(1710000000L, snapshots[1].SnapTime);
+ }
+
+ [Fact]
+ public void PveSnapshot_Deserialize_Pve9_Snap1_HasVmState()
+ {
+ var json = TestHelper.LoadFixture("pve9_snapshots.json");
+ var data = JObject.Parse(json)["data"];
+ var snapshots = data.ToObject();
+ Assert.NotNull(snapshots);
+ Assert.Equal(1, snapshots[1].VmState);
+ }
+
+ [Fact]
+ public void PveSnapshot_Deserialize_Pve9_Snap1_HasParent()
+ {
+ var json = TestHelper.LoadFixture("pve9_snapshots.json");
+ var data = JObject.Parse(json)["data"];
+ var snapshots = data.ToObject();
+ Assert.NotNull(snapshots);
+ Assert.Equal("base", snapshots[1].Parent);
+ }
+
+ [Fact]
+ public void PveSnapshot_Deserialize_Pve9_Base_HasNoVmState()
+ {
+ var json = TestHelper.LoadFixture("pve9_snapshots.json");
+ var data = JObject.Parse(json)["data"];
+ var snapshots = data.ToObject();
+ Assert.NotNull(snapshots);
+ Assert.Equal("base", snapshots[2].Name);
+ Assert.Equal(0, snapshots[2].VmState);
+ }
+
+ [Fact]
+ public void PveSnapshot_Deserialize_Pve9_Current_ParentIsSnap1()
+ {
+ var json = TestHelper.LoadFixture("pve9_snapshots.json");
+ var data = JObject.Parse(json)["data"];
+ var snapshots = data.ToObject();
+ Assert.NotNull(snapshots);
+ Assert.Equal("snap1", snapshots[0].Parent);
+ }
+
+ [Fact]
+ public void PveSnapshot_Deserialize_Pve9_Base_HasOlderSnapTime()
+ {
+ var json = TestHelper.LoadFixture("pve9_snapshots.json");
+ var data = JObject.Parse(json)["data"];
+ var snapshots = data.ToObject();
+ Assert.NotNull(snapshots);
+ Assert.Equal(1709000000L, snapshots[2].SnapTime);
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/StorageModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/StorageModelTests.cs
new file mode 100644
index 0000000..b472acb
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Models/StorageModelTests.cs
@@ -0,0 +1,138 @@
+using Xunit;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Models.Storage;
+
+namespace PSProxmoxVE.Core.Tests.Models
+{
+ public class StorageModelTests
+ {
+ [Fact]
+ public void PveStorage_Deserialize_Pve9_ReturnsCorrectCount()
+ {
+ var json = TestHelper.LoadFixture("pve9_storage.json");
+ var data = JObject.Parse(json)["data"];
+ var storages = data.ToObject();
+ Assert.NotNull(storages);
+ Assert.Equal(3, storages.Length);
+ }
+
+ [Fact]
+ public void PveStorage_Deserialize_Pve9_Local_HasCorrectId()
+ {
+ var json = TestHelper.LoadFixture("pve9_storage.json");
+ var data = JObject.Parse(json)["data"];
+ var storages = data.ToObject();
+ Assert.NotNull(storages);
+ Assert.Equal("local", storages[0].Storage);
+ }
+
+ [Fact]
+ public void PveStorage_Deserialize_Pve9_Local_HasCorrectType()
+ {
+ var json = TestHelper.LoadFixture("pve9_storage.json");
+ var data = JObject.Parse(json)["data"];
+ var storages = data.ToObject();
+ Assert.NotNull(storages);
+ Assert.Equal("dir", storages[0].Type);
+ }
+
+ [Fact]
+ public void PveStorage_Deserialize_Pve9_Local_HasContentTypes()
+ {
+ var json = TestHelper.LoadFixture("pve9_storage.json");
+ var data = JObject.Parse(json)["data"];
+ var storages = data.ToObject();
+ Assert.NotNull(storages);
+ Assert.Equal("iso,vztmpl,backup", storages[0].Content);
+ }
+
+ [Fact]
+ public void PveStorage_Deserialize_Pve9_Local_HasSizeFields()
+ {
+ var json = TestHelper.LoadFixture("pve9_storage.json");
+ var data = JObject.Parse(json)["data"];
+ var storages = data.ToObject();
+ Assert.NotNull(storages);
+ Assert.Equal(107374182400L, storages[0].Total);
+ Assert.Equal(21474836480L, storages[0].Used);
+ Assert.Equal(85899345920L, storages[0].Available);
+ }
+
+ [Fact]
+ public void PveStorage_Deserialize_Pve9_Local_IsEnabled_NotShared()
+ {
+ var json = TestHelper.LoadFixture("pve9_storage.json");
+ var data = JObject.Parse(json)["data"];
+ var storages = data.ToObject();
+ Assert.NotNull(storages);
+ Assert.Equal(1, storages[0].Enabled);
+ Assert.Equal(0, storages[0].Shared);
+ }
+
+ [Fact]
+ public void PveStorage_Deserialize_Pve9_CephPool_IsShared()
+ {
+ var json = TestHelper.LoadFixture("pve9_storage.json");
+ var data = JObject.Parse(json)["data"];
+ var storages = data.ToObject();
+ Assert.NotNull(storages);
+ var ceph = storages[2];
+ Assert.Equal("ceph-pool", ceph.Storage);
+ Assert.Equal("rbd", ceph.Type);
+ Assert.Equal(1, ceph.Shared);
+ }
+
+ [Fact]
+ public void PveStorageContent_Deserialize_Pve9_ReturnsCorrectCount()
+ {
+ var json = TestHelper.LoadFixture("pve9_storage_content.json");
+ var data = JObject.Parse(json)["data"];
+ var items = data.ToObject();
+ Assert.NotNull(items);
+ Assert.Equal(2, items.Length);
+ }
+
+ [Fact]
+ public void PveStorageContent_Deserialize_Pve9_FirstItem_HasCorrectVolId()
+ {
+ var json = TestHelper.LoadFixture("pve9_storage_content.json");
+ var data = JObject.Parse(json)["data"];
+ var items = data.ToObject();
+ Assert.NotNull(items);
+ Assert.Equal("local:iso/ubuntu-24.04-live-server-amd64.iso", items[0].VolId);
+ }
+
+ [Fact]
+ public void PveStorageContent_Deserialize_Pve9_FirstItem_HasCorrectContentType()
+ {
+ var json = TestHelper.LoadFixture("pve9_storage_content.json");
+ var data = JObject.Parse(json)["data"];
+ var items = data.ToObject();
+ Assert.NotNull(items);
+ Assert.Equal("iso", items[0].Content);
+ Assert.Equal("iso", items[0].Format);
+ }
+
+ [Fact]
+ public void PveStorageContent_Deserialize_Pve9_FirstItem_HasSize()
+ {
+ var json = TestHelper.LoadFixture("pve9_storage_content.json");
+ var data = JObject.Parse(json)["data"];
+ var items = data.ToObject();
+ Assert.NotNull(items);
+ Assert.Equal(2774532096L, items[0].Size);
+ }
+
+ [Fact]
+ public void PveStorageContent_Deserialize_Pve9_OptionalFields_NullWhenMissing()
+ {
+ var json = TestHelper.LoadFixture("pve9_storage_content.json");
+ var data = JObject.Parse(json)["data"];
+ var items = data.ToObject();
+ Assert.NotNull(items);
+ // ISO items should not have a VmId or Notes
+ Assert.Null(items[0].VmId);
+ Assert.Null(items[0].Notes);
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/TaskModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/TaskModelTests.cs
new file mode 100644
index 0000000..8047e8f
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Models/TaskModelTests.cs
@@ -0,0 +1,102 @@
+using Xunit;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Models.Vms;
+
+namespace PSProxmoxVE.Core.Tests.Models
+{
+ public class TaskModelTests
+ {
+ [Fact]
+ public void PveTask_Deserialize_Completed_HasCorrectUpid()
+ {
+ var json = TestHelper.LoadFixture("pve9_tasks.json");
+ var data = JObject.Parse(json)["data"];
+ var task = data.ToObject();
+ Assert.NotNull(task);
+ Assert.Equal("UPID:pve1:00001234:00ABCDEF:65F00000:qmstart:100:root@pam:", task.Upid);
+ }
+
+ [Fact]
+ public void PveTask_Deserialize_Completed_StatusIsStopped()
+ {
+ var json = TestHelper.LoadFixture("pve9_tasks.json");
+ var data = JObject.Parse(json)["data"];
+ var task = data.ToObject();
+ Assert.NotNull(task);
+ Assert.Equal("stopped", task.Status);
+ }
+
+ [Fact]
+ public void PveTask_Deserialize_Completed_ExitStatusIsOk()
+ {
+ var json = TestHelper.LoadFixture("pve9_tasks.json");
+ var data = JObject.Parse(json)["data"];
+ var task = data.ToObject();
+ Assert.NotNull(task);
+ Assert.Equal("OK", task.ExitStatus);
+ }
+
+ [Fact]
+ public void PveTask_Deserialize_Completed_IsSuccessful_IsTrue()
+ {
+ var json = TestHelper.LoadFixture("pve9_tasks.json");
+ var data = JObject.Parse(json)["data"];
+ var task = data.ToObject();
+ Assert.NotNull(task);
+ Assert.True(task.IsSuccessful);
+ }
+
+ [Fact]
+ public void PveTask_Deserialize_Running_HasCorrectUpid()
+ {
+ var json = TestHelper.LoadFixture("pve9_task_running.json");
+ var data = JObject.Parse(json)["data"];
+ var task = data.ToObject();
+ Assert.NotNull(task);
+ Assert.Equal("UPID:pve1:00001234:00ABCDEF:65F00000:qmstart:100:root@pam:", task.Upid);
+ }
+
+ [Fact]
+ public void PveTask_Deserialize_Running_StatusIsRunning()
+ {
+ var json = TestHelper.LoadFixture("pve9_task_running.json");
+ var data = JObject.Parse(json)["data"];
+ var task = data.ToObject();
+ Assert.NotNull(task);
+ Assert.Equal("running", task.Status);
+ }
+
+ [Fact]
+ public void PveTask_Deserialize_Running_ExitStatus_IsNull()
+ {
+ var json = TestHelper.LoadFixture("pve9_task_running.json");
+ var data = JObject.Parse(json)["data"];
+ var task = data.ToObject();
+ Assert.NotNull(task);
+ Assert.Null(task.ExitStatus);
+ }
+
+ [Fact]
+ public void PveTask_Deserialize_Running_IsSuccessful_IsFalse()
+ {
+ var json = TestHelper.LoadFixture("pve9_task_running.json");
+ var data = JObject.Parse(json)["data"];
+ var task = data.ToObject();
+ Assert.NotNull(task);
+ Assert.False(task.IsSuccessful);
+ }
+
+ [Fact]
+ public void PveTask_IsSuccessful_RequiresBothStoppedAndOk()
+ {
+ // Stopped but with an error exit status should not be successful
+ var task = new PveTask
+ {
+ Upid = "UPID:pve1:000:000:000:qmstop:100:root@pam:",
+ Status = "stopped",
+ ExitStatus = "Error: some error occurred"
+ };
+ Assert.False(task.IsSuccessful);
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/UserModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/UserModelTests.cs
new file mode 100644
index 0000000..32c8448
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Models/UserModelTests.cs
@@ -0,0 +1,145 @@
+using Xunit;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Models.Users;
+
+namespace PSProxmoxVE.Core.Tests.Models
+{
+ public class UserModelTests
+ {
+ [Fact]
+ public void PveUser_Deserialize_Pve9_ReturnsCorrectCount()
+ {
+ var json = TestHelper.LoadFixture("pve9_users.json");
+ var data = JObject.Parse(json)["data"];
+ var users = data.ToObject();
+ Assert.NotNull(users);
+ Assert.Equal(3, users.Length);
+ }
+
+ [Fact]
+ public void PveUser_Deserialize_Pve9_Root_HasCorrectUserId()
+ {
+ var json = TestHelper.LoadFixture("pve9_users.json");
+ var data = JObject.Parse(json)["data"];
+ var users = data.ToObject();
+ Assert.NotNull(users);
+ Assert.Equal("root@pam", users[0].UserId);
+ }
+
+ [Fact]
+ public void PveUser_Deserialize_Pve9_Root_HasCorrectName()
+ {
+ var json = TestHelper.LoadFixture("pve9_users.json");
+ var data = JObject.Parse(json)["data"];
+ var users = data.ToObject();
+ Assert.NotNull(users);
+ Assert.Equal("Root", users[0].FirstName);
+ Assert.Equal("User", users[0].LastName);
+ }
+
+ [Fact]
+ public void PveUser_Deserialize_Pve9_Root_HasEmail()
+ {
+ var json = TestHelper.LoadFixture("pve9_users.json");
+ var data = JObject.Parse(json)["data"];
+ var users = data.ToObject();
+ Assert.NotNull(users);
+ Assert.Equal("root@example.com", users[0].Email);
+ }
+
+ [Fact]
+ public void PveUser_Deserialize_Pve9_Root_IsEnabled()
+ {
+ var json = TestHelper.LoadFixture("pve9_users.json");
+ var data = JObject.Parse(json)["data"];
+ var users = data.ToObject();
+ Assert.NotNull(users);
+ Assert.Equal(1, users[0].Enabled);
+ }
+
+ [Fact]
+ public void PveUser_Deserialize_Pve9_Admin_HasGroups()
+ {
+ var json = TestHelper.LoadFixture("pve9_users.json");
+ var data = JObject.Parse(json)["data"];
+ var users = data.ToObject();
+ Assert.NotNull(users);
+ Assert.Equal("admin@pve", users[1].UserId);
+ Assert.Equal("admins", users[1].Groups);
+ }
+
+ [Fact]
+ public void PveUser_Deserialize_Pve9_ReadOnly_HasComment()
+ {
+ var json = TestHelper.LoadFixture("pve9_users.json");
+ var data = JObject.Parse(json)["data"];
+ var users = data.ToObject();
+ Assert.NotNull(users);
+ Assert.Equal("readonly@pve", users[2].UserId);
+ Assert.Equal("Read-only audit user", users[2].Comment);
+ }
+
+ [Fact]
+ public void PveUser_Deserialize_Pve9_Root_Groups_IsNull()
+ {
+ var json = TestHelper.LoadFixture("pve9_users.json");
+ var data = JObject.Parse(json)["data"];
+ var users = data.ToObject();
+ Assert.NotNull(users);
+ // Root user has no groups in fixture
+ Assert.Null(users[0].Groups);
+ }
+
+ [Fact]
+ public void PveRole_Deserialize_Pve9_ReturnsCorrectCount()
+ {
+ var json = TestHelper.LoadFixture("pve9_roles.json");
+ var data = JObject.Parse(json)["data"];
+ var roles = data.ToObject();
+ Assert.NotNull(roles);
+ Assert.Equal(2, roles.Length);
+ }
+
+ [Fact]
+ public void PveRole_Deserialize_Pve9_Administrator_HasCorrectId()
+ {
+ var json = TestHelper.LoadFixture("pve9_roles.json");
+ var data = JObject.Parse(json)["data"];
+ var roles = data.ToObject();
+ Assert.NotNull(roles);
+ Assert.Equal("Administrator", roles[0].RoleId);
+ }
+
+ [Fact]
+ public void PveRole_Deserialize_Pve9_Administrator_IsSpecial()
+ {
+ var json = TestHelper.LoadFixture("pve9_roles.json");
+ var data = JObject.Parse(json)["data"];
+ var roles = data.ToObject();
+ Assert.NotNull(roles);
+ Assert.Equal(1, roles[0].Special);
+ }
+
+ [Fact]
+ public void PveRole_Deserialize_Pve9_Administrator_HasPrivileges()
+ {
+ var json = TestHelper.LoadFixture("pve9_roles.json");
+ var data = JObject.Parse(json)["data"];
+ var roles = data.ToObject();
+ Assert.NotNull(roles);
+ Assert.NotNull(roles[0].Privileges);
+ Assert.Contains("VM.Allocate", roles[0].Privileges);
+ }
+
+ [Fact]
+ public void PveRole_Deserialize_Pve9_PVEAuditor_HasLimitedPrivileges()
+ {
+ var json = TestHelper.LoadFixture("pve9_roles.json");
+ var data = JObject.Parse(json)["data"];
+ var roles = data.ToObject();
+ Assert.NotNull(roles);
+ Assert.Equal("PVEAuditor", roles[1].RoleId);
+ Assert.Contains("VM.Audit", roles[1].Privileges);
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/VmModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/VmModelTests.cs
new file mode 100644
index 0000000..ddad0aa
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/Models/VmModelTests.cs
@@ -0,0 +1,163 @@
+using Xunit;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Models.Vms;
+
+namespace PSProxmoxVE.Core.Tests.Models
+{
+ public class VmModelTests
+ {
+ [Fact]
+ public void PveVm_Deserialize_Pve9_ReturnsCorrectCount()
+ {
+ var json = TestHelper.LoadFixture("pve9_vms.json");
+ var data = JObject.Parse(json)["data"];
+ var vms = data.ToObject();
+ Assert.NotNull(vms);
+ Assert.Equal(3, vms.Length);
+ }
+
+ [Fact]
+ public void PveVm_Deserialize_Pve9_FirstVm_HasCorrectVmId()
+ {
+ var json = TestHelper.LoadFixture("pve9_vms.json");
+ var data = JObject.Parse(json)["data"];
+ var vms = data.ToObject();
+ Assert.NotNull(vms);
+ Assert.Equal(100, vms[0].VmId);
+ }
+
+ [Fact]
+ public void PveVm_Deserialize_Pve9_FirstVm_HasCorrectName()
+ {
+ var json = TestHelper.LoadFixture("pve9_vms.json");
+ var data = JObject.Parse(json)["data"];
+ var vms = data.ToObject();
+ Assert.NotNull(vms);
+ Assert.Equal("test-vm-1", vms[0].Name);
+ }
+
+ [Fact]
+ public void PveVm_Deserialize_Pve9_FirstVm_HasCorrectStatus()
+ {
+ var json = TestHelper.LoadFixture("pve9_vms.json");
+ var data = JObject.Parse(json)["data"];
+ var vms = data.ToObject();
+ Assert.NotNull(vms);
+ Assert.Equal("running", vms[0].Status);
+ }
+
+ [Fact]
+ public void PveVm_Deserialize_Pve9_TemplateFlag_IsSet()
+ {
+ var json = TestHelper.LoadFixture("pve9_vms.json");
+ var data = JObject.Parse(json)["data"];
+ var vms = data.ToObject();
+ Assert.NotNull(vms);
+ // Third entry is the template
+ var template = vms[2];
+ Assert.Equal(9000, template.VmId);
+ Assert.Equal(1, template.Template);
+ }
+
+ [Fact]
+ public void PveVm_Deserialize_Pve9_RegularVm_TemplateFlag_IsZero()
+ {
+ var json = TestHelper.LoadFixture("pve9_vms.json");
+ var data = JObject.Parse(json)["data"];
+ var vms = data.ToObject();
+ Assert.NotNull(vms);
+ Assert.Equal(0, vms[0].Template);
+ }
+
+ [Fact]
+ public void PveVm_Deserialize_Pve9_FirstVm_HasExpectedMemoryAndDisk()
+ {
+ var json = TestHelper.LoadFixture("pve9_vms.json");
+ var data = JObject.Parse(json)["data"];
+ var vms = data.ToObject();
+ Assert.NotNull(vms);
+ Assert.Equal(8589934592L, vms[0].MaxMem);
+ Assert.Equal(107374182400L, vms[0].MaxDisk);
+ }
+
+ [Fact]
+ public void PveVm_Deserialize_Pve9_StoppedVm_UptimeIsZero()
+ {
+ var json = TestHelper.LoadFixture("pve9_vms.json");
+ var data = JObject.Parse(json)["data"];
+ var vms = data.ToObject();
+ Assert.NotNull(vms);
+ Assert.Equal(0L, vms[1].Uptime);
+ }
+
+ [Fact]
+ public void PveVmConfig_Deserialize_Pve9_HasCorrectCoresAndMemory()
+ {
+ var json = TestHelper.LoadFixture("pve9_vm_config.json");
+ var data = JObject.Parse(json)["data"];
+ var config = data.ToObject();
+ Assert.NotNull(config);
+ Assert.Equal(4, config.Cores);
+ Assert.Equal(1, config.Sockets);
+ Assert.Equal(8192, config.Memory);
+ }
+
+ [Fact]
+ public void PveVmConfig_Deserialize_Pve9_HasCorrectBiosAndMachine()
+ {
+ var json = TestHelper.LoadFixture("pve9_vm_config.json");
+ var data = JObject.Parse(json)["data"];
+ var config = data.ToObject();
+ Assert.NotNull(config);
+ Assert.Equal("ovmf", config.Bios);
+ Assert.Equal("q35", config.Machine);
+ }
+
+ [Fact]
+ public void PveVmConfig_Deserialize_Pve9_HasCpuTypeAndOsType()
+ {
+ var json = TestHelper.LoadFixture("pve9_vm_config.json");
+ var data = JObject.Parse(json)["data"];
+ var config = data.ToObject();
+ Assert.NotNull(config);
+ Assert.Equal("host", config.CpuType);
+ Assert.Equal("l26", config.OsType);
+ }
+
+ [Fact]
+ public void PveVmConfig_Deserialize_Pve9_HasNetworkInterface()
+ {
+ var json = TestHelper.LoadFixture("pve9_vm_config.json");
+ var data = JObject.Parse(json)["data"];
+ var config = data.ToObject();
+ Assert.NotNull(config);
+ Assert.NotNull(config.Net0);
+ Assert.Contains("vmbr0", config.Net0);
+ }
+
+ [Fact]
+ public void PveVmConfig_Deserialize_Pve9_OptionalFields_NullWhenMissing()
+ {
+ var json = TestHelper.LoadFixture("pve9_vm_config.json");
+ var data = JObject.Parse(json)["data"];
+ var config = data.ToObject();
+ Assert.NotNull(config);
+ // Fields not present in fixture should be null
+ Assert.Null(config.Net1);
+ Assert.Null(config.Virtio1);
+ Assert.Null(config.Args);
+ }
+
+ [Fact]
+ public void PveVmConfig_Deserialize_Pve9_CloudInitFields()
+ {
+ var json = TestHelper.LoadFixture("pve9_vm_config.json");
+ var data = JObject.Parse(json)["data"];
+ var config = data.ToObject();
+ Assert.NotNull(config);
+ Assert.Equal("admin", config.CiUser);
+ Assert.Equal("8.8.8.8", config.Nameserver);
+ Assert.Equal("example.com", config.Searchdomain);
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Core.Tests/TestHelper.cs b/tests/PSProxmoxVE.Core.Tests/TestHelper.cs
new file mode 100644
index 0000000..fa16deb
--- /dev/null
+++ b/tests/PSProxmoxVE.Core.Tests/TestHelper.cs
@@ -0,0 +1,52 @@
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using Moq.Protected;
+
+namespace PSProxmoxVE.Core.Tests
+{
+ public static class TestHelper
+ {
+ public static string LoadFixture(string filename)
+ {
+ var path = Path.Combine("Fixtures", filename);
+ return File.ReadAllText(path);
+ }
+
+ public static Mock CreateMockHandler(string responseBody, HttpStatusCode statusCode = HttpStatusCode.OK)
+ {
+ var mock = new Mock();
+ mock.Protected()
+ .Setup>("SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = statusCode,
+ Content = new StringContent(responseBody)
+ });
+ return mock;
+ }
+
+ public static Mock CreateMockHandlerSequence(params (string body, HttpStatusCode status)[] responses)
+ {
+ var mock = new Mock();
+ var setup = mock.Protected()
+ .SetupSequence>("SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny());
+ foreach (var (body, status) in responses)
+ {
+ setup = setup.ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = status,
+ Content = new StringContent(body)
+ });
+ }
+ return mock;
+ }
+ }
+}