9 Commits

Author SHA1 Message Date
GoodOlClint 007f116cea Merge pull request #47 from GoodOlClint/release/v0.1.2
chore: bump version to 0.1.2
2026-03-27 11:13:28 -05:00
Clint Branham d3953c8c96 chore: bump version to 0.1.2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:00:02 -05:00
GoodOlClint 69e3b0b15a Merge pull request #46 from GoodOlClint/fix/issues-43-44-45
fix: resolve issues #43, #44, #45
2026-03-27 10:36:40 -05:00
Clint Branham 5dcbde45a6 fix: resolve issues #43, #44, #45
#44 Get-PveApiToken FullTokenId empty:
  - Make FullTokenId a computed property (UserId + "!" + TokenId)
  - RawFullTokenId captures the API's "full-tokenid" for creation responses

#43 Set-PvePermission token ACLs:
  - Add "token" to Type ValidateSet
  - Auto-detect tokens from "!" in UgId (user@realm!tokenid format)
  - Add tokens parameter to UserService.SetPermission

#45 Connect-PveServer return session by default:
  - Always output session (matches Connect-AzAccount pattern)
  - Add -Quiet switch to suppress output
  - Keep -PassThru as hidden deprecated param for backwards compat

Closes #43, closes #44, closes #45

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:22:37 -05:00
GoodOlClint aaff8ed42e Merge pull request #42 from GoodOlClint/release/v0.1.1-preview
chore: bump version to 0.1.1-preview
2026-03-26 16:54:52 -05:00
Clint Branham 6e953c1cd7 chore: bump version to 0.1.1 (stable release)
Remove prerelease tag — this is now a stable release.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:49:04 -05:00
GoodOlClint 1694d7f693 Merge pull request #41 from GoodOlClint/fix/validate-api-enums-against-openapi
fix: validate API enum values against PVE OpenAPI spec
2026-03-26 16:33:58 -05:00
Clint Branham 7802b853d4 test: version-aware OpenAPI spec validation (PVE 7/8/9)
Replace single-version fixture with per-version specs from pve-api.
Tests now validate enum values against PVE 7 (best-effort), 8, and 9.

Key findings from version-specific specs:
- VM.Monitor: valid in PVE 7+8, removed in PVE 9
- VM.Replicate: added in PVE 9 only
- VM.GuestAgent.*: added in PVE 9 only
- Mapping.*: added in PVE 8+
- glusterfs: not in any version (removed before PVE 7)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:26:19 -05:00
Clint Branham d81c850b0b fix: validate API enum values against PVE OpenAPI spec
Add 70 xUnit tests that validate every ValidateSet in the module
against the PVE OpenAPI spec. Three bugs found and fixed:

- Storage: remove `glusterfs` (dropped in PVE 9), add `btrfs`, `esxi`
- Backup compression: `none` → `0` (PVE uses "0" not "none")
- Cluster resources: remove `lxc` filter (PVE uses `vm` for both)

The pve-api-enums.json fixture (199KB) is extracted from the full
OpenAPI spec and contains parameter enum values for 302 API paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:34:56 -05:00
15 changed files with 26764 additions and 18 deletions
@@ -23,10 +23,20 @@ public class PveApiToken
/// <summary>
/// The full token identifier in "user@realm!tokenid" format, e.g., "admin@pam!automation".
/// Only populated by New-PveApiToken; not returned by the list/get endpoints.
/// Computed from UserId and TokenId. The "full-tokenid" JSON field from New-PveApiToken
/// is captured by <see cref="RawFullTokenId"/> for deserialization, but this property
/// always returns a computed value so it works for list/get endpoints too.
/// </summary>
public string FullTokenId =>
string.IsNullOrEmpty(UserId) || string.IsNullOrEmpty(TokenId)
? RawFullTokenId ?? string.Empty
: $"{UserId}!{TokenId}";
/// <summary>
/// Raw "full-tokenid" value from the API (only present on token creation responses).
/// </summary>
[JsonProperty("full-tokenid")]
public string? FullTokenId { get; set; }
public string? RawFullTokenId { get; set; }
/// <summary>
/// The token secret UUID. <b>Only present on creation</b> — store it immediately,
@@ -642,6 +642,7 @@ namespace PSProxmoxVE.Core.Services
/// <param name="roles">Comma-separated role IDs.</param>
/// <param name="users">Comma-separated user IDs.</param>
/// <param name="groups">Comma-separated group names.</param>
/// <param name="tokens">Comma-separated API token IDs (user@realm!tokenid).</param>
/// <param name="propagate">Whether to propagate the permission to sub-paths.</param>
/// <param name="delete">If true, removes the specified ACL entries.</param>
public void SetPermission(
@@ -650,6 +651,7 @@ namespace PSProxmoxVE.Core.Services
string roles,
string? users = null,
string? groups = null,
string? tokens = null,
bool propagate = true,
bool delete = false)
{
@@ -666,6 +668,7 @@ namespace PSProxmoxVE.Core.Services
};
if (!string.IsNullOrEmpty(users)) formData["users"] = users!;
if (!string.IsNullOrEmpty(groups)) formData["groups"] = groups!;
if (!string.IsNullOrEmpty(tokens)) formData["tokens"] = tokens!;
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
try
@@ -48,7 +48,7 @@ namespace PSProxmoxVE.Cmdlets.Backup
/// <para type="description">The compression algorithm to use.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The compression algorithm.")]
[ValidateSet("zstd", "lzo", "gzip", "none")]
[ValidateSet("zstd", "lzo", "gzip", "0")]
public string? Compress { get; set; }
/// <summary>
@@ -51,7 +51,7 @@ namespace PSProxmoxVE.Cmdlets.Backup
/// <para type="description">The compression algorithm to use.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The compression algorithm.")]
[ValidateSet("zstd", "lzo", "gzip", "none")]
[ValidateSet("zstd", "lzo", "gzip", "0")]
public string? Compress { get; set; }
/// <summary>
@@ -56,7 +56,7 @@ namespace PSProxmoxVE.Cmdlets.Backup
/// <para type="description">Updated compression algorithm.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The compression algorithm.")]
[ValidateSet("zstd", "lzo", "gzip", "none")]
[ValidateSet("zstd", "lzo", "gzip", "0")]
public string? Compress { get; set; }
/// <summary>
@@ -19,8 +19,8 @@ namespace PSProxmoxVE.Cmdlets.Cluster
/// <summary>
/// <para type="description">Filter by resource type.</para>
/// </summary>
[Parameter(Mandatory = false, Position = 0, HelpMessage = "Filter by resource type (vm, lxc, node, storage, sdn).")]
[ValidateSet("vm", "lxc", "node", "storage", "sdn")]
[Parameter(Mandatory = false, Position = 0, HelpMessage = "Filter by resource type (vm, node, storage, sdn).")]
[ValidateSet("vm", "node", "storage", "sdn")]
public string? Type { get; set; }
/// <summary>
@@ -48,10 +48,14 @@ namespace PSProxmoxVE.Cmdlets.Connection
[Parameter(Mandatory = false, HelpMessage = "Skip TLS certificate validation.")]
public SwitchParameter SkipCertificateCheck { get; set; }
/// <summary>When specified, writes the resulting PveSession object to the pipeline.</summary>
[Parameter(Mandatory = false, HelpMessage = "Output the session object to the pipeline.")]
/// <summary>Deprecated — session is now always output. Kept for backwards compatibility.</summary>
[Parameter(Mandatory = false, DontShow = true)]
public SwitchParameter PassThru { get; set; }
/// <summary>When specified, suppresses the session object from the pipeline output.</summary>
[Parameter(Mandatory = false, HelpMessage = "Do not output the session object to the pipeline.")]
public SwitchParameter Quiet { get; set; }
protected override void ProcessRecord()
{
PveSession session;
@@ -122,7 +126,7 @@ namespace PSProxmoxVE.Cmdlets.Connection
WriteVerbose($"Connected to {Server}:{Port} as {session.AuthMode} (PVE {session.ServerVersion}).");
if (PassThru.IsPresent)
if (!Quiet.IsPresent)
WriteObject(session);
}
}
@@ -25,7 +25,7 @@ namespace PSProxmoxVE.Cmdlets.Storage
/// <summary>The storage type (e.g., "dir", "nfs", "lvm", "zfspool", "cephfs", "rbd").</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The storage type (e.g. dir, nfs, lvm, zfspool).")]
[ValidateSet("dir", "nfs", "lvm", "lvmthin", "zfspool", "zfs", "cephfs", "rbd",
"iscsi", "iscsidirect", "glusterfs", "cifs", "pbs", IgnoreCase = true)]
"iscsi", "iscsidirect", "cifs", "pbs", "btrfs", "esxi", IgnoreCase = true)]
public string Type { get; set; } = string.Empty;
/// <summary>Comma-separated list of content types to support (e.g., "iso,vztmpl,backup").</summary>
@@ -27,9 +27,10 @@ namespace PSProxmoxVE.Cmdlets.Users
[Parameter(Mandatory = true, Position = 2, HelpMessage = "The role to assign (e.g. Administrator).")]
public string Role { get; set; } = string.Empty;
/// <summary>The ACL entry type: "user" or "group".</summary>
[Parameter(Mandatory = false, HelpMessage = "ACL entry type: user or group.")]
[ValidateSet("user", "group", IgnoreCase = true)]
/// <summary>The ACL entry type: "user", "token", or "group". When set to "user", API tokens
/// (UgId containing "!") are automatically detected and sent as the "tokens" parameter.</summary>
[Parameter(Mandatory = false, HelpMessage = "ACL entry type: user, token, or group.")]
[ValidateSet("user", "token", "group", IgnoreCase = true)]
public string Type { get; set; } = "user";
/// <summary>Whether to propagate this ACL to child paths.</summary>
@@ -58,6 +59,8 @@ namespace PSProxmoxVE.Cmdlets.Users
if (string.Equals(Type, "group", System.StringComparison.OrdinalIgnoreCase))
data["groups"] = UgId;
else if (string.Equals(Type, "token", System.StringComparison.OrdinalIgnoreCase) || UgId.Contains("!"))
data["tokens"] = UgId;
else
data["users"] = UgId;
+3 -3
View File
@@ -10,7 +10,7 @@
RootModule = 'PSProxmoxVE.dll'
# Version number of this module.
ModuleVersion = '0.1.0'
ModuleVersion = '0.1.2'
# Supported PSEditions
CompatiblePSEditions = @('Desktop', 'Core')
@@ -349,8 +349,8 @@
PSData = @{
# Prerelease string for the module
Prerelease = 'preview'
# Prerelease string for the module (empty = stable release)
# Prerelease = 'preview'
# Tags applied to this module
Tags = @(
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,434 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
namespace PSProxmoxVE.Core.Tests
{
/// <summary>
/// Validates that the module's hardcoded enum values (ValidateSet, privilege names, etc.)
/// match what the PVE API actually accepts, as defined by per-version OpenAPI specs.
///
/// Fixtures pve-api-enums.pve{7,8,9}.json are extracted from the full OpenAPI specs at
/// ~/Source/pve_api/tools/pve-api-parser/ and contain parameter enum values per PVE version.
///
/// PVE 7 = best-effort support, PVE 8 + 9 = fully supported.
/// </summary>
public class OpenApiSpecValidationTests
{
private static readonly Lazy<JObject> _specPve7 = new Lazy<JObject>(() =>
JObject.Parse(TestHelper.LoadFixture("pve-api-enums.pve7.json")));
private static readonly Lazy<JObject> _specPve8 = new Lazy<JObject>(() =>
JObject.Parse(TestHelper.LoadFixture("pve-api-enums.pve8.json")));
private static readonly Lazy<JObject> _specPve9 = new Lazy<JObject>(() =>
JObject.Parse(TestHelper.LoadFixture("pve-api-enums.pve9.json")));
private static JObject SpecPve7 => _specPve7.Value;
private static JObject SpecPve8 => _specPve8.Value;
private static JObject SpecPve9 => _specPve9.Value;
private static HashSet<string> GetEnumValues(JObject spec, string path, string method, string paramName)
{
var pathData = spec["paths"]?[path]?[method]?["params"]?[paramName];
if (pathData == null) return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var values = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (pathData["enum"] is JArray enumArray)
foreach (var v in enumArray)
values.Add(v.ToString());
if (pathData["x-enum-values"] is JArray xEnumArray)
foreach (var v in xEnumArray)
values.Add(v.ToString());
return values;
}
/// <summary>
/// Asserts that a value is valid in at least one of the supported PVE versions.
/// Returns which versions accept/reject it for diagnostic purposes.
/// </summary>
private static void AssertValidInAnyVersion(string path, string method, string paramName, string value)
{
var pve7 = GetEnumValues(SpecPve7, path, method, paramName);
var pve8 = GetEnumValues(SpecPve8, path, method, paramName);
var pve9 = GetEnumValues(SpecPve9, path, method, paramName);
Assert.True(
pve7.Contains(value) || pve8.Contains(value) || pve9.Contains(value),
$"'{value}' not valid in any PVE version for {method.ToUpper()} {path} param '{paramName}'. " +
$"PVE 7: [{string.Join(", ", pve7)}], PVE 8: [{string.Join(", ", pve8)}], PVE 9: [{string.Join(", ", pve9)}]");
}
/// <summary>
/// Asserts that a value is valid in ALL fully-supported PVE versions (8 + 9).
/// </summary>
private static void AssertValidInAllSupported(string path, string method, string paramName, string value)
{
var pve8 = GetEnumValues(SpecPve8, path, method, paramName);
var pve9 = GetEnumValues(SpecPve9, path, method, paramName);
var missing = new List<string>();
if (!pve8.Contains(value)) missing.Add("PVE 8");
if (!pve9.Contains(value)) missing.Add("PVE 9");
Assert.True(missing.Count == 0,
$"'{value}' not valid in: {string.Join(", ", missing)} for {method.ToUpper()} {path} param '{paramName}'. " +
$"PVE 8: [{string.Join(", ", pve8)}], PVE 9: [{string.Join(", ", pve9)}]");
}
// ── Privileges ──────────────────────────────────────────────────────
[Fact]
public void Privileges_AllPve8And9Common()
{
var pve8 = GetEnumValues(SpecPve8, "/access/roles", "post", "privs");
var pve9 = GetEnumValues(SpecPve9, "/access/roles", "post", "privs");
Assert.NotEmpty(pve8);
Assert.NotEmpty(pve9);
var common = pve8.Intersect(pve9, StringComparer.Ordinal).OrderBy(p => p).ToList();
Assert.True(common.Count > 30, $"Expected >30 common privileges, got {common.Count}");
}
[Fact]
public void Privileges_VmMonitor_OnlyPve7And8()
{
var pve7 = GetEnumValues(SpecPve7, "/access/roles", "post", "privs");
var pve8 = GetEnumValues(SpecPve8, "/access/roles", "post", "privs");
var pve9 = GetEnumValues(SpecPve9, "/access/roles", "post", "privs");
Assert.Contains("VM.Monitor", pve7);
Assert.Contains("VM.Monitor", pve8);
Assert.DoesNotContain("VM.Monitor", pve9);
}
[Fact]
public void Privileges_Pve9Only()
{
var pve8 = GetEnumValues(SpecPve8, "/access/roles", "post", "privs");
var pve9 = GetEnumValues(SpecPve9, "/access/roles", "post", "privs");
// These privileges were added in PVE 9
var guestAgentPrivs = new[]
{
"VM.GuestAgent.Audit", "VM.GuestAgent.FileRead", "VM.GuestAgent.FileWrite",
"VM.GuestAgent.FileSystemMgmt", "VM.GuestAgent.Unrestricted",
"VM.Replicate"
};
foreach (var priv in guestAgentPrivs)
{
Assert.DoesNotContain(priv, pve8);
Assert.Contains(priv, pve9);
}
}
[Theory]
[InlineData("VM.Allocate")]
[InlineData("VM.Audit")]
[InlineData("VM.Backup")]
[InlineData("VM.Clone")]
[InlineData("VM.Config.CDROM")]
[InlineData("VM.Config.Cloudinit")]
[InlineData("VM.Config.CPU")]
[InlineData("VM.Config.Disk")]
[InlineData("VM.Config.HWType")]
[InlineData("VM.Config.Memory")]
[InlineData("VM.Config.Network")]
[InlineData("VM.Config.Options")]
[InlineData("VM.Console")]
[InlineData("VM.Migrate")]
[InlineData("VM.PowerMgmt")]
[InlineData("VM.Snapshot")]
[InlineData("VM.Snapshot.Rollback")]
[InlineData("Datastore.Allocate")]
[InlineData("Datastore.AllocateSpace")]
[InlineData("Datastore.AllocateTemplate")]
[InlineData("Datastore.Audit")]
[InlineData("Sys.Audit")]
[InlineData("Sys.Console")]
[InlineData("Sys.Modify")]
[InlineData("Sys.PowerMgmt")]
[InlineData("Sys.Syslog")]
[InlineData("Sys.AccessNetwork")]
[InlineData("Sys.Incoming")]
[InlineData("SDN.Allocate")]
[InlineData("SDN.Audit")]
[InlineData("SDN.Use")]
[InlineData("User.Modify")]
[InlineData("Permissions.Modify")]
[InlineData("Pool.Allocate")]
[InlineData("Pool.Audit")]
[InlineData("Group.Allocate")]
[InlineData("Realm.Allocate")]
[InlineData("Realm.AllocateUser")]
public void Privilege_ValidInAllSupportedVersions(string privilege)
{
AssertValidInAllSupported("/access/roles", "post", "privs", privilege);
}
[Theory]
[InlineData("Mapping.Audit")]
[InlineData("Mapping.Modify")]
[InlineData("Mapping.Use")]
public void Privilege_Pve8AndAbove(string privilege)
{
var pve7 = GetEnumValues(SpecPve7, "/access/roles", "post", "privs");
var pve8 = GetEnumValues(SpecPve8, "/access/roles", "post", "privs");
var pve9 = GetEnumValues(SpecPve9, "/access/roles", "post", "privs");
Assert.DoesNotContain(privilege, pve7);
Assert.Contains(privilege, pve8);
Assert.Contains(privilege, pve9);
}
// ── Storage Types ───────────────────────────────────────────────────
[Theory]
[InlineData("dir")]
[InlineData("nfs")]
[InlineData("lvm")]
[InlineData("lvmthin")]
[InlineData("zfspool")]
[InlineData("zfs")]
[InlineData("cephfs")]
[InlineData("rbd")]
[InlineData("iscsi")]
[InlineData("iscsidirect")]
[InlineData("cifs")]
[InlineData("pbs")]
public void StorageType_ValidInAllSupportedVersions(string storageType)
{
AssertValidInAllSupported("/storage", "post", "type", storageType);
}
[Theory]
[InlineData("btrfs")]
[InlineData("esxi")]
public void StorageType_ValidAcrossAllVersions(string storageType)
{
var pve7 = GetEnumValues(SpecPve7, "/storage", "post", "type");
var pve8 = GetEnumValues(SpecPve8, "/storage", "post", "type");
var pve9 = GetEnumValues(SpecPve9, "/storage", "post", "type");
Assert.Contains(storageType, pve7);
Assert.Contains(storageType, pve8);
Assert.Contains(storageType, pve9);
}
[Fact]
public void StorageType_GlusterfsNotInAnyVersion()
{
// glusterfs was removed before PVE 7 — should not be in any ValidateSet
var pve7 = GetEnumValues(SpecPve7, "/storage", "post", "type");
var pve8 = GetEnumValues(SpecPve8, "/storage", "post", "type");
var pve9 = GetEnumValues(SpecPve9, "/storage", "post", "type");
Assert.DoesNotContain("glusterfs", pve7);
Assert.DoesNotContain("glusterfs", pve8);
Assert.DoesNotContain("glusterfs", pve9);
}
// ── Backup Modes ────────────────────────────────────────────────────
[Theory]
[InlineData("snapshot")]
[InlineData("suspend")]
[InlineData("stop")]
public void BackupMode_ValidInAllSupportedVersions(string mode)
{
AssertValidInAllSupported("/nodes/{node}/vzdump", "post", "mode", mode);
}
// ── Backup Compression ──────────────────────────────────────────────
[Theory]
[InlineData("zstd")]
[InlineData("lzo")]
[InlineData("gzip")]
[InlineData("0")]
public void BackupCompression_ValidInAllSupportedVersions(string compress)
{
AssertValidInAllSupported("/nodes/{node}/vzdump", "post", "compress", compress);
}
// ── Disk Formats ────────────────────────────────────────────────────
[Theory]
[InlineData("raw")]
[InlineData("qcow2")]
[InlineData("vmdk")]
public void DiskFormat_ValidInAllSupportedVersions(string format)
{
AssertValidInAllSupported("/nodes/{node}/qemu/{vmid}/move_disk", "post", "format", format);
}
// ── HA Resource States ──────────────────────────────────────────────
[Theory]
[InlineData("started")]
[InlineData("stopped")]
[InlineData("disabled")]
[InlineData("ignored")]
public void HaResourceState_ValidInAllSupportedVersions(string state)
{
AssertValidInAllSupported("/cluster/ha/resources", "post", "state", state);
}
// ── SDN Zone Types ──────────────────────────────────────────────────
[Theory]
[InlineData("vlan")]
[InlineData("vxlan")]
[InlineData("evpn")]
[InlineData("simple")]
[InlineData("qinq")]
public void SdnZoneType_ValidInAllSupportedVersions(string zoneType)
{
AssertValidInAllSupported("/cluster/sdn/zones", "post", "type", zoneType);
}
// ── SDN Controller Types ────────────────────────────────────────────
[Theory]
[InlineData("evpn")]
[InlineData("bgp")]
public void SdnControllerType_ValidInAllSupportedVersions(string controllerType)
{
AssertValidInAllSupported("/cluster/sdn/controllers", "post", "type", controllerType);
}
// ── SDN IPAM Types ──────────────────────────────────────────────────
[Theory]
[InlineData("pve")]
[InlineData("netbox")]
[InlineData("phpipam")]
public void SdnIpamType_ValidInAllSupportedVersions(string ipamType)
{
AssertValidInAllSupported("/cluster/sdn/ipams", "post", "type", ipamType);
}
// ── SDN DNS Types ───────────────────────────────────────────────────
[Theory]
[InlineData("powerdns")]
public void SdnDnsType_ValidInAllSupportedVersions(string dnsType)
{
AssertValidInAllSupported("/cluster/sdn/dns", "post", "type", dnsType);
}
// ── Firewall Actions ────────────────────────────────────────────────
// The 'action' param uses a pattern regex (also accepts group names),
// not an enum. Validate against the documented pattern instead.
[Theory]
[InlineData("ACCEPT")]
[InlineData("DROP")]
[InlineData("REJECT")]
public void FirewallAction_MatchesApiPattern(string action)
{
// PVE defines action as pattern: [A-Za-z][A-Za-z0-9\-\_]+
Assert.Matches(@"^[A-Za-z][A-Za-z0-9\-_]+$", action);
}
// ── Firewall Directions ─────────────────────────────────────────────
[Theory]
[InlineData("in")]
[InlineData("out")]
[InlineData("group")]
public void FirewallDirection_ValidInAllSupportedVersions(string direction)
{
AssertValidInAllSupported("/cluster/firewall/rules", "post", "type", direction);
}
// ── Auth Domain Types ───────────────────────────────────────────────
[Theory]
[InlineData("pam")]
[InlineData("pve")]
[InlineData("ad")]
[InlineData("ldap")]
[InlineData("openid")]
public void AuthDomainType_ValidInAllSupportedVersions(string domainType)
{
AssertValidInAllSupported("/access/domains", "post", "type", domainType);
}
// ── Cluster Resource Types ──────────────────────────────────────────
[Theory]
[InlineData("vm")]
[InlineData("node")]
[InlineData("storage")]
[InlineData("sdn")]
public void ClusterResourceType_ValidInAllSupportedVersions(string resourceType)
{
AssertValidInAllSupported("/cluster/resources", "get", "type", resourceType);
}
// ── Content Types (Upload) ──────────────────────────────────────────
[Theory]
[InlineData("iso")]
[InlineData("vztmpl")]
public void UploadContentType_ValidInAllSupportedVersions(string contentType)
{
AssertValidInAllSupported("/nodes/{node}/storage/{storage}/upload", "post", "content", contentType);
}
// ── Console Viewer Types ────────────────────────────────────────────
[Theory]
[InlineData("applet")]
[InlineData("vv")]
[InlineData("html5")]
[InlineData("xtermjs")]
public void ConsoleViewer_ValidInAnyVersion(string viewer)
{
// 'applet' was removed in later versions — validate across any
AssertValidInAnyVersion("/cluster/options", "put", "console", viewer);
}
// ── Network Interface Types ─────────────────────────────────────────
[Theory]
[InlineData("bridge")]
[InlineData("bond")]
[InlineData("eth")]
[InlineData("alias")]
[InlineData("vlan")]
[InlineData("OVSBridge")]
[InlineData("OVSBond")]
[InlineData("OVSPort")]
[InlineData("OVSIntPort")]
public void NetworkInterfaceType_ValidInAllSupportedVersions(string ifaceType)
{
AssertValidInAllSupported("/nodes/{node}/network", "post", "type", ifaceType);
}
// ── Version Progression ─────────────────────────────────────────────
[Fact]
public void Pve9_HasMorePrivilegesThanPve8()
{
var pve8 = GetEnumValues(SpecPve8, "/access/roles", "post", "privs");
var pve9 = GetEnumValues(SpecPve9, "/access/roles", "post", "privs");
Assert.True(pve9.Count > pve8.Count,
$"Expected PVE 9 ({pve9.Count}) to have more privileges than PVE 8 ({pve8.Count})");
}
[Fact]
public void Pve9_HasMorePathsThanPve8()
{
var pve8Paths = SpecPve8["paths"]?.Children().Count() ?? 0;
var pve9Paths = SpecPve9["paths"]?.Children().Count() ?? 0;
Assert.True(pve9Paths > pve8Paths,
$"Expected PVE 9 ({pve9Paths}) to have more paths than PVE 8 ({pve8Paths})");
}
}
}
@@ -94,12 +94,18 @@ Describe 'Connect-PveServer' {
Should -Be ([System.Management.Automation.SwitchParameter])
}
It 'Should have a PassThru switch parameter' {
It 'Should have a PassThru switch parameter (deprecated, hidden)' {
$script:Cmd.Parameters.ContainsKey('PassThru') | Should -BeTrue
$script:Cmd.Parameters['PassThru'].ParameterType |
Should -Be ([System.Management.Automation.SwitchParameter])
}
It 'Should have a Quiet switch parameter' {
$script:Cmd.Parameters.ContainsKey('Quiet') | Should -BeTrue
$script:Cmd.Parameters['Quiet'].ParameterType |
Should -Be ([System.Management.Automation.SwitchParameter])
}
It 'Credential and ApiToken should belong to different parameter sets' {
$credSets = $script:Cmd.Parameters['Credential'].ParameterSets.Keys
$tokenSets = $script:Cmd.Parameters['ApiToken'].ParameterSets.Keys