fix: remediate findings F045, F047, F048, F064, F070, F071, F076-F079

Phase 1 — Trivial fixes:
- F071: Add Uri.EscapeDataString to GetPveTemplateCmdlet node path
- F077: Add ValidateRange(100, 999999999) to GetPveTaskListCmdlet.VmId
- F076: Create .github/dependabot.yml (nuget + github-actions, weekly)
- F079: Fix unit-tests.yml dotnet SDK from 9.0.x to 10.0.x
- F048: Mark wont_fix — sync-over-async accepted for PS 5.1 compat

Phase 2 — Framework targeting (D009 compliance):
- F047: Reduce publishable csproj to netstandard2.0 only, remove all
  #if NET48/NETSTANDARD2_0 conditionals from PveHttpClient.cs,
  restructure build.yml for netstandard2.0 publish + net10.0/net48 tests
- F064: Resolved by F047 — SMA 7.5.0 ItemGroup removed with net10.0 TFM
- F070: Add PS 5.1 smoke-test job to publish.yml (windows-latest)

Phase 3 — IPveHttpClient interface extraction (F045):
- Extract IPveHttpClient interface from PveHttpClient
- Add constructor injection to all 14 service classes
- Services use injected client when available, create+dispose when not

Phase 4 — Service unit tests (F078):
- 196 new xUnit tests across 10 service test files
- All services tested via Moq-mocked IPveHttpClient
- Total test count: 382 (was 186)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-23 13:51:37 -05:00
parent 94424367bf
commit 68dadbfdc0
35 changed files with 5719 additions and 915 deletions
@@ -0,0 +1,424 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using Newtonsoft.Json.Linq;
using Xunit;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Core.Tests.Services
{
public class BackupServiceTests
{
private const string Node = "pve1";
private static PveSession CreateSession()
{
return new PveSession("pve.example.com", 8006, false,
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
}
// ---------------------------------------------------------------
// CreateBackup
// ---------------------------------------------------------------
[Fact]
public void CreateBackup_CallsPostAsync_ReturnsUpid()
{
// Arrange
const string upid = "UPID:pve1:000ABC:00000001:5F1234AB:vzdump:100:root@pam:";
var json = $@"{{""data"": ""{upid}""}}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(json);
var config = new Dictionary<string, string>
{
["vmid"] = "100",
["storage"] = "local",
["mode"] = "snapshot",
["compress"] = "zstd"
};
var service = new BackupService(mockClient.Object);
// Act
var task = service.CreateBackup(CreateSession(), Node, config);
// Assert
Assert.Equal(upid, task.Upid);
Assert.Equal(Node, task.Node);
mockClient.Verify(c => c.PostAsync(
$"nodes/{Node}/vzdump",
It.Is<Dictionary<string, string>>(d => d["vmid"] == "100" && d["storage"] == "local")),
Times.Once);
}
[Fact]
public void CreateBackup_NullSession_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
var config = new Dictionary<string, string> { ["vmid"] = "100" };
Assert.Throws<ArgumentNullException>("session", () => service.CreateBackup(null!, Node, config));
}
[Fact]
public void CreateBackup_NullConfig_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("config", () => service.CreateBackup(CreateSession(), Node, null!));
}
// ---------------------------------------------------------------
// GetBackupJobs
// ---------------------------------------------------------------
[Fact]
public void GetBackupJobs_ReturnsJobArray()
{
// Arrange
var json = @"{
""data"": [
{
""id"": ""backup-001"",
""type"": ""vzdump"",
""enabled"": 1,
""schedule"": ""0 2 * * *"",
""storage"": ""pbs-store"",
""mode"": ""snapshot"",
""vmid"": ""100,101,102"",
""compress"": ""zstd""
},
{
""id"": ""backup-002"",
""type"": ""vzdump"",
""enabled"": 0,
""schedule"": ""0 3 * * 0"",
""storage"": ""local"",
""mode"": ""stop"",
""all"": 1,
""compress"": ""lzo""
}
]
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("cluster/backup"))
.ReturnsAsync(json);
var service = new BackupService(mockClient.Object);
// Act
var jobs = service.GetBackupJobs(CreateSession());
// Assert
Assert.Equal(2, jobs.Length);
Assert.Equal("backup-001", jobs[0].Id);
Assert.Equal("vzdump", jobs[0].Type);
Assert.Equal(1, jobs[0].Enabled);
Assert.Equal("0 2 * * *", jobs[0].Schedule);
Assert.Equal("pbs-store", jobs[0].Storage);
Assert.Equal("snapshot", jobs[0].Mode);
Assert.Equal("100,101,102", jobs[0].VmId);
Assert.Equal("backup-002", jobs[1].Id);
Assert.Equal(0, jobs[1].Enabled);
Assert.Equal(1, jobs[1].All);
}
[Fact]
public void GetBackupJobs_EmptyData_ReturnsEmptyArray()
{
// Arrange
var json = @"{""data"": []}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("cluster/backup"))
.ReturnsAsync(json);
var service = new BackupService(mockClient.Object);
// Act
var jobs = service.GetBackupJobs(CreateSession());
// Assert
Assert.Empty(jobs);
}
[Fact]
public void GetBackupJobs_NullSession_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("session", () => service.GetBackupJobs(null!));
}
// ---------------------------------------------------------------
// GetBackupJob (single)
// ---------------------------------------------------------------
[Fact]
public void GetBackupJob_ReturnsJob()
{
// Arrange
var json = @"{
""data"": {
""id"": ""backup-001"",
""type"": ""vzdump"",
""enabled"": 1,
""schedule"": ""0 2 * * *"",
""storage"": ""pbs-store"",
""mode"": ""snapshot"",
""vmid"": ""100""
}
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("cluster/backup/backup-001"))
.ReturnsAsync(json);
var service = new BackupService(mockClient.Object);
// Act
var job = service.GetBackupJob(CreateSession(), "backup-001");
// Assert
Assert.NotNull(job);
Assert.Equal("backup-001", job!.Id);
Assert.Equal("snapshot", job.Mode);
Assert.Equal("pbs-store", job.Storage);
}
[Fact]
public void GetBackupJob_NullSession_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("session", () => service.GetBackupJob(null!, "backup-001"));
}
[Fact]
public void GetBackupJob_NullId_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("id", () => service.GetBackupJob(CreateSession(), null!));
}
// ---------------------------------------------------------------
// CreateBackupJob
// ---------------------------------------------------------------
[Fact]
public void CreateBackupJob_CallsPostAsync()
{
// Arrange
var json = @"{""data"": null}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(json);
var config = new Dictionary<string, string>
{
["schedule"] = "0 2 * * *",
["storage"] = "local",
["mode"] = "snapshot",
["vmid"] = "100",
["compress"] = "zstd"
};
var service = new BackupService(mockClient.Object);
// Act
service.CreateBackupJob(CreateSession(), config);
// Assert
mockClient.Verify(c => c.PostAsync(
"cluster/backup",
It.Is<Dictionary<string, string>>(d =>
d["schedule"] == "0 2 * * *" &&
d["storage"] == "local")),
Times.Once);
}
[Fact]
public void CreateBackupJob_NullSession_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
var config = new Dictionary<string, string> { ["vmid"] = "100" };
Assert.Throws<ArgumentNullException>("session", () => service.CreateBackupJob(null!, config));
}
[Fact]
public void CreateBackupJob_NullConfig_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("config", () => service.CreateBackupJob(CreateSession(), null!));
}
// ---------------------------------------------------------------
// UpdateBackupJob
// ---------------------------------------------------------------
[Fact]
public void UpdateBackupJob_CallsPutAsync()
{
// Arrange
var json = @"{""data"": null}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PutAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(json);
var config = new Dictionary<string, string>
{
["schedule"] = "0 4 * * *",
["enabled"] = "1"
};
var service = new BackupService(mockClient.Object);
// Act
service.UpdateBackupJob(CreateSession(), "backup-001", config);
// Assert
mockClient.Verify(c => c.PutAsync(
"cluster/backup/backup-001",
It.Is<Dictionary<string, string>>(d => d["schedule"] == "0 4 * * *")),
Times.Once);
}
[Fact]
public void UpdateBackupJob_NullSession_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
var config = new Dictionary<string, string> { ["enabled"] = "1" };
Assert.Throws<ArgumentNullException>("session", () => service.UpdateBackupJob(null!, "id", config));
}
[Fact]
public void UpdateBackupJob_NullId_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
var config = new Dictionary<string, string> { ["enabled"] = "1" };
Assert.Throws<ArgumentNullException>("id", () => service.UpdateBackupJob(CreateSession(), null!, config));
}
[Fact]
public void UpdateBackupJob_NullConfig_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("config", () => service.UpdateBackupJob(CreateSession(), "id", null!));
}
// ---------------------------------------------------------------
// RemoveBackupJob
// ---------------------------------------------------------------
[Fact]
public void RemoveBackupJob_CallsDeleteAsync()
{
// Arrange
var json = @"{""data"": null}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.DeleteAsync(It.IsAny<string>()))
.ReturnsAsync(json);
var service = new BackupService(mockClient.Object);
// Act
service.RemoveBackupJob(CreateSession(), "backup-001");
// Assert
mockClient.Verify(c => c.DeleteAsync("cluster/backup/backup-001"), Times.Once);
}
[Fact]
public void RemoveBackupJob_NullSession_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("session", () => service.RemoveBackupJob(null!, "id"));
}
[Fact]
public void RemoveBackupJob_NullId_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("id", () => service.RemoveBackupJob(CreateSession(), null!));
}
// ---------------------------------------------------------------
// GetNotBackedUp
// ---------------------------------------------------------------
[Fact]
public void GetNotBackedUp_ReturnsJArray()
{
// Arrange
var json = @"{
""data"": [
{ ""vmid"": 100, ""name"": ""webserver"", ""type"": ""qemu"" },
{ ""vmid"": 200, ""name"": ""database"", ""type"": ""lxc"" }
]
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("cluster/backup-info/not-backed-up"))
.ReturnsAsync(json);
var service = new BackupService(mockClient.Object);
// Act
var result = service.GetNotBackedUp(CreateSession());
// Assert
Assert.IsType<JArray>(result);
Assert.Equal(2, result.Count);
Assert.Equal(100, result[0]["vmid"]!.Value<int>());
Assert.Equal("webserver", result[0]["name"]!.Value<string>());
}
[Fact]
public void GetNotBackedUp_EmptyData_ReturnsEmptyJArray()
{
// Arrange
var json = @"{""data"": []}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("cluster/backup-info/not-backed-up"))
.ReturnsAsync(json);
var service = new BackupService(mockClient.Object);
// Act
var result = service.GetNotBackedUp(CreateSession());
// Assert
Assert.Empty(result);
}
[Fact]
public void GetNotBackedUp_NullSession_ThrowsArgumentNullException()
{
var service = new BackupService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("session", () => service.GetNotBackedUp(null!));
}
// ---------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------
[Fact]
public void Constructor_NullClient_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>("client", () => new BackupService(null!));
}
}
}
@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using Xunit;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Core.Tests.Services
{
public class CloudInitServiceTests
{
private static PveSession CreateSession()
{
return new PveSession("pve.example.com", 8006, false,
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
}
[Fact]
public void GetCloudInitConfig_ReturnsConfig()
{
// Arrange
var json = @"{""data"": {
""ciuser"": ""ubuntu"",
""ipconfig0"": ""ip=dhcp"",
""nameserver"": ""8.8.8.8"",
""searchdomain"": ""example.com"",
""sshkeys"": ""ssh-rsa%20AAAA...%20user%40host"",
""boot"": ""order=scsi0;net0"",
""cores"": 4,
""memory"": 8192
}}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("nodes/pve1/qemu/100/config")).ReturnsAsync(json);
var service = new CloudInitService(mockClient.Object);
// Act
var config = service.GetCloudInitConfig(CreateSession(), "pve1", 100);
// Assert
Assert.NotNull(config);
Assert.Equal("ubuntu", config.CiUser);
Assert.Equal("ip=dhcp", config.IpConfig0);
Assert.Equal("8.8.8.8", config.Nameserver);
Assert.Equal("example.com", config.Searchdomain);
Assert.Equal("ssh-rsa%20AAAA...%20user%40host", config.SshKeys);
mockClient.Verify(c => c.GetAsync("nodes/pve1/qemu/100/config"), Times.Once);
}
[Fact]
public void GetCloudInitConfig_ExcludesNonCiFields()
{
// Arrange — response includes non-CI fields that should be ignored
var json = @"{""data"": {
""ciuser"": ""admin"",
""cores"": 4,
""memory"": 8192,
""boot"": ""order=scsi0""
}}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("nodes/pve1/qemu/200/config")).ReturnsAsync(json);
var service = new CloudInitService(mockClient.Object);
// Act
var config = service.GetCloudInitConfig(CreateSession(), "pve1", 200);
// Assert
Assert.Equal("admin", config.CiUser);
// Non-CI fields should not appear in the result
Assert.Null(config.IpConfig0);
Assert.Null(config.Nameserver);
}
[Fact]
public void SetCloudInitConfig_CallsPutAsyncWithCorrectResource()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PutAsync(
"nodes/pve1/qemu/100/config",
It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync("{}");
var service = new CloudInitService(mockClient.Object);
var config = new Dictionary<string, object>
{
["ciuser"] = "ubuntu",
["ipconfig0"] = "ip=dhcp"
};
// Act
service.SetCloudInitConfig(CreateSession(), "pve1", 100, config);
// Assert
mockClient.Verify(c => c.PutAsync(
"nodes/pve1/qemu/100/config",
It.Is<Dictionary<string, string>>(d =>
d["ciuser"] == "ubuntu" && d["ipconfig0"] == "ip=dhcp")),
Times.Once);
}
[Fact]
public void RegenerateCloudInitImage_CallsGetAsyncAndReturnsData()
{
// Arrange
var json = @"{""data"": ""#cloud-config\nuser: ubuntu\npassword: secret\n""}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("nodes/pve1/qemu/100/cloudinit/dump?type=user"))
.ReturnsAsync(json);
var service = new CloudInitService(mockClient.Object);
// Act
var result = service.RegenerateCloudInitImage(CreateSession(), "pve1", 100);
// Assert
Assert.Contains("#cloud-config", result);
Assert.Contains("ubuntu", result);
mockClient.Verify(c => c.GetAsync("nodes/pve1/qemu/100/cloudinit/dump?type=user"), Times.Once);
}
[Fact]
public void GetCloudInitConfig_NullSession_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
var service = new CloudInitService(mockClient.Object);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => service.GetCloudInitConfig(null!, "pve1", 100));
}
[Fact]
public void SetCloudInitConfig_NullSession_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
var service = new CloudInitService(mockClient.Object);
var config = new Dictionary<string, object> { ["ciuser"] = "test" };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => service.SetCloudInitConfig(null!, "pve1", 100, config));
}
[Fact]
public void RegenerateCloudInitImage_NullSession_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
var service = new CloudInitService(mockClient.Object);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => service.RegenerateCloudInitImage(null!, "pve1", 100));
}
[Fact]
public void Constructor_NullClient_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => new CloudInitService(null!));
}
}
}
@@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using Xunit;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Core.Tests.Services
{
public class ClusterServiceTests
{
private static PveSession CreateSession()
{
return new PveSession("pve.example.com", 8006, false,
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
}
[Fact]
public void GetClusterStatus_ReturnsStatusArray()
{
// Arrange
var json = @"{""data"": [
{""type"": ""cluster"", ""name"": ""pve-cluster"", ""nodes"": 3, ""quorate"": 1, ""version"": 5},
{""type"": ""node"", ""name"": ""pve1"", ""online"": 1, ""local"": 1, ""nodeid"": 1, ""ip"": ""10.0.0.1""},
{""type"": ""node"", ""name"": ""pve2"", ""online"": 1, ""local"": 0, ""nodeid"": 2, ""ip"": ""10.0.0.2""}
]}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("cluster/status")).ReturnsAsync(json);
var service = new ClusterService(mockClient.Object);
// Act
var statuses = service.GetClusterStatus(CreateSession());
// Assert
Assert.Equal(3, statuses.Length);
Assert.Equal("cluster", statuses[0].Type);
Assert.Equal("pve-cluster", statuses[0].Name);
Assert.Equal(3, statuses[0].Nodes);
Assert.Equal(1, statuses[0].Quorate);
Assert.Equal("node", statuses[1].Type);
Assert.Equal("pve1", statuses[1].Name);
Assert.Equal(1, statuses[1].Online);
Assert.Equal("10.0.0.1", statuses[1].Ip);
Assert.Equal("node", statuses[2].Type);
Assert.Equal("pve2", statuses[2].Name);
mockClient.Verify(c => c.GetAsync("cluster/status"), Times.Once);
}
[Fact]
public void GetClusterResources_ReturnsResourcesArray()
{
// Arrange
var json = @"{""data"": [
{""id"": ""node/pve1"", ""type"": ""node"", ""node"": ""pve1"", ""status"": ""online"", ""maxcpu"": 16, ""maxmem"": 68719476736, ""cpu"": 0.05},
{""id"": ""qemu/100"", ""type"": ""qemu"", ""node"": ""pve1"", ""name"": ""test-vm"", ""status"": ""running"", ""vmid"": 100, ""maxcpu"": 4, ""maxmem"": 8589934592},
{""id"": ""storage/local"", ""type"": ""storage"", ""node"": ""pve1"", ""status"": ""available"", ""maxdisk"": 107374182400, ""disk"": 53687091200}
]}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("cluster/resources")).ReturnsAsync(json);
var service = new ClusterService(mockClient.Object);
// Act
var resources = service.GetClusterResources(CreateSession());
// Assert
Assert.Equal(3, resources.Length);
Assert.Equal("node/pve1", resources[0].Id);
Assert.Equal("node", resources[0].Type);
Assert.Equal("qemu/100", resources[1].Id);
Assert.Equal("test-vm", resources[1].Name);
Assert.Equal(100, resources[1].VmId);
Assert.Equal("storage/local", resources[2].Id);
Assert.Equal("storage", resources[2].Type);
mockClient.Verify(c => c.GetAsync("cluster/resources"), Times.Once);
}
[Fact]
public void GetClusterResources_WithTypeFilter_AppendsQueryString()
{
// Arrange
var json = @"{""data"": [
{""id"": ""qemu/100"", ""type"": ""qemu"", ""node"": ""pve1"", ""name"": ""test-vm"", ""status"": ""running"", ""vmid"": 100}
]}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("cluster/resources?type=vm")).ReturnsAsync(json);
var service = new ClusterService(mockClient.Object);
// Act
var resources = service.GetClusterResources(CreateSession(), "vm");
// Assert
Assert.Single(resources);
Assert.Equal("qemu/100", resources[0].Id);
mockClient.Verify(c => c.GetAsync("cluster/resources?type=vm"), Times.Once);
}
[Fact]
public void GetClusterStatus_NullSession_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
var service = new ClusterService(mockClient.Object);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => service.GetClusterStatus(null!));
}
[Fact]
public void GetClusterResources_NullSession_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
var service = new ClusterService(mockClient.Object);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => service.GetClusterResources(null!));
}
[Fact]
public void Constructor_NullClient_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => new ClusterService(null!));
}
}
}
@@ -0,0 +1,254 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using Newtonsoft.Json.Linq;
using Xunit;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Core.Tests.Services
{
public class NodeServiceTests
{
private static PveSession CreateSession()
{
return new PveSession("pve.example.com", 8006, false,
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
}
[Fact]
public void GetNodes_ReturnsArrayOfPveNode()
{
// Arrange
var json = @"{""data"": [
{""node"": ""pve1"", ""status"": ""online"", ""maxcpu"": 16, ""maxmem"": 68719476736},
{""node"": ""pve2"", ""status"": ""online"", ""maxcpu"": 8, ""maxmem"": 34359738368}
]}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("nodes")).ReturnsAsync(json);
var service = new NodeService(mockClient.Object);
// Act
var nodes = service.GetNodes(CreateSession());
// Assert
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);
Assert.Equal("pve2", nodes[1].Name);
Assert.Equal(8, nodes[1].CpuCount);
mockClient.Verify(c => c.GetAsync("nodes"), Times.Once);
}
[Fact]
public void GetNodeStatus_ReturnsPveNodeStatus()
{
// Arrange
var json = @"{""data"": {
""node"": ""pve1"", ""status"": ""online"", ""maxcpu"": 16,
""maxmem"": 68719476736, ""mem"": 17179869184,
""uptime"": 864000, ""cpu"": 0.125
}}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("nodes/pve1/status")).ReturnsAsync(json);
var service = new NodeService(mockClient.Object);
// Act
var status = service.GetNodeStatus(CreateSession(), "pve1");
// Assert
Assert.Equal("pve1", status.Node);
Assert.Equal("online", status.Status);
Assert.Equal(0.125, status.CpuUsage);
Assert.Equal(68719476736L, status.MemoryTotal);
Assert.Equal(17179869184L, status.MemoryUsed);
Assert.Equal(864000L, status.Uptime);
mockClient.Verify(c => c.GetAsync("nodes/pve1/status"), Times.Once);
}
[Fact]
public void GetNodeConfig_ReturnsJObject()
{
// Arrange
var json = @"{""data"": {""description"": ""Primary node"", ""wakeonlan"": ""AA:BB:CC:DD:EE:FF""}}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("nodes/pve1/config")).ReturnsAsync(json);
var service = new NodeService(mockClient.Object);
// Act
var config = service.GetNodeConfig(CreateSession(), "pve1");
// Assert
Assert.NotNull(config);
Assert.Equal("Primary node", config["description"]?.ToString());
Assert.Equal("AA:BB:CC:DD:EE:FF", config["wakeonlan"]?.ToString());
mockClient.Verify(c => c.GetAsync("nodes/pve1/config"), Times.Once);
}
[Fact]
public void SetNodeConfig_CallsPutAsyncWithCorrectResource()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PutAsync(
"nodes/pve1/config",
It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync("{}");
var service = new NodeService(mockClient.Object);
var config = new Dictionary<string, string>
{
["description"] = "Updated node"
};
// Act
service.SetNodeConfig(CreateSession(), "pve1", config);
// Assert
mockClient.Verify(c => c.PutAsync(
"nodes/pve1/config",
It.Is<Dictionary<string, string>>(d =>
d["description"] == "Updated node")),
Times.Once);
}
[Fact]
public void GetNodeDns_ReturnsJObject()
{
// Arrange
var json = @"{""data"": {""dns1"": ""8.8.8.8"", ""dns2"": ""8.8.4.4"", ""search"": ""example.com""}}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("nodes/pve1/dns")).ReturnsAsync(json);
var service = new NodeService(mockClient.Object);
// Act
var dns = service.GetNodeDns(CreateSession(), "pve1");
// Assert
Assert.NotNull(dns);
Assert.Equal("8.8.8.8", dns["dns1"]?.ToString());
Assert.Equal("8.8.4.4", dns["dns2"]?.ToString());
Assert.Equal("example.com", dns["search"]?.ToString());
mockClient.Verify(c => c.GetAsync("nodes/pve1/dns"), Times.Once);
}
[Fact]
public void SetNodeDns_CallsPutAsyncWithCorrectResource()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PutAsync(
"nodes/pve1/dns",
It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync("{}");
var service = new NodeService(mockClient.Object);
var config = new Dictionary<string, string>
{
["dns1"] = "1.1.1.1",
["search"] = "lab.local"
};
// Act
service.SetNodeDns(CreateSession(), "pve1", config);
// Assert
mockClient.Verify(c => c.PutAsync(
"nodes/pve1/dns",
It.Is<Dictionary<string, string>>(d =>
d["dns1"] == "1.1.1.1" && d["search"] == "lab.local")),
Times.Once);
}
[Fact]
public void StartAll_CallsPostAsync()
{
// Arrange
var json = @"{""data"": ""UPID:pve1:000ABC:00000001:5F1234AB:startall::root@pam:""}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PostAsync(
"nodes/pve1/startall",
It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(json);
var service = new NodeService(mockClient.Object);
// Act
var task = service.StartAll(CreateSession(), "pve1");
// Assert
Assert.NotNull(task);
Assert.Contains("startall", task.Upid);
Assert.Equal("pve1", task.Node);
mockClient.Verify(c => c.PostAsync(
"nodes/pve1/startall",
It.IsAny<Dictionary<string, string>>()),
Times.Once);
}
[Fact]
public void StopAll_CallsPostAsync()
{
// Arrange
var json = @"{""data"": ""UPID:pve1:000DEF:00000002:5F1234AC:stopall::root@pam:""}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PostAsync(
"nodes/pve1/stopall",
It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(json);
var service = new NodeService(mockClient.Object);
// Act
var task = service.StopAll(CreateSession(), "pve1");
// Assert
Assert.NotNull(task);
Assert.Contains("stopall", task.Upid);
Assert.Equal("pve1", task.Node);
mockClient.Verify(c => c.PostAsync(
"nodes/pve1/stopall",
It.IsAny<Dictionary<string, string>>()),
Times.Once);
}
[Fact]
public void GetNodes_NullSession_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
var service = new NodeService(mockClient.Object);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => service.GetNodes(null!));
}
[Fact]
public void GetNodeStatus_NullSession_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
var service = new NodeService(mockClient.Object);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => service.GetNodeStatus(null!, "pve1"));
}
[Fact]
public void GetNodeStatus_NullNode_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
var service = new NodeService(mockClient.Object);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => service.GetNodeStatus(CreateSession(), null!));
}
[Fact]
public void Constructor_NullClient_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => new NodeService(null!));
}
}
}
@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using Xunit;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Core.Tests.Services
{
public class PoolServiceTests
{
private static PveSession CreateSession()
{
return new PveSession("pve1.example.com", 8006, true, "PVE:root@pam:TEST_TOKEN");
}
[Fact]
public void GetPools_HappyPath_ReturnsArray()
{
// Arrange
var json = @"{
""data"": [
{ ""poolid"": ""dev-pool"", ""comment"": ""Development resources"" },
{ ""poolid"": ""prod-pool"", ""comment"": ""Production resources"" }
]
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("pools"))
.ReturnsAsync(json);
var service = new PoolService(mockClient.Object);
var session = CreateSession();
// Act
var pools = service.GetPools(session);
// Assert
Assert.Equal(2, pools.Length);
Assert.Equal("dev-pool", pools[0].PoolId);
Assert.Equal("Development resources", pools[0].Comment);
Assert.Equal("prod-pool", pools[1].PoolId);
}
[Fact]
public void GetPool_HappyPath_ReturnsSinglePool()
{
// Arrange
var json = @"{
""data"": {
""poolid"": ""dev-pool"",
""comment"": ""Development resources"",
""members"": [
{ ""id"": ""qemu/100"", ""node"": ""pve1"", ""type"": ""qemu"", ""vmid"": 100 }
]
}
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("pools/dev-pool"))
.ReturnsAsync(json);
var service = new PoolService(mockClient.Object);
var session = CreateSession();
// Act
var pool = service.GetPool(session, "dev-pool");
// Assert
Assert.NotNull(pool);
Assert.Equal("dev-pool", pool.PoolId);
Assert.Equal("Development resources", pool.Comment);
Assert.NotNull(pool.Members);
Assert.Single(pool.Members);
}
[Fact]
public void CreatePool_CallsPostAsync()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PostAsync("pools", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync("{}");
var service = new PoolService(mockClient.Object);
var session = CreateSession();
// Act
service.CreatePool(session, "test-pool", "A test pool");
// Assert
mockClient.Verify(c => c.PostAsync("pools",
It.Is<Dictionary<string, string>>(d =>
d["poolid"] == "test-pool" &&
d["comment"] == "A test pool")),
Times.Once);
}
[Fact]
public void UpdatePool_CallsPutAsync()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PutAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync("{}");
var service = new PoolService(mockClient.Object);
var session = CreateSession();
var config = new Dictionary<string, string>
{
{ "comment", "Updated comment" }
};
// Act
service.UpdatePool(session, "dev-pool", config);
// Assert
mockClient.Verify(c => c.PutAsync("pools/dev-pool",
It.Is<Dictionary<string, string>>(d =>
d["comment"] == "Updated comment")),
Times.Once);
}
[Fact]
public void RemovePool_CallsDeleteAsync()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.DeleteAsync(It.IsAny<string>()))
.ReturnsAsync("{}");
var service = new PoolService(mockClient.Object);
var session = CreateSession();
// Act
service.RemovePool(session, "dev-pool");
// Assert
mockClient.Verify(c => c.DeleteAsync("pools/dev-pool"), Times.Once);
}
}
}
@@ -0,0 +1,216 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using Xunit;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Core.Tests.Services
{
public class SnapshotServiceTests
{
private const string Node = "pve1";
private const int VmId = 100;
private static PveSession CreateSession()
{
return new PveSession("pve.example.com", 8006, false,
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
}
[Fact]
public void GetSnapshots_ReturnsSnapshotArray()
{
// Arrange
var json = @"{
""data"": [
{
""name"": ""clean-install"",
""description"": ""Fresh OS install"",
""snaptime"": 1700000000,
""vmstate"": 0,
""parent"": null
},
{
""name"": ""post-update"",
""description"": ""After apt upgrade"",
""snaptime"": 1700100000,
""vmstate"": 1,
""parent"": ""clean-install""
}
]
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
.ReturnsAsync(json);
var service = new SnapshotService(mockClient.Object);
var session = CreateSession();
// Act
var snapshots = service.GetSnapshots(session, Node, VmId);
// Assert
Assert.Equal(2, snapshots.Length);
Assert.Equal("clean-install", snapshots[0].Name);
Assert.Equal("Fresh OS install", snapshots[0].Description);
Assert.Equal(1700000000L, snapshots[0].SnapTime);
Assert.Equal(0, snapshots[0].VmState);
Assert.Null(snapshots[0].Parent);
Assert.Equal("post-update", snapshots[1].Name);
Assert.Equal(1, snapshots[1].VmState);
Assert.Equal("clean-install", snapshots[1].Parent);
mockClient.Verify(c => c.GetAsync($"nodes/{Node}/qemu/{VmId}/snapshot"), Times.Once);
}
[Fact]
public void GetSnapshots_EmptyData_ReturnsEmptyArray()
{
// Arrange
var json = @"{""data"": []}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
.ReturnsAsync(json);
var service = new SnapshotService(mockClient.Object);
// Act
var snapshots = service.GetSnapshots(CreateSession(), Node, VmId);
// Assert
Assert.Empty(snapshots);
}
[Fact]
public void CreateSnapshot_CallsPostAsync_ReturnsUpid()
{
// Arrange
const string upid = "UPID:pve1:000ABC:00000001:5F1234AB:qmsnapshot:100:root@pam:";
var json = $@"{{""data"": ""{upid}""}}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(json);
var service = new SnapshotService(mockClient.Object);
// Act
var task = service.CreateSnapshot(CreateSession(), Node, VmId, "my-snap", "Test snapshot", vmstate: true);
// Assert
Assert.Equal(upid, task.Upid);
Assert.Equal(Node, task.Node);
mockClient.Verify(c => c.PostAsync(
$"nodes/{Node}/qemu/{VmId}/snapshot",
It.Is<Dictionary<string, string>>(d =>
d["snapname"] == "my-snap" &&
d["vmstate"] == "1" &&
d["description"] == "Test snapshot")),
Times.Once);
}
[Fact]
public void CreateSnapshot_WithoutDescription_OmitsDescriptionField()
{
// Arrange
const string upid = "UPID:pve1:000ABC:00000001:5F1234AB:qmsnapshot:100:root@pam:";
var json = $@"{{""data"": ""{upid}""}}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(json);
var service = new SnapshotService(mockClient.Object);
// Act
service.CreateSnapshot(CreateSession(), Node, VmId, "my-snap");
// Assert
mockClient.Verify(c => c.PostAsync(
It.IsAny<string>(),
It.Is<Dictionary<string, string>>(d =>
!d.ContainsKey("description") &&
d["vmstate"] == "0")),
Times.Once);
}
[Fact]
public void RemoveSnapshot_CallsDeleteAsync_ReturnsUpid()
{
// Arrange
const string upid = "UPID:pve1:000DEF:00000002:5F1234AC:qmdelsnap:100:root@pam:";
var json = $@"{{""data"": ""{upid}""}}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.DeleteAsync(It.IsAny<string>()))
.ReturnsAsync(json);
var service = new SnapshotService(mockClient.Object);
// Act
var task = service.RemoveSnapshot(CreateSession(), Node, VmId, "clean-install");
// Assert
Assert.Equal(upid, task.Upid);
Assert.Equal(Node, task.Node);
mockClient.Verify(c => c.DeleteAsync($"nodes/{Node}/qemu/{VmId}/snapshot/clean-install"), Times.Once);
}
[Fact]
public void RollbackSnapshot_CallsPostAsync_ReturnsUpid()
{
// Arrange
const string upid = "UPID:pve1:000GHI:00000003:5F1234AD:qmrollback:100:root@pam:";
var json = $@"{{""data"": ""{upid}""}}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(json);
var service = new SnapshotService(mockClient.Object);
// Act
var task = service.RollbackSnapshot(CreateSession(), Node, VmId, "clean-install");
// Assert
Assert.Equal(upid, task.Upid);
Assert.Equal(Node, task.Node);
mockClient.Verify(c => c.PostAsync(
$"nodes/{Node}/qemu/{VmId}/snapshot/clean-install/rollback",
It.IsAny<Dictionary<string, string>>()),
Times.Once);
}
[Fact]
public void GetSnapshots_NullSession_ThrowsArgumentNullException()
{
var service = new SnapshotService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("session", () => service.GetSnapshots(null!, Node, VmId));
}
[Fact]
public void CreateSnapshot_NullSession_ThrowsArgumentNullException()
{
var service = new SnapshotService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("session", () => service.CreateSnapshot(null!, Node, VmId, "snap"));
}
[Fact]
public void RemoveSnapshot_NullSession_ThrowsArgumentNullException()
{
var service = new SnapshotService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("session", () => service.RemoveSnapshot(null!, Node, VmId, "snap"));
}
[Fact]
public void RollbackSnapshot_NullSession_ThrowsArgumentNullException()
{
var service = new SnapshotService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("session", () => service.RollbackSnapshot(null!, Node, VmId, "snap"));
}
}
}
@@ -0,0 +1,494 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using Xunit;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Core.Tests.Services
{
public class StorageServiceTests
{
private readonly Mock<IPveHttpClient> _mockClient;
private readonly StorageService _service;
private readonly PveSession _session;
public StorageServiceTests()
{
_mockClient = new Mock<IPveHttpClient>();
_service = new StorageService(_mockClient.Object);
_session = new PveSession(
"pve.example.com",
8006,
skipCertificateCheck: true,
apiToken: "root@pam!test=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
}
// -----------------------------------------------------------------
// GetStorages
// -----------------------------------------------------------------
[Fact]
public void GetStorages_ClusterWide_ReturnsStorageArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("storage"))
.ReturnsAsync(@"{""data"":[
{""storage"":""local"",""type"":""dir"",""content"":""images,iso,vztmpl"",""total"":107374182400,""used"":53687091200,""avail"":53687091200,""enabled"":1,""shared"":0,""active"":1},
{""storage"":""local-lvm"",""type"":""lvmthin"",""content"":""images,rootdir"",""total"":214748364800,""used"":107374182400,""avail"":107374182400,""enabled"":1,""shared"":0,""active"":1}
]}");
// Act
var result = _service.GetStorages(_session);
// Assert
Assert.Equal(2, result.Length);
Assert.Equal("local", result[0].Storage);
Assert.Equal("dir", result[0].Type);
Assert.Equal(107374182400L, result[0].Total);
Assert.Equal("local-lvm", result[1].Storage);
Assert.Equal("lvmthin", result[1].Type);
_mockClient.Verify(c => c.GetAsync("storage"), Times.Once);
}
[Fact]
public void GetStorages_ByNode_QueriesNodeEndpoint()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("nodes/pve1/storage"))
.ReturnsAsync(@"{""data"":[{""storage"":""ceph-pool"",""type"":""rbd"",""content"":""images"",""enabled"":1,""shared"":1,""active"":1}]}");
// Act
var result = _service.GetStorages(_session, "pve1");
// Assert
Assert.Single(result);
Assert.Equal("ceph-pool", result[0].Storage);
Assert.Equal("rbd", result[0].Type);
_mockClient.Verify(c => c.GetAsync("nodes/pve1/storage"), Times.Once);
}
[Fact]
public void GetStorages_EmptyData_ReturnsEmptyArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("storage"))
.ReturnsAsync(@"{""data"":[]}");
// Act
var result = _service.GetStorages(_session);
// Assert
Assert.Empty(result);
}
[Fact]
public void GetStorages_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetStorages(null!));
}
// -----------------------------------------------------------------
// GetStorageContent
// -----------------------------------------------------------------
[Fact]
public void GetStorageContent_ReturnsContentArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("nodes/pve1/storage/local/content"))
.ReturnsAsync(@"{""data"":[
{""volid"":""local:iso/debian-12.iso"",""content"":""iso"",""format"":""iso"",""size"":3909091328,""ctime"":1700000000},
{""volid"":""local:vztmpl/ubuntu-22.04-standard.tar.zst"",""content"":""vztmpl"",""format"":""tgz"",""size"":131072000,""ctime"":1700100000}
]}");
// Act
var result = _service.GetStorageContent(_session, "pve1", "local");
// Assert
Assert.Equal(2, result.Length);
Assert.Equal("local:iso/debian-12.iso", result[0].VolId);
Assert.Equal("iso", result[0].Content);
Assert.Equal(3909091328L, result[0].Size);
Assert.Equal("local:vztmpl/ubuntu-22.04-standard.tar.zst", result[1].VolId);
}
[Fact]
public void GetStorageContent_WithContentTypeFilter_AppendsQueryParam()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("nodes/pve1/storage/local/content?content=iso"))
.ReturnsAsync(@"{""data"":[{""volid"":""local:iso/debian-12.iso"",""content"":""iso"",""format"":""iso"",""size"":3909091328}]}");
// Act
var result = _service.GetStorageContent(_session, "pve1", "local", "iso");
// Assert
Assert.Single(result);
Assert.Equal("iso", result[0].Content);
_mockClient.Verify(c => c.GetAsync("nodes/pve1/storage/local/content?content=iso"), Times.Once);
}
[Fact]
public void GetStorageContent_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetStorageContent(null!, "pve1", "local"));
}
[Fact]
public void GetStorageContent_NullNode_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetStorageContent(_session, null!, "local"));
}
[Fact]
public void GetStorageContent_NullStorage_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetStorageContent(_session, "pve1", null!));
}
// -----------------------------------------------------------------
// CreateStorage
// -----------------------------------------------------------------
[Fact]
public void CreateStorage_ReturnsCreatedStorage()
{
// Arrange
var config = new Dictionary<string, object>
{
["storage"] = "nfs-backup",
["type"] = "nfs",
["server"] = "192.168.1.10",
["export"] = "/mnt/backup",
["content"] = "backup"
};
_mockClient.Setup(c => c.PostAsync("storage", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":{""storage"":""nfs-backup"",""type"":""nfs"",""content"":""backup"",""enabled"":1,""shared"":1}}");
// Act
var result = _service.CreateStorage(_session, config);
// Assert
Assert.Equal("nfs-backup", result.Storage);
Assert.Equal("nfs", result.Type);
Assert.Equal("backup", result.Content);
_mockClient.Verify(c => c.PostAsync("storage", It.IsAny<Dictionary<string, string>>()), Times.Once);
}
[Fact]
public void CreateStorage_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateStorage(null!, new Dictionary<string, object>()));
}
[Fact]
public void CreateStorage_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateStorage(_session, null!));
}
// -----------------------------------------------------------------
// UpdateStorage
// -----------------------------------------------------------------
[Fact]
public void UpdateStorage_CallsPutWithCorrectPath()
{
// Arrange
var config = new Dictionary<string, string> { ["content"] = "backup,images" };
_mockClient.Setup(c => c.PutAsync("storage/nfs-backup", config))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.UpdateStorage(_session, "nfs-backup", config);
// Assert
_mockClient.Verify(c => c.PutAsync("storage/nfs-backup", config), Times.Once);
}
[Fact]
public void UpdateStorage_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.UpdateStorage(null!, "local", new Dictionary<string, string>()));
}
[Fact]
public void UpdateStorage_NullStorage_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.UpdateStorage(_session, null!, new Dictionary<string, string>()));
}
[Fact]
public void UpdateStorage_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.UpdateStorage(_session, "local", null!));
}
// -----------------------------------------------------------------
// RemoveStorage
// -----------------------------------------------------------------
[Fact]
public void RemoveStorage_CallsDeleteWithCorrectPath()
{
// Arrange
_mockClient.Setup(c => c.DeleteAsync("storage/nfs-backup"))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.RemoveStorage(_session, "nfs-backup");
// Assert
_mockClient.Verify(c => c.DeleteAsync("storage/nfs-backup"), Times.Once);
}
[Fact]
public void RemoveStorage_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveStorage(null!, "local"));
}
[Fact]
public void RemoveStorage_NullStorage_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveStorage(_session, null!));
}
// -----------------------------------------------------------------
// GetStorageStatus
// -----------------------------------------------------------------
[Fact]
public void GetStorageStatus_ReturnsStatus()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("nodes/pve1/storage/local/status"))
.ReturnsAsync(@"{""data"":{""total"":107374182400,""used"":53687091200,""avail"":53687091200,""active"":1,""enabled"":1,""shared"":0,""type"":""dir"",""content"":""images,iso""}}");
// Act
var result = _service.GetStorageStatus(_session, "pve1", "local");
// Assert
Assert.Equal(107374182400L, result.Total);
Assert.Equal(53687091200L, result.Used);
Assert.Equal(53687091200L, result.Available);
Assert.Equal(1, result.Active);
Assert.Equal("dir", result.Type);
}
[Fact]
public void GetStorageStatus_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetStorageStatus(null!, "pve1", "local"));
}
[Fact]
public void GetStorageStatus_NullNode_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetStorageStatus(_session, null!, "local"));
}
[Fact]
public void GetStorageStatus_NullStorage_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetStorageStatus(_session, "pve1", null!));
}
// -----------------------------------------------------------------
// RemoveContent
// -----------------------------------------------------------------
[Fact]
public void RemoveContent_CallsDeleteWithCorrectPath()
{
// Arrange
_mockClient.Setup(c => c.DeleteAsync("nodes/pve1/storage/local/content/local%3Aiso%2Fdebian-12.iso"))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.RemoveContent(_session, "pve1", "local", "local:iso/debian-12.iso");
// Assert
_mockClient.Verify(c => c.DeleteAsync("nodes/pve1/storage/local/content/local%3Aiso%2Fdebian-12.iso"), Times.Once);
}
[Fact]
public void RemoveContent_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveContent(null!, "pve1", "local", "vol"));
}
[Fact]
public void RemoveContent_NullVolume_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveContent(_session, "pve1", "local", null!));
}
// -----------------------------------------------------------------
// UpdateContent
// -----------------------------------------------------------------
[Fact]
public void UpdateContent_CallsPutWithCorrectPath()
{
// Arrange
var config = new Dictionary<string, string> { ["notes"] = "Weekly backup" };
_mockClient.Setup(c => c.PutAsync(
"nodes/pve1/storage/local/content/local%3Abackup%2Fvzdump-qemu-100.vma.zst",
config))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.UpdateContent(_session, "pve1", "local", "local:backup/vzdump-qemu-100.vma.zst", config);
// Assert
_mockClient.Verify(c => c.PutAsync(
"nodes/pve1/storage/local/content/local%3Abackup%2Fvzdump-qemu-100.vma.zst",
config), Times.Once);
}
[Fact]
public void UpdateContent_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.UpdateContent(null!, "pve1", "local", "vol", new Dictionary<string, string>()));
}
[Fact]
public void UpdateContent_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.UpdateContent(_session, "pve1", "local", "vol", null!));
}
// -----------------------------------------------------------------
// UploadIso
// -----------------------------------------------------------------
[Fact]
public void UploadIso_ReturnsTaskWithUpid()
{
// Arrange
_mockClient.Setup(c => c.UploadFileAsync(
"nodes/pve1/storage/local/upload",
"/tmp/debian-12.iso",
It.IsAny<Dictionary<string, string>>(),
null, null, null))
.ReturnsAsync(@"{""data"":""UPID:pve1:000AAA:00000001:65F00000:upload:local:root@pam:""}");
// Act
var result = _service.UploadIso(_session, "pve1", "local", "/tmp/debian-12.iso");
// Assert
Assert.Contains("UPID:pve1", result.Upid);
Assert.Equal("pve1", result.Node);
}
[Fact]
public void UploadIso_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.UploadIso(null!, "pve1", "local", "/tmp/test.iso"));
}
[Fact]
public void UploadIso_NullFilePath_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.UploadIso(_session, "pve1", "local", null!));
}
// -----------------------------------------------------------------
// DownloadUrl
// -----------------------------------------------------------------
[Fact]
public void DownloadUrl_ReturnsTaskWithUpid()
{
// Arrange
_mockClient.Setup(c => c.PostAsync(
"nodes/pve1/storage/local/download-url",
It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":""UPID:pve1:000BBB:00000002:65F00001:download:local:root@pam:""}");
// Act
var result = _service.DownloadUrl(
_session, "pve1", "local",
"https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img",
"noble-server-cloudimg-amd64.img", "iso");
// Assert
Assert.Contains("UPID:pve1", result.Upid);
Assert.Equal("pve1", result.Node);
}
[Fact]
public void DownloadUrl_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.DownloadUrl(null!, "pve1", "local", "https://example.com/f.iso", "f.iso", "iso"));
}
[Fact]
public void DownloadUrl_NullUrl_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.DownloadUrl(_session, "pve1", "local", null!, "f.iso", "iso"));
}
[Fact]
public void DownloadUrl_NullFilename_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.DownloadUrl(_session, "pve1", "local", "https://example.com/f.iso", null!, "iso"));
}
[Fact]
public void DownloadUrl_NullContentType_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.DownloadUrl(_session, "pve1", "local", "https://example.com/f.iso", "f.iso", null!));
}
// -----------------------------------------------------------------
// AllocateDisk
// -----------------------------------------------------------------
[Fact]
public void AllocateDisk_ReturnsTaskWithUpid()
{
// Arrange
var config = new Dictionary<string, string>
{
["filename"] = "vm-200-disk-0",
["size"] = "32G",
["format"] = "qcow2"
};
_mockClient.Setup(c => c.PostAsync(
"nodes/pve1/storage/local-lvm/content",
config))
.ReturnsAsync(@"{""data"":""UPID:pve1:000CCC:00000003:65F00002:alloc:local-lvm:root@pam:""}");
// Act
var result = _service.AllocateDisk(_session, "pve1", "local-lvm", config);
// Assert
Assert.Contains("UPID:pve1", result.Upid);
Assert.Equal("pve1", result.Node);
}
[Fact]
public void AllocateDisk_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.AllocateDisk(null!, "pve1", "local", new Dictionary<string, string>()));
}
[Fact]
public void AllocateDisk_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.AllocateDisk(_session, "pve1", "local", null!));
}
}
}
@@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using Xunit;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Exceptions;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Core.Tests.Services
{
public class TaskServiceTests
{
private const string TestNode = "pve1";
private const string TestUpid = "UPID:pve1:000ABC:00000001:5F1234AB:qmstart:100:root@pam:";
private static PveSession CreateSession()
{
return new PveSession("pve1.example.com", 8006, true, "PVE:root@pam:TEST_TOKEN");
}
[Fact]
public void GetTask_HappyPath_ReturnsCorrectFields()
{
// Arrange
var json = @"{
""data"": {
""upid"": ""UPID:pve1:000ABC:00000001:5F1234AB:qmstart:100:root@pam:"",
""type"": ""qmstart"",
""status"": ""stopped"",
""exitstatus"": ""OK"",
""node"": ""pve1"",
""starttime"": 1595000000,
""user"": ""root@pam"",
""id"": ""100""
}
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
.ReturnsAsync(json);
var service = new TaskService(mockClient.Object);
var session = CreateSession();
// Act
var task = service.GetTask(session, TestNode, TestUpid);
// Assert
Assert.Equal(TestUpid, task.Upid);
Assert.Equal("qmstart", task.Type);
Assert.Equal("stopped", task.Status);
Assert.Equal("OK", task.ExitStatus);
Assert.Equal(TestNode, task.Node);
Assert.Equal("root@pam", task.User);
Assert.Equal("100", task.Id);
Assert.True(task.IsSuccessful);
}
[Fact]
public void GetTask_NullSession_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
var service = new TaskService(mockClient.Object);
// Act & Assert
var ex = Assert.Throws<ArgumentNullException>(() =>
service.GetTask(null!, TestNode, TestUpid));
Assert.Equal("session", ex.ParamName);
}
[Fact]
public void GetTaskLog_HappyPath_ReturnsLogEntries()
{
// Arrange
var json = @"{
""data"": [
{ ""n"": 1, ""t"": ""starting task qmstart"" },
{ ""n"": 2, ""t"": ""VM 100 started"" },
{ ""n"": 3, ""t"": ""TASK OK"" }
]
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
.ReturnsAsync(json);
var service = new TaskService(mockClient.Object);
var session = CreateSession();
// Act
var logs = service.GetTaskLog(session, TestNode, TestUpid);
// Assert
Assert.Equal(3, logs.Length);
Assert.Equal(1, logs[0].LineNumber);
Assert.Equal("starting task qmstart", logs[0].Text);
Assert.Equal("TASK OK", logs[2].Text);
}
[Fact]
public void GetTasks_HappyPath_ReturnsCorrectCount()
{
// Arrange
var json = @"{
""data"": [
{
""upid"": ""UPID:pve1:000001:00000001:5F1234AB:qmstart:100:root@pam:"",
""type"": ""qmstart"",
""status"": ""stopped"",
""exitstatus"": ""OK"",
""user"": ""root@pam"",
""id"": ""100""
},
{
""upid"": ""UPID:pve1:000002:00000002:5F1234AC:qmstop:101:root@pam:"",
""type"": ""qmstop"",
""status"": ""running"",
""user"": ""root@pam"",
""id"": ""101""
}
]
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync(It.Is<string>(s => s.Contains("tasks?"))))
.ReturnsAsync(json);
var service = new TaskService(mockClient.Object);
var session = CreateSession();
// Act
var tasks = service.GetTasks(session, TestNode, vmid: 100, typeFilter: "qmstart", limit: 10);
// Assert
Assert.Equal(2, tasks.Length);
Assert.Equal("qmstart", tasks[0].Type);
Assert.Equal(TestNode, tasks[0].Node);
Assert.Equal(TestNode, tasks[1].Node);
mockClient.Verify(c => c.GetAsync(It.Is<string>(s =>
s.Contains("limit=10") &&
s.Contains("vmid=100") &&
s.Contains("typefilter=qmstart"))), Times.Once);
}
[Fact]
public void WaitForTask_CompletesWithOK_ReturnsTask()
{
// Arrange
var json = @"{
""data"": {
""upid"": ""UPID:pve1:000ABC:00000001:5F1234AB:qmstart:100:root@pam:"",
""type"": ""qmstart"",
""status"": ""stopped"",
""exitstatus"": ""OK"",
""user"": ""root@pam""
}
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
.ReturnsAsync(json);
var service = new TaskService(mockClient.Object);
var session = CreateSession();
// Act
var task = service.WaitForTask(session, TestNode, TestUpid,
timeout: TimeSpan.FromSeconds(5),
pollInterval: TimeSpan.FromSeconds(1));
// Assert
Assert.Equal("stopped", task.Status);
Assert.Equal("OK", task.ExitStatus);
Assert.True(task.IsSuccessful);
}
[Fact]
public void WaitForTask_FailedExitStatus_ThrowsPveTaskFailedException()
{
// Arrange
var json = @"{
""data"": {
""upid"": ""UPID:pve1:000ABC:00000001:5F1234AB:qmstart:100:root@pam:"",
""type"": ""qmstart"",
""status"": ""stopped"",
""exitstatus"": ""ERROR: VM 100 already running"",
""user"": ""root@pam""
}
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
.ReturnsAsync(json);
var service = new TaskService(mockClient.Object);
var session = CreateSession();
// Act & Assert
var ex = Assert.Throws<PveTaskFailedException>(() =>
service.WaitForTask(session, TestNode, TestUpid,
timeout: TimeSpan.FromSeconds(5),
pollInterval: TimeSpan.FromSeconds(1)));
Assert.Equal(TestUpid, ex.Upid);
Assert.Equal("ERROR: VM 100 already running", ex.ExitStatus);
}
[Fact]
public void WaitForTask_Timeout_ThrowsPveTaskTimeoutException()
{
// Arrange — task stays "running" forever
var json = @"{
""data"": {
""upid"": ""UPID:pve1:000ABC:00000001:5F1234AB:qmstart:100:root@pam:"",
""type"": ""qmstart"",
""status"": ""running"",
""user"": ""root@pam""
}
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
.ReturnsAsync(json);
var service = new TaskService(mockClient.Object);
var session = CreateSession();
// Act & Assert — use very short timeout so the test completes quickly
var timeout = TimeSpan.FromMilliseconds(100);
var ex = Assert.Throws<PveTaskTimeoutException>(() =>
service.WaitForTask(session, TestNode, TestUpid,
timeout: timeout,
pollInterval: TimeSpan.FromMilliseconds(50)));
Assert.Equal(TestUpid, ex.Upid);
Assert.Equal(timeout, ex.Timeout);
}
[Fact]
public void StopTask_CallsDeleteAsyncWithCorrectPath()
{
// Arrange
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.DeleteAsync(It.IsAny<string>()))
.ReturnsAsync("{}");
var service = new TaskService(mockClient.Object);
var session = CreateSession();
// Act
service.StopTask(session, TestNode, TestUpid);
// Assert
var encodedUpid = Uri.EscapeDataString(TestUpid);
mockClient.Verify(c => c.DeleteAsync(
It.Is<string>(s => s == $"nodes/{TestNode}/tasks/{encodedUpid}")),
Times.Once);
}
}
}
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using Xunit;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Core.Tests.Services
{
public class TemplateServiceTests
{
private const string Node = "pve1";
private const int VmId = 9000;
private static PveSession CreateSession()
{
return new PveSession("pve.example.com", 8006, false,
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
}
[Fact]
public void CreateTemplate_CallsPostAsync_ReturnsUpid()
{
// Arrange
const string upid = "UPID:pve1:000ABC:00000001:5F1234AB:qmtemplate:9000:root@pam:";
var json = $@"{{""data"": ""{upid}""}}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(json);
var service = new TemplateService(mockClient.Object);
// Act
var task = service.CreateTemplate(CreateSession(), Node, VmId);
// Assert
Assert.Equal(upid, task.Upid);
Assert.Equal(Node, task.Node);
mockClient.Verify(c => c.PostAsync(
$"nodes/{Node}/qemu/{VmId}/template",
It.IsAny<Dictionary<string, string>>()),
Times.Once);
}
[Fact]
public void CreateTemplate_WithTaskObject_ParsesCorrectly()
{
// Arrange — some PVE versions return a full task object instead of a bare UPID string
var json = @"{
""data"": {
""upid"": ""UPID:pve1:000ABC:00000001:5F1234AB:qmtemplate:9000:root@pam:"",
""type"": ""qmtemplate"",
""status"": ""running"",
""node"": ""pve1"",
""user"": ""root@pam""
}
}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(json);
var service = new TemplateService(mockClient.Object);
// Act
var task = service.CreateTemplate(CreateSession(), Node, VmId);
// Assert
Assert.Equal("UPID:pve1:000ABC:00000001:5F1234AB:qmtemplate:9000:root@pam:", task.Upid);
Assert.Equal("qmtemplate", task.Type);
Assert.Equal(Node, task.Node);
}
[Fact]
public void CreateTemplate_NullSession_ThrowsArgumentNullException()
{
var service = new TemplateService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("session", () => service.CreateTemplate(null!, Node, VmId));
}
[Fact]
public void CreateTemplate_NullNode_ThrowsArgumentNullException()
{
var service = new TemplateService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("node", () => service.CreateTemplate(CreateSession(), null!, VmId));
}
[Fact]
public void CreateTemplate_EmptyNode_ThrowsArgumentNullException()
{
var service = new TemplateService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>("node", () => service.CreateTemplate(CreateSession(), " ", VmId));
}
[Fact]
public void Constructor_NullClient_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>("client", () => new TemplateService(null!));
}
}
}
@@ -0,0 +1,882 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using Xunit;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Core.Tests.Services
{
public class UserServiceTests
{
private readonly Mock<IPveHttpClient> _mockClient;
private readonly UserService _service;
private readonly PveSession _session;
public UserServiceTests()
{
_mockClient = new Mock<IPveHttpClient>();
_service = new UserService(_mockClient.Object);
_session = new PveSession(
"pve.example.com",
8006,
skipCertificateCheck: true,
apiToken: "root@pam!test=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
}
// =================================================================
// Users
// =================================================================
[Fact]
public void GetUsers_ReturnsUserArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/users"))
.ReturnsAsync(@"{""data"":[
{""userid"":""root@pam"",""enable"":1,""email"":""root@example.com"",""firstname"":""Root"",""lastname"":""Admin""},
{""userid"":""deploy@pve"",""enable"":1,""groups"":""admins"",""comment"":""Deployment account""}
]}");
// Act
var result = _service.GetUsers(_session);
// Assert
Assert.Equal(2, result.Length);
Assert.Equal("root@pam", result[0].UserId);
Assert.Equal("root@example.com", result[0].Email);
Assert.Equal("Root", result[0].FirstName);
Assert.Equal("deploy@pve", result[1].UserId);
Assert.Equal("admins", result[1].Groups);
_mockClient.Verify(c => c.GetAsync("access/users"), Times.Once);
}
[Fact]
public void GetUsers_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetUsers(null!));
}
[Fact]
public void GetUser_ReturnsSingleUser()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/users/root%40pam"))
.ReturnsAsync(@"{""data"":{""email"":""root@example.com"",""firstname"":""Root"",""lastname"":""Admin"",""enable"":1}}");
// Act
var result = _service.GetUser(_session, "root@pam");
// Assert
Assert.Equal("root@pam", result.UserId);
Assert.Equal("root@example.com", result.Email);
Assert.Equal(1, result.Enabled);
}
[Fact]
public void GetUser_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetUser(null!, "root@pam"));
}
[Fact]
public void GetUser_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetUser(_session, null!));
}
[Fact]
public void CreateUser_PostsFormData()
{
// Arrange
var config = new Dictionary<string, object>
{
["email"] = "newuser@example.com",
["firstname"] = "New",
["lastname"] = "User"
};
_mockClient.Setup(c => c.PostAsync("access/users", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.CreateUser(_session, "newuser@pve", config);
// Assert
_mockClient.Verify(c => c.PostAsync("access/users",
It.Is<Dictionary<string, string>>(d =>
d["userid"] == "newuser@pve" &&
d["email"] == "newuser@example.com")),
Times.Once);
}
[Fact]
public void CreateUser_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateUser(null!, "user@pve"));
}
[Fact]
public void CreateUser_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateUser(_session, null!));
}
[Fact]
public void SetUser_PutsFormData()
{
// Arrange
var config = new Dictionary<string, object>
{
["email"] = "updated@example.com",
["comment"] = "Updated account"
};
_mockClient.Setup(c => c.PutAsync("access/users/deploy%40pve", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.SetUser(_session, "deploy@pve", config);
// Assert
_mockClient.Verify(c => c.PutAsync("access/users/deploy%40pve",
It.Is<Dictionary<string, string>>(d =>
d["email"] == "updated@example.com" &&
d["comment"] == "Updated account")),
Times.Once);
}
[Fact]
public void SetUser_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.SetUser(null!, "user@pve", new Dictionary<string, object>()));
}
[Fact]
public void SetUser_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.SetUser(_session, null!, new Dictionary<string, object>()));
}
[Fact]
public void SetUser_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.SetUser(_session, "user@pve", null!));
}
[Fact]
public void RemoveUser_CallsDeleteWithCorrectPath()
{
// Arrange
_mockClient.Setup(c => c.DeleteAsync("access/users/deploy%40pve"))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.RemoveUser(_session, "deploy@pve");
// Assert
_mockClient.Verify(c => c.DeleteAsync("access/users/deploy%40pve"), Times.Once);
}
[Fact]
public void RemoveUser_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveUser(null!, "user@pve"));
}
[Fact]
public void RemoveUser_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveUser(_session, null!));
}
// =================================================================
// API Tokens
// =================================================================
[Fact]
public void GetApiTokens_ReturnsTokenArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/users/root%40pam/token"))
.ReturnsAsync(@"{""data"":[
{""tokenid"":""automation"",""privsep"":1,""expire"":0,""comment"":""CI/CD token""},
{""tokenid"":""monitoring"",""privsep"":0,""expire"":1735689600}
]}");
// Act
var result = _service.GetApiTokens(_session, "root@pam");
// Assert
Assert.Equal(2, result.Length);
Assert.Equal("automation", result[0].TokenId);
Assert.Equal("root@pam", result[0].UserId);
Assert.Equal(1, result[0].PrivilegeSeparation);
Assert.Equal("monitoring", result[1].TokenId);
Assert.Equal("root@pam", result[1].UserId);
}
[Fact]
public void GetApiTokens_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetApiTokens(null!, "root@pam"));
}
[Fact]
public void GetApiTokens_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetApiTokens(_session, null!));
}
[Fact]
public void CreateApiToken_ReturnsTokenWithSecret()
{
// Arrange
_mockClient.Setup(c => c.PostAsync(
"access/users/root%40pam/token/deploy",
It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":{""full-tokenid"":""root@pam!deploy"",""value"":""aabbccdd-1122-3344-5566-778899aabbcc"",""info"":{""privsep"":1,""expire"":0}}}");
// Act
var result = _service.CreateApiToken(_session, "root@pam", "deploy", comment: "Deploy token", privilegeSeparation: true);
// Assert
Assert.Equal("root@pam", result.UserId);
Assert.Equal("deploy", result.TokenId);
Assert.Equal("aabbccdd-1122-3344-5566-778899aabbcc", result.Value);
}
[Fact]
public void CreateApiToken_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.CreateApiToken(null!, "root@pam", "token1"));
}
[Fact]
public void CreateApiToken_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.CreateApiToken(_session, null!, "token1"));
}
[Fact]
public void CreateApiToken_NullTokenId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.CreateApiToken(_session, "root@pam", null!));
}
[Fact]
public void RemoveApiToken_CallsDeleteWithCorrectPath()
{
// Arrange
_mockClient.Setup(c => c.DeleteAsync("access/users/root%40pam/token/automation"))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.RemoveApiToken(_session, "root@pam", "automation");
// Assert
_mockClient.Verify(c => c.DeleteAsync("access/users/root%40pam/token/automation"), Times.Once);
}
[Fact]
public void RemoveApiToken_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.RemoveApiToken(null!, "root@pam", "token1"));
}
[Fact]
public void RemoveApiToken_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.RemoveApiToken(_session, null!, "token1"));
}
[Fact]
public void RemoveApiToken_NullTokenId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.RemoveApiToken(_session, "root@pam", null!));
}
[Fact]
public void UpdateApiToken_CallsPutWithCorrectPath()
{
// Arrange
var config = new Dictionary<string, string> { ["comment"] = "Updated comment" };
_mockClient.Setup(c => c.PutAsync("access/users/root%40pam/token/automation", config))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.UpdateApiToken(_session, "root@pam", "automation", config);
// Assert
_mockClient.Verify(c => c.PutAsync("access/users/root%40pam/token/automation", config), Times.Once);
}
[Fact]
public void UpdateApiToken_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateApiToken(null!, "root@pam", "token1", new Dictionary<string, string>()));
}
[Fact]
public void UpdateApiToken_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateApiToken(_session, "root@pam", "token1", null!));
}
// =================================================================
// Roles
// =================================================================
[Fact]
public void GetRoles_ReturnsRoleArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/roles"))
.ReturnsAsync(@"{""data"":[
{""roleid"":""Administrator"",""privs"":""Datastore.Allocate,Datastore.AllocateSpace,Datastore.Audit"",""special"":1},
{""roleid"":""PVEVMAdmin"",""privs"":""VM.Allocate,VM.Config.Disk,VM.Config.CPU"",""special"":1},
{""roleid"":""CustomOps"",""privs"":""VM.PowerMgmt,VM.Console"",""special"":0}
]}");
// Act
var result = _service.GetRoles(_session);
// Assert
Assert.Equal(3, result.Length);
Assert.Equal("Administrator", result[0].RoleId);
Assert.Equal(1, result[0].Special);
Assert.Equal("CustomOps", result[2].RoleId);
Assert.Equal(0, result[2].Special);
}
[Fact]
public void GetRoles_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetRoles(null!));
}
[Fact]
public void CreateRole_PostsFormData()
{
// Arrange
_mockClient.Setup(c => c.PostAsync("access/roles", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.CreateRole(_session, "BackupOperator", "Datastore.Audit,Datastore.AllocateSpace");
// Assert
_mockClient.Verify(c => c.PostAsync("access/roles",
It.Is<Dictionary<string, string>>(d =>
d["roleid"] == "BackupOperator" &&
d["privs"] == "Datastore.Audit,Datastore.AllocateSpace")),
Times.Once);
}
[Fact]
public void CreateRole_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateRole(null!, "role1"));
}
[Fact]
public void CreateRole_NullRoleId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateRole(_session, null!));
}
[Fact]
public void UpdateRole_PutsPrivileges()
{
// Arrange
_mockClient.Setup(c => c.PutAsync("access/roles/BackupOperator", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.UpdateRole(_session, "BackupOperator", "Datastore.Audit,Datastore.AllocateSpace,Datastore.Allocate");
// Assert
_mockClient.Verify(c => c.PutAsync("access/roles/BackupOperator",
It.Is<Dictionary<string, string>>(d =>
d["privs"] == "Datastore.Audit,Datastore.AllocateSpace,Datastore.Allocate")),
Times.Once);
}
[Fact]
public void UpdateRole_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateRole(null!, "role1", "privs"));
}
[Fact]
public void UpdateRole_NullRoleId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateRole(_session, null!, "privs"));
}
[Fact]
public void UpdateRole_NullPrivileges_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateRole(_session, "role1", null!));
}
[Fact]
public void RemoveRole_CallsDeleteWithCorrectPath()
{
// Arrange
_mockClient.Setup(c => c.DeleteAsync("access/roles/BackupOperator"))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.RemoveRole(_session, "BackupOperator");
// Assert
_mockClient.Verify(c => c.DeleteAsync("access/roles/BackupOperator"), Times.Once);
}
[Fact]
public void RemoveRole_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveRole(null!, "role1"));
}
[Fact]
public void RemoveRole_NullRoleId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveRole(_session, null!));
}
// =================================================================
// Groups
// =================================================================
[Fact]
public void GetGroups_ReturnsGroupArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/groups"))
.ReturnsAsync(@"{""data"":[
{""groupid"":""admins"",""comment"":""System administrators"",""users"":""root@pam,admin@pve""},
{""groupid"":""operators"",""comment"":""Operations team""}
]}");
// Act
var result = _service.GetGroups(_session);
// Assert
Assert.Equal(2, result.Length);
Assert.Equal("admins", result[0].GroupId);
Assert.Equal("System administrators", result[0].Comment);
Assert.Equal("root@pam,admin@pve", result[0].Users);
Assert.Equal("operators", result[1].GroupId);
}
[Fact]
public void GetGroups_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetGroups(null!));
}
[Fact]
public void CreateGroup_PostsFormData()
{
// Arrange
_mockClient.Setup(c => c.PostAsync("access/groups", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.CreateGroup(_session, "devops", "DevOps engineering team");
// Assert
_mockClient.Verify(c => c.PostAsync("access/groups",
It.Is<Dictionary<string, string>>(d =>
d["groupid"] == "devops" &&
d["comment"] == "DevOps engineering team")),
Times.Once);
}
[Fact]
public void CreateGroup_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateGroup(null!, "group1"));
}
[Fact]
public void CreateGroup_NullGroupId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateGroup(_session, null!));
}
[Fact]
public void UpdateGroup_PutsConfig()
{
// Arrange
var config = new Dictionary<string, string> { ["comment"] = "Updated description" };
_mockClient.Setup(c => c.PutAsync("access/groups/devops", config))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.UpdateGroup(_session, "devops", config);
// Assert
_mockClient.Verify(c => c.PutAsync("access/groups/devops", config), Times.Once);
}
[Fact]
public void UpdateGroup_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateGroup(null!, "group1", new Dictionary<string, string>()));
}
[Fact]
public void UpdateGroup_NullGroupId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateGroup(_session, null!, new Dictionary<string, string>()));
}
[Fact]
public void UpdateGroup_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateGroup(_session, "group1", null!));
}
[Fact]
public void RemoveGroup_CallsDeleteWithCorrectPath()
{
// Arrange
_mockClient.Setup(c => c.DeleteAsync("access/groups/devops"))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.RemoveGroup(_session, "devops");
// Assert
_mockClient.Verify(c => c.DeleteAsync("access/groups/devops"), Times.Once);
}
[Fact]
public void RemoveGroup_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveGroup(null!, "group1"));
}
[Fact]
public void RemoveGroup_NullGroupId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveGroup(_session, null!));
}
// =================================================================
// Domains / Realms
// =================================================================
[Fact]
public void GetDomains_ReturnsDomainArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/domains"))
.ReturnsAsync(@"{""data"":[
{""realm"":""pam"",""type"":""pam"",""comment"":""Linux PAM standard authentication"",""default"":0},
{""realm"":""pve"",""type"":""pve"",""comment"":""Proxmox VE authentication server"",""default"":1},
{""realm"":""corp-ldap"",""type"":""ldap"",""comment"":""Corporate LDAP""}
]}");
// Act
var result = _service.GetDomains(_session);
// Assert
Assert.Equal(3, result.Length);
Assert.Equal("pam", result[0].Realm);
Assert.Equal("pam", result[0].Type);
Assert.Equal("pve", result[1].Realm);
Assert.Equal(1, result[1].Default);
Assert.Equal("corp-ldap", result[2].Realm);
Assert.Equal("ldap", result[2].Type);
}
[Fact]
public void GetDomains_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetDomains(null!));
}
[Fact]
public void CreateDomain_PostsConfig()
{
// Arrange
var config = new Dictionary<string, string>
{
["realm"] = "corp-ad",
["type"] = "ad",
["server1"] = "dc01.corp.local",
["domain"] = "corp.local"
};
_mockClient.Setup(c => c.PostAsync("access/domains", config))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.CreateDomain(_session, config);
// Assert
_mockClient.Verify(c => c.PostAsync("access/domains", config), Times.Once);
}
[Fact]
public void CreateDomain_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.CreateDomain(null!, new Dictionary<string, string>()));
}
[Fact]
public void CreateDomain_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.CreateDomain(_session, null!));
}
[Fact]
public void UpdateDomain_PutsConfig()
{
// Arrange
var config = new Dictionary<string, string> { ["comment"] = "Updated AD realm" };
_mockClient.Setup(c => c.PutAsync("access/domains/corp-ad", config))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.UpdateDomain(_session, "corp-ad", config);
// Assert
_mockClient.Verify(c => c.PutAsync("access/domains/corp-ad", config), Times.Once);
}
[Fact]
public void UpdateDomain_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateDomain(null!, "pam", new Dictionary<string, string>()));
}
[Fact]
public void UpdateDomain_NullRealm_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateDomain(_session, null!, new Dictionary<string, string>()));
}
[Fact]
public void UpdateDomain_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateDomain(_session, "pam", null!));
}
[Fact]
public void RemoveDomain_CallsDeleteWithCorrectPath()
{
// Arrange
_mockClient.Setup(c => c.DeleteAsync("access/domains/corp-ad"))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.RemoveDomain(_session, "corp-ad");
// Assert
_mockClient.Verify(c => c.DeleteAsync("access/domains/corp-ad"), Times.Once);
}
[Fact]
public void RemoveDomain_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveDomain(null!, "pam"));
}
[Fact]
public void RemoveDomain_NullRealm_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveDomain(_session, null!));
}
// =================================================================
// Password
// =================================================================
[Fact]
public void ChangePassword_PutsCredentials()
{
// Arrange
_mockClient.Setup(c => c.PutAsync("access/password", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.ChangePassword(_session, "deploy@pve", "newSecureP@ss!");
// Assert
_mockClient.Verify(c => c.PutAsync("access/password",
It.Is<Dictionary<string, string>>(d =>
d["userid"] == "deploy@pve" &&
d["password"] == "newSecureP@ss!")),
Times.Once);
}
[Fact]
public void ChangePassword_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.ChangePassword(null!, "user@pve", "pass"));
}
[Fact]
public void ChangePassword_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.ChangePassword(_session, null!, "pass"));
}
[Fact]
public void ChangePassword_NullPassword_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.ChangePassword(_session, "user@pve", null!));
}
// =================================================================
// Permissions / ACLs
// =================================================================
[Fact]
public void GetPermissions_ReturnsPermissionsFromObjectResponse()
{
// Arrange — the PVE API returns permissions as a path-keyed object
_mockClient.Setup(c => c.GetAsync("access/permissions"))
.ReturnsAsync(@"{""data"":{
""/"":{ ""Datastore.Audit"":1, ""VM.Audit"":1 },
""/nodes/pve1"":{ ""Sys.Console"":1 }
}}");
// Act
var result = _service.GetPermissions(_session);
// Assert
Assert.Equal(2, result.Length);
Assert.Contains(result, p => p.Path == "/");
Assert.Contains(result, p => p.Path == "/nodes/pve1");
}
[Fact]
public void GetPermissions_WithUserIdFilter_AppendsQueryParam()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/permissions?userid=deploy%40pve"))
.ReturnsAsync(@"{""data"":{""/"":{}}}");
// Act
var result = _service.GetPermissions(_session, userId: "deploy@pve");
// Assert
_mockClient.Verify(c => c.GetAsync("access/permissions?userid=deploy%40pve"), Times.Once);
Assert.Single(result);
}
[Fact]
public void GetPermissions_WithPathFilter_AppendsQueryParam()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/permissions?path=%2Fvms%2F100"))
.ReturnsAsync(@"{""data"":{}}");
// Act
var result = _service.GetPermissions(_session, path: "/vms/100");
// Assert
_mockClient.Verify(c => c.GetAsync("access/permissions?path=%2Fvms%2F100"), Times.Once);
Assert.Empty(result);
}
[Fact]
public void GetPermissions_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetPermissions(null!));
}
[Fact]
public void SetPermission_PutsAclData()
{
// Arrange
_mockClient.Setup(c => c.PutAsync("access/acl", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.SetPermission(_session, "/vms/100", "PVEVMAdmin", users: "deploy@pve", propagate: true);
// Assert
_mockClient.Verify(c => c.PutAsync("access/acl",
It.Is<Dictionary<string, string>>(d =>
d["path"] == "/vms/100" &&
d["roles"] == "PVEVMAdmin" &&
d["users"] == "deploy@pve" &&
d["propagate"] == "1" &&
d["delete"] == "0")),
Times.Once);
}
[Fact]
public void SetPermission_WithGroupAndDelete_PutsCorrectFlags()
{
// Arrange
_mockClient.Setup(c => c.PutAsync("access/acl", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.SetPermission(_session, "/", "Administrator", groups: "admins", propagate: false, delete: true);
// Assert
_mockClient.Verify(c => c.PutAsync("access/acl",
It.Is<Dictionary<string, string>>(d =>
d["path"] == "/" &&
d["roles"] == "Administrator" &&
d["groups"] == "admins" &&
d["propagate"] == "0" &&
d["delete"] == "1")),
Times.Once);
}
[Fact]
public void SetPermission_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.SetPermission(null!, "/", "Admin"));
}
[Fact]
public void SetPermission_NullPath_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.SetPermission(_session, null!, "Admin"));
}
[Fact]
public void SetPermission_NullRoles_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.SetPermission(_session, "/", null!));
}
}
}