mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-08-21 02:06:36 +00:00
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:
@@ -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!));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user