diff --git a/src/PSProxmoxVE.Core/Models/Users/PveApiToken.cs b/src/PSProxmoxVE.Core/Models/Users/PveApiToken.cs
new file mode 100644
index 0000000..f526436
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Users/PveApiToken.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Text.Json.Serialization;
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.Users;
+
+///
+/// Represents a Proxmox VE API token as returned by the /access/users/{userid}/token endpoints.
+///
+public class PveApiToken
+{
+ ///
+ /// The user ID that owns this token (e.g., "admin@pam").
+ /// Not returned by the API — populated by the cmdlet from the request path.
+ ///
+ [JsonPropertyName("userid")]
+ [JsonProperty("userid")]
+ public string UserId { get; set; } = string.Empty;
+
+ ///
+ /// The token identifier (the part after "!"), e.g., "automation".
+ ///
+ [JsonPropertyName("tokenid")]
+ [JsonProperty("tokenid")]
+ public string TokenId { get; set; } = string.Empty;
+
+ ///
+ /// 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.
+ ///
+ [JsonPropertyName("full-tokenid")]
+ [JsonProperty("full-tokenid")]
+ public string? FullTokenId { get; set; }
+
+ ///
+ /// The token secret UUID. Only present on creation — store it immediately,
+ /// as it cannot be retrieved again.
+ ///
+ [JsonPropertyName("value")]
+ [JsonProperty("value")]
+ public string? Value { get; set; }
+
+ ///
+ /// Optional comment or description for this token.
+ ///
+ [JsonPropertyName("comment")]
+ [JsonProperty("comment")]
+ public string? Comment { get; set; }
+
+ ///
+ /// Token expiry as a Unix timestamp. 0 or null means the token never expires.
+ ///
+ [JsonPropertyName("expire")]
+ [JsonProperty("expire")]
+ public long? Expire { get; set; }
+
+ ///
+ /// Whether privilege separation is enabled (1) or disabled (0).
+ /// When enabled, the token's permissions are the intersection of the user's ACLs and
+ /// any explicit ACLs granted to the token itself.
+ ///
+ [JsonPropertyName("privsep")]
+ [JsonProperty("privsep")]
+ public int? PrivilegeSeparation { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ var full = FullTokenId ?? $"{UserId}!{TokenId}";
+ var privsep = PrivilegeSeparation is 1 ? "privsep" : "no-privsep";
+ var expireStr = Expire is > 0
+ ? DateTimeOffset.FromUnixTimeSeconds(Expire.Value).ToString("yyyy-MM-dd")
+ : "never";
+ return $"Token: {full} | {privsep} | Expires: {expireStr}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Services/UserService.cs b/src/PSProxmoxVE.Core/Services/UserService.cs
index 4191ca8..bdbe957 100644
--- a/src/PSProxmoxVE.Core/Services/UserService.cs
+++ b/src/PSProxmoxVE.Core/Services/UserService.cs
@@ -98,6 +98,82 @@ namespace PSProxmoxVE.Core.Services
client.PutAsync($"access/users/{encodedId}", formData).GetAwaiter().GetResult();
}
+ // -------------------------------------------------------------------------
+ // API Tokens
+ // -------------------------------------------------------------------------
+
+ /// Returns all API tokens for the specified user.
+ public PveApiToken[] GetApiTokens(PveSession session, string userId)
+ {
+ if (session == null) throw new ArgumentNullException(nameof(session));
+ if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
+
+ using var client = new PveHttpClient(session);
+ var encodedId = Uri.EscapeDataString(userId);
+ var response = client.GetAsync($"access/users/{encodedId}/token").GetAwaiter().GetResult();
+ var data = JObject.Parse(response)["data"];
+ var tokens = data?.ToObject() ?? Array.Empty();
+ foreach (var t in tokens)
+ t.UserId = userId;
+ return tokens;
+ }
+
+ ///
+ /// Creates a new API token for the specified user and returns the token object,
+ /// including the secret Value (shown only once).
+ ///
+ /// User ID in "username@realm" format.
+ /// Token identifier (alphanumeric and hyphens).
+ /// Optional description.
+ /// Expiry as a Unix timestamp; 0 = never.
+ ///
+ /// When true, the token's effective permissions are intersected with the user's ACLs.
+ ///
+ public PveApiToken CreateApiToken(
+ PveSession session,
+ string userId,
+ string tokenId,
+ string? comment = null,
+ long? expire = null,
+ bool? privilegeSeparation = null)
+ {
+ if (session == null) throw new ArgumentNullException(nameof(session));
+ if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
+ if (string.IsNullOrWhiteSpace(tokenId)) throw new ArgumentNullException(nameof(tokenId));
+
+ var formData = new Dictionary();
+ if (!string.IsNullOrEmpty(comment)) formData["comment"] = comment!;
+ if (expire.HasValue) formData["expire"] = expire.Value.ToString();
+ if (privilegeSeparation.HasValue) formData["privsep"] = privilegeSeparation.Value ? "1" : "0";
+
+ using var client = new PveHttpClient(session);
+ var encodedUser = Uri.EscapeDataString(userId);
+ var encodedToken = Uri.EscapeDataString(tokenId);
+ var response = client.PostAsync(
+ $"access/users/{encodedUser}/token/{encodedToken}", formData)
+ .GetAwaiter().GetResult();
+
+ var data = JObject.Parse(response)["data"];
+ var token = data?.ToObject() ?? new PveApiToken();
+ token.UserId = userId;
+ token.TokenId = tokenId;
+ return token;
+ }
+
+ /// Removes an API token.
+ public void RemoveApiToken(PveSession session, string userId, string tokenId)
+ {
+ if (session == null) throw new ArgumentNullException(nameof(session));
+ if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
+ if (string.IsNullOrWhiteSpace(tokenId)) throw new ArgumentNullException(nameof(tokenId));
+
+ using var client = new PveHttpClient(session);
+ var encodedUser = Uri.EscapeDataString(userId);
+ var encodedToken = Uri.EscapeDataString(tokenId);
+ client.DeleteAsync($"access/users/{encodedUser}/token/{encodedToken}")
+ .GetAwaiter().GetResult();
+ }
+
// -------------------------------------------------------------------------
// Roles
// -------------------------------------------------------------------------
diff --git a/src/PSProxmoxVE/Cmdlets/Users/GetPveApiTokenCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/GetPveApiTokenCmdlet.cs
new file mode 100644
index 0000000..3a0d320
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Users/GetPveApiTokenCmdlet.cs
@@ -0,0 +1,47 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Users;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Users
+{
+ ///
+ /// Lists API tokens for a Proxmox VE user.
+ ///
+ /// Returns the API tokens associated with the specified user. When -TokenId is omitted
+ /// all tokens for the user are returned. Note: the token secret (Value) is never
+ /// returned by the GET endpoints — it is only available immediately after creation
+ /// via New-PveApiToken.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveApiToken")]
+ [OutputType(typeof(PveApiToken))]
+ public class GetPveApiTokenCmdlet : PveCmdletBase
+ {
+ ///
+ /// The user ID whose tokens to list, in "username@realm" format (e.g., "admin@pam").
+ /// Accepts pipeline input from Get-PveUser (PveUser.UserId).
+ ///
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ public string UserId { get; set; } = string.Empty;
+
+ /// Filter to a specific token identifier (e.g., "automation").
+ [Parameter(Mandatory = false, Position = 1)]
+ public string? TokenId { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new UserService();
+ var tokens = service.GetApiTokens(session, UserId);
+
+ foreach (var token in tokens)
+ {
+ if (!string.IsNullOrEmpty(TokenId) &&
+ !string.Equals(token.TokenId, TokenId, System.StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ WriteObject(token);
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Users/NewPveApiTokenCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/NewPveApiTokenCmdlet.cs
new file mode 100644
index 0000000..ce4bb01
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Users/NewPveApiTokenCmdlet.cs
@@ -0,0 +1,68 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Users;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Users
+{
+ ///
+ /// Creates a new API token for a Proxmox VE user.
+ ///
+ /// Generates a new API token and returns a PveApiToken object containing both the
+ /// FullTokenId (e.g., "admin@pam!automation") and the token secret in the Value
+ /// property. The secret is shown only once — save it immediately. Use the FullTokenId
+ /// and Value together as the API token credential for Connect-PveServer.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveApiToken", SupportsShouldProcess = true)]
+ [OutputType(typeof(PveApiToken))]
+ public class NewPveApiTokenCmdlet : PveCmdletBase
+ {
+ ///
+ /// The user ID to create the token for, in "username@realm" format (e.g., "admin@pam").
+ /// Accepts pipeline input from Get-PveUser (PveUser.UserId).
+ ///
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ public string UserId { get; set; } = string.Empty;
+
+ /// The token identifier (alphanumeric, hyphens allowed; e.g., "automation").
+ [Parameter(Mandatory = true, Position = 1)]
+ public string TokenId { get; set; } = string.Empty;
+
+ /// Optional description for this token.
+ [Parameter(Mandatory = false)]
+ public string? Comment { get; set; }
+
+ ///
+ /// Token expiry as a Unix timestamp. Use 0 or omit for no expiry.
+ ///
+ [Parameter(Mandatory = false)]
+ public long? Expire { get; set; }
+
+ ///
+ /// When specified, privilege separation is enabled: the token's effective permissions
+ /// are the intersection of the user's ACLs and any explicit ACLs granted to the token.
+ /// When omitted, the token inherits the full permissions of its user.
+ ///
+ [Parameter(Mandatory = false)]
+ public SwitchParameter PrivilegeSeparation { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var fullTokenId = $"{UserId}!{TokenId}";
+ if (!ShouldProcess(fullTokenId, "Create PVE API Token"))
+ return;
+
+ var session = GetSession();
+ var service = new UserService();
+ var token = service.CreateApiToken(
+ session,
+ UserId,
+ TokenId,
+ comment: Comment,
+ expire: Expire,
+ privilegeSeparation: PrivilegeSeparation.IsPresent ? true : (bool?)null);
+
+ WriteObject(token);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Users/RemovePveApiTokenCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/RemovePveApiTokenCmdlet.cs
new file mode 100644
index 0000000..d065f76
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Users/RemovePveApiTokenCmdlet.cs
@@ -0,0 +1,42 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Users
+{
+ ///
+ /// Removes an API token from a Proxmox VE user.
+ ///
+ /// Permanently deletes the specified API token. Any automation using this token will
+ /// immediately lose access. This operation cannot be undone.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Remove, "PveApiToken", SupportsShouldProcess = true,
+ ConfirmImpact = ConfirmImpact.High)]
+ public class RemovePveApiTokenCmdlet : PveCmdletBase
+ {
+ ///
+ /// The user ID that owns the token, in "username@realm" format (e.g., "admin@pam").
+ /// Accepts pipeline input from Get-PveApiToken (PveApiToken.UserId).
+ ///
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ public string UserId { get; set; } = string.Empty;
+
+ ///
+ /// The token identifier to remove (e.g., "automation").
+ /// Accepts pipeline input from Get-PveApiToken (PveApiToken.TokenId).
+ ///
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
+ public string TokenId { get; set; } = string.Empty;
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+
+ if (!ShouldProcess($"{UserId}!{TokenId}", "Remove PVE API Token"))
+ return;
+
+ var service = new UserService();
+ service.RemoveApiToken(session, UserId, TokenId);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/PSProxmoxVE.psd1 b/src/PSProxmoxVE/PSProxmoxVE.psd1
index 87e5dd9..87d4f14 100644
--- a/src/PSProxmoxVE/PSProxmoxVE.psd1
+++ b/src/PSProxmoxVE/PSProxmoxVE.psd1
@@ -132,6 +132,11 @@
'Get-PvePermission',
'Set-PvePermission',
+ # API Tokens
+ 'Get-PveApiToken',
+ 'New-PveApiToken',
+ 'Remove-PveApiToken',
+
# Templates
'Get-PveTemplate',
'New-PveTemplate',
diff --git a/tests/PSProxmoxVE.Tests/Users/ApiTokenCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Users/ApiTokenCmdlets.Tests.ps1
new file mode 100644
index 0000000..f91e80a
--- /dev/null
+++ b/tests/PSProxmoxVE.Tests/Users/ApiTokenCmdlets.Tests.ps1
@@ -0,0 +1,270 @@
+#Requires -Module Pester
+<#
+.SYNOPSIS
+ Pester 5 tests for API token cmdlets:
+ Get-PveApiToken, New-PveApiToken, Remove-PveApiToken.
+
+ All tests are fully offline — no live Proxmox VE target is required.
+ If a cmdlet is not yet compiled the test is marked Skipped.
+#>
+
+BeforeAll {
+ $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE')
+ $dllCandidates = @(
+ Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll'
+ Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll'
+ Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll'
+ Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll'
+ )
+
+ $script:ModuleDll = $dllCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
+
+ if ($null -eq $script:ModuleDll) {
+ throw "PSProxmoxVE.dll not found. Build the project before running Pester tests."
+ }
+
+ Import-Module $script:ModuleDll -Force -ErrorAction Stop
+
+ $allNames = @('Get-PveApiToken', 'New-PveApiToken', 'Remove-PveApiToken')
+
+ $script:Availability = @{}
+ foreach ($name in $allNames) {
+ $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue)
+ }
+
+ function Skip-IfMissing([string]$Name) {
+ if (-not $script:Availability[$Name]) {
+ Set-ItResult -Skipped -Because "$Name is not yet implemented in this build"
+ }
+ }
+}
+
+# ---------------------------------------------------------------------------
+# Manifest contract
+# ---------------------------------------------------------------------------
+Describe 'API Token cmdlets — manifest declarations' {
+ BeforeAll {
+ $manifestPath = Join-Path $moduleRoot 'PSProxmoxVE.psd1'
+ $script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null }
+ }
+
+ It " should be declared in CmdletsToExport" -TestCases @(
+ @{ cmdName = 'Get-PveApiToken' }
+ @{ cmdName = 'New-PveApiToken' }
+ @{ cmdName = 'Remove-PveApiToken' }
+ ) {
+ if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return }
+ $script:Manifest.CmdletsToExport | Should -Contain $cmdName
+ }
+}
+
+# ---------------------------------------------------------------------------
+# Get-PveApiToken
+# ---------------------------------------------------------------------------
+Describe 'Get-PveApiToken' {
+
+ BeforeAll { $script:Cmd = Get-Command 'Get-PveApiToken' -ErrorAction SilentlyContinue }
+
+ Context 'Command existence' {
+ It 'Should be available after module import' {
+ Skip-IfMissing 'Get-PveApiToken'
+ $script:Cmd | Should -Not -BeNullOrEmpty
+ }
+
+ It 'Should be a CmdletInfo (binary cmdlet)' {
+ Skip-IfMissing 'Get-PveApiToken'
+ $script:Cmd.CommandType | Should -Be 'Cmdlet'
+ }
+ }
+
+ Context 'Parameter metadata' {
+ It 'Should have UserId as mandatory parameter' {
+ Skip-IfMissing 'Get-PveApiToken'
+ $isMandatory = $script:Cmd.Parameters['UserId'].ParameterSets.Values |
+ Where-Object { $_.IsMandatory }
+ $isMandatory | Should -Not -BeNullOrEmpty
+ }
+
+ It 'UserId should accept pipeline input by property name' {
+ Skip-IfMissing 'Get-PveApiToken'
+ $byPropName = $script:Cmd.Parameters['UserId'].ParameterSets.Values |
+ Where-Object { $_.ValueFromPipelineByPropertyName }
+ $byPropName | Should -Not -BeNullOrEmpty
+ }
+
+ It 'Should have TokenId as optional filter parameter' {
+ Skip-IfMissing 'Get-PveApiToken'
+ $script:Cmd.Parameters.ContainsKey('TokenId') | Should -BeTrue
+ }
+
+ It 'TokenId should not be Mandatory' {
+ Skip-IfMissing 'Get-PveApiToken'
+ $isMandatory = $script:Cmd.Parameters['TokenId'].ParameterSets.Values |
+ Where-Object { $_.IsMandatory }
+ $isMandatory | Should -BeNullOrEmpty
+ }
+
+ It 'Should have Session parameter' {
+ Skip-IfMissing 'Get-PveApiToken'
+ $script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
+ }
+ }
+
+ Context 'Without active session' {
+ It 'Should throw when no session is active' {
+ Skip-IfMissing 'Get-PveApiToken'
+ { Get-PveApiToken -UserId 'admin@pam' -ErrorAction Stop } | Should -Throw '*No active Proxmox VE session*'
+ }
+ }
+}
+
+# ---------------------------------------------------------------------------
+# New-PveApiToken
+# ---------------------------------------------------------------------------
+Describe 'New-PveApiToken' {
+
+ BeforeAll { $script:Cmd = Get-Command 'New-PveApiToken' -ErrorAction SilentlyContinue }
+
+ Context 'Command existence' {
+ It 'Should be available after module import' {
+ Skip-IfMissing 'New-PveApiToken'
+ $script:Cmd | Should -Not -BeNullOrEmpty
+ }
+
+ It 'Should be a CmdletInfo (binary cmdlet)' {
+ Skip-IfMissing 'New-PveApiToken'
+ $script:Cmd.CommandType | Should -Be 'Cmdlet'
+ }
+ }
+
+ Context 'ShouldProcess support' {
+ It 'Should support WhatIf' {
+ Skip-IfMissing 'New-PveApiToken'
+ $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
+ }
+
+ It 'WhatIf should not throw even without a session' {
+ Skip-IfMissing 'New-PveApiToken'
+ { New-PveApiToken -UserId 'admin@pam' -TokenId 'test' -WhatIf } | Should -Not -Throw
+ }
+ }
+
+ Context 'Required parameters' {
+ It 'UserId should be Mandatory' {
+ Skip-IfMissing 'New-PveApiToken'
+ $isMandatory = $script:Cmd.Parameters['UserId'].ParameterSets.Values |
+ Where-Object { $_.IsMandatory }
+ $isMandatory | Should -Not -BeNullOrEmpty
+ }
+
+ It 'UserId should accept pipeline input by property name' {
+ Skip-IfMissing 'New-PveApiToken'
+ $byPropName = $script:Cmd.Parameters['UserId'].ParameterSets.Values |
+ Where-Object { $_.ValueFromPipelineByPropertyName }
+ $byPropName | Should -Not -BeNullOrEmpty
+ }
+
+ It 'TokenId should be Mandatory' {
+ Skip-IfMissing 'New-PveApiToken'
+ $isMandatory = $script:Cmd.Parameters['TokenId'].ParameterSets.Values |
+ Where-Object { $_.IsMandatory }
+ $isMandatory | Should -Not -BeNullOrEmpty
+ }
+ }
+
+ Context 'Optional parameters' {
+ It 'Should have Comment parameter' {
+ Skip-IfMissing 'New-PveApiToken'
+ $script:Cmd.Parameters.ContainsKey('Comment') | Should -BeTrue
+ }
+
+ It 'Should have Expire parameter' {
+ Skip-IfMissing 'New-PveApiToken'
+ $script:Cmd.Parameters.ContainsKey('Expire') | Should -BeTrue
+ }
+
+ It 'Should have PrivilegeSeparation switch' {
+ Skip-IfMissing 'New-PveApiToken'
+ $script:Cmd.Parameters.ContainsKey('PrivilegeSeparation') | Should -BeTrue
+ }
+ }
+
+ Context 'Without active session' {
+ It 'Should throw when no session is active' {
+ Skip-IfMissing 'New-PveApiToken'
+ { New-PveApiToken -UserId 'admin@pam' -TokenId 'test' -Confirm:$false -ErrorAction Stop } | Should -Throw '*No active Proxmox VE session*'
+ }
+ }
+}
+
+# ---------------------------------------------------------------------------
+# Remove-PveApiToken
+# ---------------------------------------------------------------------------
+Describe 'Remove-PveApiToken' {
+
+ BeforeAll { $script:Cmd = Get-Command 'Remove-PveApiToken' -ErrorAction SilentlyContinue }
+
+ Context 'Command existence' {
+ It 'Should be available after module import' {
+ Skip-IfMissing 'Remove-PveApiToken'
+ $script:Cmd | Should -Not -BeNullOrEmpty
+ }
+
+ It 'Should be a CmdletInfo (binary cmdlet)' {
+ Skip-IfMissing 'Remove-PveApiToken'
+ $script:Cmd.CommandType | Should -Be 'Cmdlet'
+ }
+ }
+
+ Context 'ShouldProcess / ConfirmImpact' {
+ It 'Should support WhatIf' {
+ Skip-IfMissing 'Remove-PveApiToken'
+ $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
+ }
+
+ It 'Should declare ConfirmImpact High' {
+ Skip-IfMissing 'Remove-PveApiToken'
+ $attr = $script:Cmd.ImplementingType.GetCustomAttributes(
+ [System.Management.Automation.CmdletAttribute], $false) |
+ Select-Object -First 1
+ $attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High)
+ }
+ }
+
+ Context 'Required parameters' {
+ It 'UserId should be Mandatory' {
+ Skip-IfMissing 'Remove-PveApiToken'
+ $isMandatory = $script:Cmd.Parameters['UserId'].ParameterSets.Values |
+ Where-Object { $_.IsMandatory }
+ $isMandatory | Should -Not -BeNullOrEmpty
+ }
+
+ It 'UserId should accept pipeline input by property name' {
+ Skip-IfMissing 'Remove-PveApiToken'
+ $byPropName = $script:Cmd.Parameters['UserId'].ParameterSets.Values |
+ Where-Object { $_.ValueFromPipelineByPropertyName }
+ $byPropName | Should -Not -BeNullOrEmpty
+ }
+
+ It 'TokenId should be Mandatory' {
+ Skip-IfMissing 'Remove-PveApiToken'
+ $isMandatory = $script:Cmd.Parameters['TokenId'].ParameterSets.Values |
+ Where-Object { $_.IsMandatory }
+ $isMandatory | Should -Not -BeNullOrEmpty
+ }
+
+ It 'TokenId should accept pipeline input by property name' {
+ Skip-IfMissing 'Remove-PveApiToken'
+ $byPropName = $script:Cmd.Parameters['TokenId'].ParameterSets.Values |
+ Where-Object { $_.ValueFromPipelineByPropertyName }
+ $byPropName | Should -Not -BeNullOrEmpty
+ }
+ }
+
+ Context 'Without active session' {
+ It 'Should throw when no session is active' {
+ Skip-IfMissing 'Remove-PveApiToken'
+ { Remove-PveApiToken -UserId 'admin@pam' -TokenId 'test' -Confirm:$false -ErrorAction Stop } | Should -Throw '*No active Proxmox VE session*'
+ }
+ }
+}