Files
PSProxmoxVE/tests/PSProxmoxVE.Core.Tests/Services/CloudInitServiceTests.cs
T
goodolclint-claude[bot] 3f7f69b250 refactor: route the CloudInit/Templates cmdlets through their service seams (#126) (#200)
Get-PveCloudInitConfig and Get-PveTemplate each built their own PveHttpClient
inline. They now go through CloudInitService and TemplateService, which is
offline-testable per ADR 0021.

Get-PveCloudInitConfig's shipped output is the full PveVmConfig (per its own
synopsis), not the CI-key-filtered PveCloudInitConfig that
CloudInitService.GetCloudInitConfig already returns for a different purpose.
CloudInitService gains GetFullVmConfig, a thin delegation to the existing
VmService.GetVmConfig (already used by Get-PveVmConfig) rather than a
parallel HTTP implementation.

Get-PveTemplate's TemplateService.GetTemplates now forwards VmService.GetVms's
onNodeSkipped callback, matching Get-PveVm/Get-PveContainer, so an unreachable
node during the all-nodes listing produces a warning instead of a silently
short result. Two review-found regressions were fixed before commit: an empty
-Node value used to mean "query all nodes" and now does again (it had
started reaching the API literally as nodes//qemu), and the per-node warning
above.

Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
2026-09-03 00:11:50 +00:00

210 lines
7.7 KiB
C#

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_CallsPutAsyncAndReturnsUpid()
{
// Arrange
var json = @"{""data"": ""UPID:pve1:00001234:00005678:12345678:cloudinit:100:root@pam:""}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.PutAsync("nodes/pve1/qemu/100/cloudinit", null))
.ReturnsAsync(json);
var service = new CloudInitService(mockClient.Object);
// Act
var result = service.RegenerateCloudInitImage(CreateSession(), "pve1", 100);
// Assert
Assert.Contains("UPID:", result);
mockClient.Verify(c => c.PutAsync("nodes/pve1/qemu/100/cloudinit", null), 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!));
}
[Fact]
public void GetFullVmConfig_ReturnsFullConfig_IncludingCloudInitAndOtherFields()
{
// Arrange
var json = @"{""data"": {
""ciuser"": ""ubuntu"",
""ipconfig0"": ""ip=dhcp"",
""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.GetFullVmConfig(CreateSession(), "pve1", 100);
// Assert — cloud-init fields and every other config field are both present
Assert.Equal("ubuntu", config.CiUser);
Assert.Equal("ip=dhcp", config.IpConfig0);
Assert.Equal(4, config.Cores);
Assert.Equal(8192, config.Memory);
mockClient.Verify(c => c.GetAsync("nodes/pve1/qemu/100/config"), Times.Once);
}
[Fact]
public void GetFullVmConfig_EscapesNodeInPath()
{
// Arrange
var json = @"{""data"": {}}";
var mockClient = new Mock<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync("nodes/pve%20node/qemu/100/config")).ReturnsAsync(json);
var service = new CloudInitService(mockClient.Object);
// Act
service.GetFullVmConfig(CreateSession(), "pve node", 100);
// Assert
mockClient.Verify(c => c.GetAsync("nodes/pve%20node/qemu/100/config"), Times.Once);
}
[Fact]
public void GetFullVmConfig_NullSession_ThrowsArgumentNullException()
{
var service = new CloudInitService(new Mock<IPveHttpClient>().Object);
Assert.Throws<ArgumentNullException>(() => service.GetFullVmConfig(null!, "pve1", 100));
}
}
}