test: add xUnit unit tests for core library

144 tests covering authentication (version parsing, session expiry, token
format validation), model deserialization from real PVE 8.x/9.x API JSON
fixtures, and service layer behavior. Includes TestHelper with mock
HttpMessageHandler factory and 21 JSON fixture files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-17 15:48:01 -05:00
parent c5d6cde6eb
commit c1a9dd5ca1
35 changed files with 1604 additions and 0 deletions
@@ -0,0 +1,85 @@
using System;
using Xunit;
using PSProxmoxVE.Core.Authentication;
namespace PSProxmoxVE.Core.Tests.Authentication
{
/// <summary>
/// Tests for PveAuthenticator input validation. These tests exercise the
/// argument-validation logic without making actual HTTP calls.
/// </summary>
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<ArgumentException>(() =>
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<ArgumentException>(() =>
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<ArgumentException>(() =>
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<ArgumentException>(() =>
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<ArgumentException>(() =>
PveAuthenticator.AuthenticateWithCredentials(hostname, ValidPort, false, "root@pam", "password"));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void ValidateHostname_NullOrEmpty_Throws_ForApiToken(string hostname)
{
Assert.Throws<ArgumentException>(() =>
PveAuthenticator.AuthenticateWithApiToken(hostname, ValidPort, false, "root@pam!mytoken=uuid"));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void ValidateApiToken_NullOrEmpty_Throws(string token)
{
Assert.Throws<ArgumentException>(() =>
PveAuthenticator.AuthenticateWithApiToken(ValidHostname, ValidPort, false, token));
}
}
}
@@ -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);
}
}
}
@@ -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<ArgumentException>(() => PveVersion.Parse(input));
}
[Fact]
public void Parse_InvalidFormat_Throws()
{
Assert.Throws<FormatException>(() => 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());
}
}
}
@@ -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"]}]}
@@ -0,0 +1 @@
{"data":{"version":"8.3-2","release":"8.3","repoid":"12345678","console":"xtermjs","keyboard":"en-us"}}
@@ -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":""}]}
@@ -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}]}
@@ -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}]}
@@ -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"}}
@@ -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"]}]}
@@ -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}]}
@@ -0,0 +1 @@
{"data":[{"vnet":"myvnet","zone":"localzone","tag":100,"alias":"My VNET"},{"vnet":"prodnet","zone":"vxlanzone","tag":200,"alias":"Production Network","vlanaware":1}]}
@@ -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}]}
@@ -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}]}
@@ -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}]}
@@ -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}]}
@@ -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"}}
@@ -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"}}
@@ -0,0 +1 @@
{"data":{"username":"root@pam","ticket":"PVE:root@pam:65F00000::abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890","CSRFPreventionToken":"65F00000:abcdef1234567890abcdef12"}}
@@ -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}]}
@@ -0,0 +1 @@
{"data":{"version":"9.1-1","release":"9.1","repoid":"abcdef12","console":"xtermjs","keyboard":"en-us"}}
@@ -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"}}
@@ -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}]}
@@ -0,0 +1 @@
{"errors":{"vmid":"property is missing and it is not optional"},"data":null}
@@ -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<PveClusterStatus[]>();
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<PveClusterStatus[]>();
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<PveClusterStatus[]>();
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<PveClusterStatus[]>();
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<PveClusterStatus[]>();
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<PveClusterStatus[]>();
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<PveClusterStatus[]>();
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<PveClusterStatus[]>();
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<PveClusterStatus[]>();
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<PveClusterStatus[]>();
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<PveClusterStatus[]>();
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<PveClusterStatus[]>();
Assert.NotNull(entries);
// Node-type entries should not have cluster-specific fields
Assert.Null(entries[1].Nodes);
Assert.Null(entries[1].Quorate);
}
}
}
@@ -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<PveContainer[]>();
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<PveContainer[]>();
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<PveContainer[]>();
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<PveContainer[]>();
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<PveContainer[]>();
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<PveContainer[]>();
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<PveContainer[]>();
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<PveContainer[]>();
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<PveContainer[]>();
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<PveContainer[]>();
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<PveContainer[]>();
Assert.NotNull(containers);
// Tags field is only in the first container
Assert.Equal("web", containers[0].Tags);
Assert.Null(containers[1].Tags);
}
}
}
@@ -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<PveNetwork[]>();
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<PveNetwork[]>();
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<PveNetwork[]>();
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<PveNetwork[]>();
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<PveNetwork[]>();
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<PveNetwork[]>();
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<PveNetwork[]>();
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<PveNetwork[]>();
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<PveNetwork[]>();
Assert.NotNull(networks);
Assert.Equal("vmbr1", networks[1].Iface);
Assert.Equal("10.0.0.1/24", networks[1].Cidr);
}
}
}
@@ -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<PveNode[]>();
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<PveNode[]>();
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<PveNode[]>();
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<PveNode[]>();
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<PveNodeStatus>();
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<PveNodeStatus>();
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<PveNodeStatus>();
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<PveNode[]>();
Assert.NotNull(nodes);
Assert.NotNull(nodes[0].LoadAverage);
Assert.Equal(3, nodes[0].LoadAverage!.Length);
}
}
}
@@ -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<PveSdnZone[]>();
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<PveSdnZone[]>();
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<PveSdnZone[]>();
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<PveSdnZone[]>();
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<PveSdnZone[]>();
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<PveSdnVnet[]>();
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<PveSdnVnet[]>();
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<PveSdnVnet[]>();
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<PveSdnVnet[]>();
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<PveSdnVnet[]>();
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<PveSdnVnet[]>();
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<PveSdnVnet[]>();
Assert.NotNull(vnets);
// Comments not present in fixture entries
Assert.Null(vnets[0].Comments);
}
}
}
@@ -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<PveSnapshot[]>();
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<PveSnapshot[]>();
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<PveSnapshot[]>();
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<PveSnapshot[]>();
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<PveSnapshot[]>();
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<PveSnapshot[]>();
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<PveSnapshot[]>();
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<PveSnapshot[]>();
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<PveSnapshot[]>();
Assert.NotNull(snapshots);
Assert.Equal(1709000000L, snapshots[2].SnapTime);
}
}
}
@@ -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<PveStorage[]>();
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<PveStorage[]>();
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<PveStorage[]>();
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<PveStorage[]>();
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<PveStorage[]>();
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<PveStorage[]>();
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<PveStorage[]>();
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<PveStorageContent[]>();
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<PveStorageContent[]>();
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<PveStorageContent[]>();
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<PveStorageContent[]>();
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<PveStorageContent[]>();
Assert.NotNull(items);
// ISO items should not have a VmId or Notes
Assert.Null(items[0].VmId);
Assert.Null(items[0].Notes);
}
}
}
@@ -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<PveTask>();
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<PveTask>();
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<PveTask>();
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<PveTask>();
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<PveTask>();
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<PveTask>();
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<PveTask>();
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<PveTask>();
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);
}
}
}
@@ -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<PveUser[]>();
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<PveUser[]>();
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<PveUser[]>();
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<PveUser[]>();
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<PveUser[]>();
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<PveUser[]>();
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<PveUser[]>();
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<PveUser[]>();
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<PveRole[]>();
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<PveRole[]>();
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<PveRole[]>();
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<PveRole[]>();
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<PveRole[]>();
Assert.NotNull(roles);
Assert.Equal("PVEAuditor", roles[1].RoleId);
Assert.Contains("VM.Audit", roles[1].Privileges);
}
}
}
@@ -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<PveVm[]>();
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<PveVm[]>();
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<PveVm[]>();
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<PveVm[]>();
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<PveVm[]>();
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<PveVm[]>();
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<PveVm[]>();
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<PveVm[]>();
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<PveVmConfig>();
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<PveVmConfig>();
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<PveVmConfig>();
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<PveVmConfig>();
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<PveVmConfig>();
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<PveVmConfig>();
Assert.NotNull(config);
Assert.Equal("admin", config.CiUser);
Assert.Equal("8.8.8.8", config.Nameserver);
Assert.Equal("example.com", config.Searchdomain);
}
}
}
@@ -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<HttpMessageHandler> CreateMockHandler(string responseBody, HttpStatusCode statusCode = HttpStatusCode.OK)
{
var mock = new Mock<HttpMessageHandler>();
mock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(responseBody)
});
return mock;
}
public static Mock<HttpMessageHandler> CreateMockHandlerSequence(params (string body, HttpStatusCode status)[] responses)
{
var mock = new Mock<HttpMessageHandler>();
var setup = mock.Protected()
.SetupSequence<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>());
foreach (var (body, status) in responses)
{
setup = setup.ReturnsAsync(new HttpResponseMessage
{
StatusCode = status,
Content = new StringContent(body)
});
}
return mock;
}
}
}