using System; using System.Collections.Generic; using System.Linq; using Moq; using PSProxmoxVE.Core.Authentication; using PSProxmoxVE.Core.Client; using PSProxmoxVE.Core.Services; using Xunit; namespace PSProxmoxVE.Core.Tests.Services { public class VmServiceTests { private const string TestNode = "pve1"; private const int TestVmId = 100; private static PveSession CreateSession() => new PveSession("pve1.example.com", 8006, true, "PVE:root@pam:TEST_TOKEN"); [Fact] public void ExecuteGuestCommand_SendsCommandAndArgsAsRepeatedCommandArray() { List>? captured = null; var mockClient = new Mock(); mockClient .Setup(c => c.PostAsync(It.IsAny(), It.IsAny>>())) .Callback>>((_, data) => captured = data.ToList()) .ReturnsAsync("{\"data\":{\"pid\":4242}}"); var service = new VmService(mockClient.Object); var pid = service.ExecuteGuestCommand(CreateSession(), TestNode, TestVmId, "cmd.exe", new[] { "/c", "echo", "WLMARK42" }); Assert.Equal(4242, pid); Assert.NotNull(captured); // Every element (exe + each arg) is its own "command" entry, in order. Assert.All(captured!, kvp => Assert.Equal("command", kvp.Key)); Assert.Equal( new[] { "cmd.exe", "/c", "echo", "WLMARK42" }, captured!.Select(kvp => kvp.Value).ToArray()); } [Fact] public void ExecuteGuestCommand_DoesNotUseInputDataForArgs() { List>? captured = null; var mockClient = new Mock(); mockClient .Setup(c => c.PostAsync(It.IsAny(), It.IsAny>>())) .Callback>>((_, data) => captured = data.ToList()) .ReturnsAsync("{\"data\":{\"pid\":1}}"); var service = new VmService(mockClient.Object); service.ExecuteGuestCommand(CreateSession(), TestNode, TestVmId, "powershell.exe", new[] { "-NoProfile", "-Command", "echo hi" }); Assert.NotNull(captured); // Args are argv, not STDIN — "input-data" must never be emitted. Assert.DoesNotContain(captured!, kvp => kvp.Key == "input-data"); } [Fact] public void ExecuteGuestCommand_NoArgs_SendsSingleCommandEntry() { List>? captured = null; var mockClient = new Mock(); mockClient .Setup(c => c.PostAsync(It.IsAny(), It.IsAny>>())) .Callback>>((_, data) => captured = data.ToList()) .ReturnsAsync("{\"data\":{\"pid\":7}}"); var service = new VmService(mockClient.Object); service.ExecuteGuestCommand(CreateSession(), TestNode, TestVmId, "whoami", null); Assert.NotNull(captured); var only = Assert.Single(captured!); Assert.Equal("command", only.Key); Assert.Equal("whoami", only.Value); } [Fact] public void ExecuteGuestCommand_EmptyArgs_SendsSingleCommandEntry() { List>? captured = null; var mockClient = new Mock(); mockClient .Setup(c => c.PostAsync(It.IsAny(), It.IsAny>>())) .Callback>>((_, data) => captured = data.ToList()) .ReturnsAsync("{\"data\":{\"pid\":9}}"); var service = new VmService(mockClient.Object); service.ExecuteGuestCommand(CreateSession(), TestNode, TestVmId, "whoami", new string[0]); Assert.NotNull(captured); var only = Assert.Single(captured!); Assert.Equal("command", only.Key); Assert.Equal("whoami", only.Value); } [Fact] public void ExecuteGuestCommand_NullArgElement_ThrowsArgumentException() { var mockClient = new Mock(); var service = new VmService(mockClient.Object); var ex = Assert.Throws(() => service.ExecuteGuestCommand(CreateSession(), TestNode, TestVmId, "cmd.exe", new[] { "/c", null!, "echo" })); Assert.Equal("args", ex.ParamName); } [Fact] public void RebootVm_PostsToTheNativeRebootEndpoint() { string? resource = null; var mockClient = new Mock(); mockClient .Setup(c => c.PostAsync(It.IsAny(), It.IsAny>())) .Callback>((r, _) => resource = r) .ReturnsAsync("{\"data\":\"UPID:pve1:00001234:00005678:6A970AAB:qmreboot:100:root@pam:\"}"); var service = new VmService(mockClient.Object); var task = service.RebootVm(CreateSession(), TestNode, TestVmId); // Composing a reboot as shutdown + start races PVE's post-stop cleanup for the // config lock; the native endpoint keeps the whole restart server-side. Assert.Equal($"nodes/{TestNode}/qemu/{TestVmId}/status/reboot", resource); Assert.Contains("qmreboot", task.Upid); } [Fact] public void RebootVm_SendsTimeoutWhenSupplied() { List>? captured = null; var mockClient = new Mock(); mockClient .Setup(c => c.PostAsync(It.IsAny(), It.IsAny>())) .Callback>((_, data) => captured = data.ToList()) .ReturnsAsync("{\"data\":\"UPID:pve1:00001234:00005678:6A970AAB:qmreboot:100:root@pam:\"}"); var service = new VmService(mockClient.Object); service.RebootVm(CreateSession(), TestNode, TestVmId, 45); Assert.NotNull(captured); Assert.Single(captured!); Assert.Equal("timeout", captured![0].Key); Assert.Equal("45", captured![0].Value); } [Fact] public void RebootVm_OmitsTimeoutWhenNotSupplied() { List>? captured = null; var mockClient = new Mock(); mockClient .Setup(c => c.PostAsync(It.IsAny(), It.IsAny>())) .Callback>((_, data) => captured = data.ToList()) .ReturnsAsync("{\"data\":\"UPID:pve1:00001234:00005678:6A970AAB:qmreboot:100:root@pam:\"}"); var service = new VmService(mockClient.Object); service.RebootVm(CreateSession(), TestNode, TestVmId); Assert.NotNull(captured); Assert.Empty(captured!); } [Fact] public void RemoveVm_WithSkipLockTrue_IncludesSkiplockInQueryString() { string? resource = null; var mockClient = new Mock(); mockClient .Setup(c => c.DeleteAsync(It.IsAny())) .Callback(r => resource = r) .ReturnsAsync("{\"data\":\"UPID:pve1:00001234:00005678:6A970AAB:qmremove:100:root@pam:\"}"); var service = new VmService(mockClient.Object); service.RemoveVm(CreateSession(), TestNode, TestVmId, purge: false, skipLock: true); Assert.NotNull(resource); Assert.Contains("skiplock=1", resource!); } [Fact] public void RemoveVm_WithSkipLockFalse_OmitsSkiplockFromQueryString() { string? resource = null; var mockClient = new Mock(); mockClient .Setup(c => c.DeleteAsync(It.IsAny())) .Callback(r => resource = r) .ReturnsAsync("{\"data\":\"UPID:pve1:00001234:00005678:6A970AAB:qmremove:100:root@pam:\"}"); var service = new VmService(mockClient.Object); service.RemoveVm(CreateSession(), TestNode, TestVmId, purge: false, skipLock: false); Assert.NotNull(resource); Assert.DoesNotContain("skiplock", resource!); } [Fact] public void RemoveVm_WithPurgeAndSkipLock_IncludesBothInQueryString() { string? resource = null; var mockClient = new Mock(); mockClient .Setup(c => c.DeleteAsync(It.IsAny())) .Callback(r => resource = r) .ReturnsAsync("{\"data\":\"UPID:pve1:00001234:00005678:6A970AAB:qmremove:100:root@pam:\"}"); var service = new VmService(mockClient.Object); service.RemoveVm(CreateSession(), TestNode, TestVmId, purge: true, skipLock: true); Assert.NotNull(resource); Assert.Contains("purge=1", resource!); Assert.Contains("skiplock=1", resource!); } } }