mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-09-03 18:55:33 +00:00
b43948b696
* refactor: route the storage cmdlets through StorageService (#126) New-PveStorage, Invoke-PveStorageDownload and Send-PveFile each built their own PveHttpClient. They now call StorageService, which already had CreateStorage/DownloadUrl/UploadIso with zero callers. StorageService.UploadIso previously hardcoded content=iso regardless of the cmdlet's ContentType parameter (iso/vztmpl/import), which would have silently broken vztmpl/import uploads on conversion; it now takes an optional contentType parameter. DownloadUrl gained an optional timeout parameter so Invoke-PveStorageDownload -TimeoutSeconds keeps working, matching UploadIso's existing default. Per issue #194, both cmdlets construct a fresh StorageService() with no injected client so the timeout override reaches PveServiceBase.CreateClient instead of being silently dropped. ParseTask now stamps Status = "running" on the UPID-string branch, matching what the cmdlets stamped locally before conversion (same rule PR #196 established for SnapshotService). * test: add StorageService coverage for the #126 storage seam --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
782 lines
29 KiB
C#
782 lines
29 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 StorageServiceTests
|
|
{
|
|
private sealed class CapturedPost
|
|
{
|
|
public int Calls { get; set; }
|
|
public string? Path { get; set; }
|
|
public Dictionary<string, string>? Form { get; set; }
|
|
}
|
|
|
|
private static CapturedPost CapturePost(Mock<IPveHttpClient> mockClient, string json)
|
|
{
|
|
var captured = new CapturedPost();
|
|
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
|
|
.Callback<string, Dictionary<string, string>?>((path, form) =>
|
|
{
|
|
captured.Calls++;
|
|
captured.Path = path;
|
|
captured.Form = form;
|
|
})
|
|
.ReturnsAsync(json);
|
|
return captured;
|
|
}
|
|
|
|
private sealed class CapturedUpload
|
|
{
|
|
public int Calls { get; set; }
|
|
public string? Path { get; set; }
|
|
public Dictionary<string, string>? Fields { get; set; }
|
|
}
|
|
|
|
private static CapturedUpload CaptureUpload(Mock<IPveHttpClient> mockClient, string json)
|
|
{
|
|
var captured = new CapturedUpload();
|
|
mockClient.Setup(c => c.UploadFileAsync(
|
|
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Dictionary<string, string>>(),
|
|
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Action<long, long>>()))
|
|
.Callback<string, string, Dictionary<string, string>?, string?, string?, Action<long, long>?>(
|
|
(path, _, fields, _, _, _) =>
|
|
{
|
|
captured.Calls++;
|
|
captured.Path = path;
|
|
captured.Fields = fields;
|
|
})
|
|
.ReturnsAsync(json);
|
|
return captured;
|
|
}
|
|
|
|
/// <summary>Test seam that records the timeout override the base class receives for a call.</summary>
|
|
private sealed class TimeoutCapturingStorageService : StorageService
|
|
{
|
|
private readonly IPveHttpClient _client;
|
|
public TimeSpan? SeenTimeout;
|
|
|
|
public TimeoutCapturingStorageService(IPveHttpClient client)
|
|
{
|
|
_client = client;
|
|
}
|
|
|
|
internal override IPveHttpClient CreateClient(PveSession session, TimeSpan? timeoutOverride)
|
|
{
|
|
SeenTimeout = timeoutOverride;
|
|
return _client;
|
|
}
|
|
}
|
|
|
|
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_SendsExactForm()
|
|
{
|
|
// Arrange
|
|
var captured = CapturePost(_mockClient, @"{""data"":{""storage"":""local"",""type"":""dir""}}");
|
|
var config = new Dictionary<string, object>
|
|
{
|
|
["storage"] = "local",
|
|
["type"] = "dir",
|
|
["shared"] = "1"
|
|
};
|
|
|
|
// Act
|
|
_service.CreateStorage(_session, config);
|
|
|
|
// Assert
|
|
Assert.Equal(1, captured.Calls);
|
|
Assert.Equal("storage", captured.Path);
|
|
Assert.NotNull(captured.Form);
|
|
Assert.Equal("local", captured.Form!["storage"]);
|
|
Assert.Equal("dir", captured.Form["type"]);
|
|
Assert.Equal("1", captured.Form["shared"]);
|
|
Assert.Equal(3, captured.Form.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateStorage_NonStringValues_AreStringified()
|
|
{
|
|
// Arrange
|
|
var captured = CapturePost(_mockClient, @"{""data"":{""storage"":""local"",""type"":""dir""}}");
|
|
var config = new Dictionary<string, object>
|
|
{
|
|
["storage"] = "local",
|
|
["shared"] = 1,
|
|
["disable"] = true,
|
|
["notes"] = null!
|
|
};
|
|
|
|
// Act
|
|
_service.CreateStorage(_session, config);
|
|
|
|
// Assert
|
|
Assert.NotNull(captured.Form);
|
|
Assert.Equal("1", captured.Form!["shared"]);
|
|
Assert.Equal("True", captured.Form["disable"]);
|
|
Assert.Equal(string.Empty, captured.Form["notes"]);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateStorage_EmptyResponseBody_ReturnsEmptyStorage()
|
|
{
|
|
// Arrange
|
|
CapturePost(_mockClient, string.Empty);
|
|
|
|
// Act
|
|
var result = _service.CreateStorage(_session, new Dictionary<string, object> { ["storage"] = "local" });
|
|
|
|
// Assert
|
|
Assert.Equal(string.Empty, result.Storage);
|
|
}
|
|
|
|
[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_EscapesPathTraversalInName()
|
|
{
|
|
// Arrange
|
|
_mockClient.Setup(c => c.DeleteAsync("storage/..%2Faccess%2Fusers%2Fx"))
|
|
.ReturnsAsync(@"{""data"":null}");
|
|
|
|
// Act
|
|
_service.RemoveStorage(_session, "../access/users/x");
|
|
|
|
// Assert
|
|
_mockClient.Verify(c => c.DeleteAsync("storage/..%2Faccess%2Fusers%2Fx"), 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);
|
|
Assert.Equal("running", result.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void UploadIso_DefaultContentType_SendsIso()
|
|
{
|
|
// Arrange
|
|
var captured = CaptureUpload(_mockClient, @"{""data"":""UPID:pve1:000AAA:00000001:65F00000:upload:local:root@pam:""}");
|
|
|
|
// Act
|
|
_service.UploadIso(_session, "pve1", "local", "/tmp/debian-12.iso");
|
|
|
|
// Assert
|
|
Assert.Equal(1, captured.Calls);
|
|
Assert.Equal("nodes/pve1/storage/local/upload", captured.Path);
|
|
Assert.NotNull(captured.Fields);
|
|
Assert.Equal("iso", captured.Fields!["content"]);
|
|
Assert.Single(captured.Fields);
|
|
}
|
|
|
|
[Fact]
|
|
public void UploadIso_ExplicitContentType_SendsIt()
|
|
{
|
|
// Arrange
|
|
var captured = CaptureUpload(_mockClient, @"{""data"":""UPID:pve1:000AAA:00000001:65F00000:upload:local:root@pam:""}");
|
|
|
|
// Act
|
|
_service.UploadIso(_session, "pve1", "local", "/tmp/rootfs.tar.zst", contentType: "vztmpl");
|
|
|
|
// Assert
|
|
Assert.Equal(1, captured.Calls);
|
|
Assert.NotNull(captured.Fields);
|
|
Assert.Equal("vztmpl", captured.Fields!["content"]);
|
|
Assert.Single(captured.Fields);
|
|
}
|
|
|
|
[Fact]
|
|
public void UploadIso_EscapesNodeAndStorageInPath()
|
|
{
|
|
// Arrange
|
|
var captured = CaptureUpload(_mockClient, @"{""data"":""UPID:pve1:000AAA:00000001:65F00000:upload:local:root@pam:""}");
|
|
|
|
// Act
|
|
_service.UploadIso(_session, "pve node", "local storage", "/tmp/debian-12.iso");
|
|
|
|
// Assert
|
|
Assert.Equal("nodes/pve%20node/storage/local%20storage/upload", captured.Path);
|
|
}
|
|
|
|
[Fact]
|
|
public void UploadIso_WhitespaceContentType_ThrowsArgumentNullException()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
_service.UploadIso(_session, "pve1", "local", "/tmp/test.iso", contentType: " "));
|
|
}
|
|
|
|
[Fact]
|
|
public void UploadIso_TimeoutOverride_PassesItToClientConstruction()
|
|
{
|
|
// Arrange
|
|
var captured = CaptureUpload(_mockClient, @"{""data"":""UPID:pve1:000AAA:00000001:65F00000:upload:local:root@pam:""}");
|
|
var service = new TimeoutCapturingStorageService(_mockClient.Object);
|
|
|
|
// Act
|
|
service.UploadIso(_session, "pve1", "local", "/tmp/debian-12.iso", timeout: TimeSpan.FromMinutes(45));
|
|
|
|
// Assert
|
|
Assert.Equal(TimeSpan.FromMinutes(45), service.SeenTimeout);
|
|
}
|
|
|
|
[Fact]
|
|
public void UploadIso_NoTimeout_DefaultsToThirtyMinutes()
|
|
{
|
|
// Arrange
|
|
CaptureUpload(_mockClient, @"{""data"":""UPID:pve1:000AAA:00000001:65F00000:upload:local:root@pam:""}");
|
|
var service = new TimeoutCapturingStorageService(_mockClient.Object);
|
|
|
|
// Act
|
|
service.UploadIso(_session, "pve1", "local", "/tmp/debian-12.iso");
|
|
|
|
// Assert
|
|
Assert.Equal(TimeSpan.FromMinutes(30), service.SeenTimeout);
|
|
}
|
|
|
|
[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);
|
|
Assert.Equal("running", result.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void DownloadUrl_SendsExactForm()
|
|
{
|
|
// Arrange
|
|
var captured = CapturePost(_mockClient, @"{""data"":""UPID:pve1:000BBB:00000002:65F00001:download:local:root@pam:""}");
|
|
|
|
// Act
|
|
_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.Equal(1, captured.Calls);
|
|
Assert.Equal("nodes/pve1/storage/local/download-url", captured.Path);
|
|
Assert.NotNull(captured.Form);
|
|
Assert.Equal("https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img", captured.Form!["url"]);
|
|
Assert.Equal("noble-server-cloudimg-amd64.img", captured.Form["filename"]);
|
|
Assert.Equal("iso", captured.Form["content"]);
|
|
Assert.Equal(3, captured.Form.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void DownloadUrl_EscapesNodeAndStorageInPath()
|
|
{
|
|
// Arrange
|
|
var captured = CapturePost(_mockClient, @"{""data"":""UPID:pve1:000BBB:00000002:65F00001:download:local:root@pam:""}");
|
|
|
|
// Act
|
|
_service.DownloadUrl(_session, "pve node", "local storage", "https://example.com/f.iso", "f.iso", "iso");
|
|
|
|
// Assert
|
|
Assert.Equal(1, captured.Calls);
|
|
Assert.Equal("nodes/pve%20node/storage/local%20storage/download-url", captured.Path);
|
|
}
|
|
|
|
[Fact]
|
|
public void DownloadUrl_TimeoutOverride_PassesItToClientConstruction()
|
|
{
|
|
// Arrange
|
|
var captured = CapturePost(_mockClient, @"{""data"":""UPID:pve1:000BBB:00000002:65F00001:download:local:root@pam:""}");
|
|
var service = new TimeoutCapturingStorageService(_mockClient.Object);
|
|
|
|
// Act
|
|
service.DownloadUrl(_session, "pve1", "local", "https://example.com/f.iso", "f.iso", "iso",
|
|
timeout: TimeSpan.FromMinutes(45));
|
|
|
|
// Assert
|
|
Assert.Equal(TimeSpan.FromMinutes(45), service.SeenTimeout);
|
|
Assert.Equal(1, captured.Calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void DownloadUrl_NoTimeout_DefaultsToThirtyMinutes()
|
|
{
|
|
// Arrange
|
|
CapturePost(_mockClient, @"{""data"":""UPID:pve1:000BBB:00000002:65F00001:download:local:root@pam:""}");
|
|
var service = new TimeoutCapturingStorageService(_mockClient.Object);
|
|
|
|
// Act
|
|
service.DownloadUrl(_session, "pve1", "local", "https://example.com/f.iso", "f.iso", "iso");
|
|
|
|
// Assert
|
|
Assert.Equal(TimeSpan.FromMinutes(30), service.SeenTimeout);
|
|
}
|
|
|
|
[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!));
|
|
}
|
|
}
|
|
}
|