Files
PSProxmoxVE/tests/PSProxmoxVE.Core.Tests/Services/UserServiceTests.cs
T
Clint Branham 68dadbfdc0 fix: remediate findings F045, F047, F048, F064, F070, F071, F076-F079
Phase 1 — Trivial fixes:
- F071: Add Uri.EscapeDataString to GetPveTemplateCmdlet node path
- F077: Add ValidateRange(100, 999999999) to GetPveTaskListCmdlet.VmId
- F076: Create .github/dependabot.yml (nuget + github-actions, weekly)
- F079: Fix unit-tests.yml dotnet SDK from 9.0.x to 10.0.x
- F048: Mark wont_fix — sync-over-async accepted for PS 5.1 compat

Phase 2 — Framework targeting (D009 compliance):
- F047: Reduce publishable csproj to netstandard2.0 only, remove all
  #if NET48/NETSTANDARD2_0 conditionals from PveHttpClient.cs,
  restructure build.yml for netstandard2.0 publish + net10.0/net48 tests
- F064: Resolved by F047 — SMA 7.5.0 ItemGroup removed with net10.0 TFM
- F070: Add PS 5.1 smoke-test job to publish.yml (windows-latest)

Phase 3 — IPveHttpClient interface extraction (F045):
- Extract IPveHttpClient interface from PveHttpClient
- Add constructor injection to all 14 service classes
- Services use injected client when available, create+dispose when not

Phase 4 — Service unit tests (F078):
- 196 new xUnit tests across 10 service test files
- All services tested via Moq-mocked IPveHttpClient
- Total test count: 382 (was 186)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:51:37 -05:00

883 lines
31 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 UserServiceTests
{
private readonly Mock<IPveHttpClient> _mockClient;
private readonly UserService _service;
private readonly PveSession _session;
public UserServiceTests()
{
_mockClient = new Mock<IPveHttpClient>();
_service = new UserService(_mockClient.Object);
_session = new PveSession(
"pve.example.com",
8006,
skipCertificateCheck: true,
apiToken: "root@pam!test=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
}
// =================================================================
// Users
// =================================================================
[Fact]
public void GetUsers_ReturnsUserArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/users"))
.ReturnsAsync(@"{""data"":[
{""userid"":""root@pam"",""enable"":1,""email"":""root@example.com"",""firstname"":""Root"",""lastname"":""Admin""},
{""userid"":""deploy@pve"",""enable"":1,""groups"":""admins"",""comment"":""Deployment account""}
]}");
// Act
var result = _service.GetUsers(_session);
// Assert
Assert.Equal(2, result.Length);
Assert.Equal("root@pam", result[0].UserId);
Assert.Equal("root@example.com", result[0].Email);
Assert.Equal("Root", result[0].FirstName);
Assert.Equal("deploy@pve", result[1].UserId);
Assert.Equal("admins", result[1].Groups);
_mockClient.Verify(c => c.GetAsync("access/users"), Times.Once);
}
[Fact]
public void GetUsers_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetUsers(null!));
}
[Fact]
public void GetUser_ReturnsSingleUser()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/users/root%40pam"))
.ReturnsAsync(@"{""data"":{""email"":""root@example.com"",""firstname"":""Root"",""lastname"":""Admin"",""enable"":1}}");
// Act
var result = _service.GetUser(_session, "root@pam");
// Assert
Assert.Equal("root@pam", result.UserId);
Assert.Equal("root@example.com", result.Email);
Assert.Equal(1, result.Enabled);
}
[Fact]
public void GetUser_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetUser(null!, "root@pam"));
}
[Fact]
public void GetUser_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetUser(_session, null!));
}
[Fact]
public void CreateUser_PostsFormData()
{
// Arrange
var config = new Dictionary<string, object>
{
["email"] = "newuser@example.com",
["firstname"] = "New",
["lastname"] = "User"
};
_mockClient.Setup(c => c.PostAsync("access/users", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.CreateUser(_session, "newuser@pve", config);
// Assert
_mockClient.Verify(c => c.PostAsync("access/users",
It.Is<Dictionary<string, string>>(d =>
d["userid"] == "newuser@pve" &&
d["email"] == "newuser@example.com")),
Times.Once);
}
[Fact]
public void CreateUser_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateUser(null!, "user@pve"));
}
[Fact]
public void CreateUser_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateUser(_session, null!));
}
[Fact]
public void SetUser_PutsFormData()
{
// Arrange
var config = new Dictionary<string, object>
{
["email"] = "updated@example.com",
["comment"] = "Updated account"
};
_mockClient.Setup(c => c.PutAsync("access/users/deploy%40pve", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.SetUser(_session, "deploy@pve", config);
// Assert
_mockClient.Verify(c => c.PutAsync("access/users/deploy%40pve",
It.Is<Dictionary<string, string>>(d =>
d["email"] == "updated@example.com" &&
d["comment"] == "Updated account")),
Times.Once);
}
[Fact]
public void SetUser_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.SetUser(null!, "user@pve", new Dictionary<string, object>()));
}
[Fact]
public void SetUser_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.SetUser(_session, null!, new Dictionary<string, object>()));
}
[Fact]
public void SetUser_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.SetUser(_session, "user@pve", null!));
}
[Fact]
public void RemoveUser_CallsDeleteWithCorrectPath()
{
// Arrange
_mockClient.Setup(c => c.DeleteAsync("access/users/deploy%40pve"))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.RemoveUser(_session, "deploy@pve");
// Assert
_mockClient.Verify(c => c.DeleteAsync("access/users/deploy%40pve"), Times.Once);
}
[Fact]
public void RemoveUser_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveUser(null!, "user@pve"));
}
[Fact]
public void RemoveUser_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveUser(_session, null!));
}
// =================================================================
// API Tokens
// =================================================================
[Fact]
public void GetApiTokens_ReturnsTokenArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/users/root%40pam/token"))
.ReturnsAsync(@"{""data"":[
{""tokenid"":""automation"",""privsep"":1,""expire"":0,""comment"":""CI/CD token""},
{""tokenid"":""monitoring"",""privsep"":0,""expire"":1735689600}
]}");
// Act
var result = _service.GetApiTokens(_session, "root@pam");
// Assert
Assert.Equal(2, result.Length);
Assert.Equal("automation", result[0].TokenId);
Assert.Equal("root@pam", result[0].UserId);
Assert.Equal(1, result[0].PrivilegeSeparation);
Assert.Equal("monitoring", result[1].TokenId);
Assert.Equal("root@pam", result[1].UserId);
}
[Fact]
public void GetApiTokens_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetApiTokens(null!, "root@pam"));
}
[Fact]
public void GetApiTokens_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetApiTokens(_session, null!));
}
[Fact]
public void CreateApiToken_ReturnsTokenWithSecret()
{
// Arrange
_mockClient.Setup(c => c.PostAsync(
"access/users/root%40pam/token/deploy",
It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":{""full-tokenid"":""root@pam!deploy"",""value"":""aabbccdd-1122-3344-5566-778899aabbcc"",""info"":{""privsep"":1,""expire"":0}}}");
// Act
var result = _service.CreateApiToken(_session, "root@pam", "deploy", comment: "Deploy token", privilegeSeparation: true);
// Assert
Assert.Equal("root@pam", result.UserId);
Assert.Equal("deploy", result.TokenId);
Assert.Equal("aabbccdd-1122-3344-5566-778899aabbcc", result.Value);
}
[Fact]
public void CreateApiToken_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.CreateApiToken(null!, "root@pam", "token1"));
}
[Fact]
public void CreateApiToken_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.CreateApiToken(_session, null!, "token1"));
}
[Fact]
public void CreateApiToken_NullTokenId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.CreateApiToken(_session, "root@pam", null!));
}
[Fact]
public void RemoveApiToken_CallsDeleteWithCorrectPath()
{
// Arrange
_mockClient.Setup(c => c.DeleteAsync("access/users/root%40pam/token/automation"))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.RemoveApiToken(_session, "root@pam", "automation");
// Assert
_mockClient.Verify(c => c.DeleteAsync("access/users/root%40pam/token/automation"), Times.Once);
}
[Fact]
public void RemoveApiToken_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.RemoveApiToken(null!, "root@pam", "token1"));
}
[Fact]
public void RemoveApiToken_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.RemoveApiToken(_session, null!, "token1"));
}
[Fact]
public void RemoveApiToken_NullTokenId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.RemoveApiToken(_session, "root@pam", null!));
}
[Fact]
public void UpdateApiToken_CallsPutWithCorrectPath()
{
// Arrange
var config = new Dictionary<string, string> { ["comment"] = "Updated comment" };
_mockClient.Setup(c => c.PutAsync("access/users/root%40pam/token/automation", config))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.UpdateApiToken(_session, "root@pam", "automation", config);
// Assert
_mockClient.Verify(c => c.PutAsync("access/users/root%40pam/token/automation", config), Times.Once);
}
[Fact]
public void UpdateApiToken_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateApiToken(null!, "root@pam", "token1", new Dictionary<string, string>()));
}
[Fact]
public void UpdateApiToken_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateApiToken(_session, "root@pam", "token1", null!));
}
// =================================================================
// Roles
// =================================================================
[Fact]
public void GetRoles_ReturnsRoleArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/roles"))
.ReturnsAsync(@"{""data"":[
{""roleid"":""Administrator"",""privs"":""Datastore.Allocate,Datastore.AllocateSpace,Datastore.Audit"",""special"":1},
{""roleid"":""PVEVMAdmin"",""privs"":""VM.Allocate,VM.Config.Disk,VM.Config.CPU"",""special"":1},
{""roleid"":""CustomOps"",""privs"":""VM.PowerMgmt,VM.Console"",""special"":0}
]}");
// Act
var result = _service.GetRoles(_session);
// Assert
Assert.Equal(3, result.Length);
Assert.Equal("Administrator", result[0].RoleId);
Assert.Equal(1, result[0].Special);
Assert.Equal("CustomOps", result[2].RoleId);
Assert.Equal(0, result[2].Special);
}
[Fact]
public void GetRoles_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetRoles(null!));
}
[Fact]
public void CreateRole_PostsFormData()
{
// Arrange
_mockClient.Setup(c => c.PostAsync("access/roles", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.CreateRole(_session, "BackupOperator", "Datastore.Audit,Datastore.AllocateSpace");
// Assert
_mockClient.Verify(c => c.PostAsync("access/roles",
It.Is<Dictionary<string, string>>(d =>
d["roleid"] == "BackupOperator" &&
d["privs"] == "Datastore.Audit,Datastore.AllocateSpace")),
Times.Once);
}
[Fact]
public void CreateRole_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateRole(null!, "role1"));
}
[Fact]
public void CreateRole_NullRoleId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateRole(_session, null!));
}
[Fact]
public void UpdateRole_PutsPrivileges()
{
// Arrange
_mockClient.Setup(c => c.PutAsync("access/roles/BackupOperator", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.UpdateRole(_session, "BackupOperator", "Datastore.Audit,Datastore.AllocateSpace,Datastore.Allocate");
// Assert
_mockClient.Verify(c => c.PutAsync("access/roles/BackupOperator",
It.Is<Dictionary<string, string>>(d =>
d["privs"] == "Datastore.Audit,Datastore.AllocateSpace,Datastore.Allocate")),
Times.Once);
}
[Fact]
public void UpdateRole_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateRole(null!, "role1", "privs"));
}
[Fact]
public void UpdateRole_NullRoleId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateRole(_session, null!, "privs"));
}
[Fact]
public void UpdateRole_NullPrivileges_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateRole(_session, "role1", null!));
}
[Fact]
public void RemoveRole_CallsDeleteWithCorrectPath()
{
// Arrange
_mockClient.Setup(c => c.DeleteAsync("access/roles/BackupOperator"))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.RemoveRole(_session, "BackupOperator");
// Assert
_mockClient.Verify(c => c.DeleteAsync("access/roles/BackupOperator"), Times.Once);
}
[Fact]
public void RemoveRole_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveRole(null!, "role1"));
}
[Fact]
public void RemoveRole_NullRoleId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveRole(_session, null!));
}
// =================================================================
// Groups
// =================================================================
[Fact]
public void GetGroups_ReturnsGroupArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/groups"))
.ReturnsAsync(@"{""data"":[
{""groupid"":""admins"",""comment"":""System administrators"",""users"":""root@pam,admin@pve""},
{""groupid"":""operators"",""comment"":""Operations team""}
]}");
// Act
var result = _service.GetGroups(_session);
// Assert
Assert.Equal(2, result.Length);
Assert.Equal("admins", result[0].GroupId);
Assert.Equal("System administrators", result[0].Comment);
Assert.Equal("root@pam,admin@pve", result[0].Users);
Assert.Equal("operators", result[1].GroupId);
}
[Fact]
public void GetGroups_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetGroups(null!));
}
[Fact]
public void CreateGroup_PostsFormData()
{
// Arrange
_mockClient.Setup(c => c.PostAsync("access/groups", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.CreateGroup(_session, "devops", "DevOps engineering team");
// Assert
_mockClient.Verify(c => c.PostAsync("access/groups",
It.Is<Dictionary<string, string>>(d =>
d["groupid"] == "devops" &&
d["comment"] == "DevOps engineering team")),
Times.Once);
}
[Fact]
public void CreateGroup_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateGroup(null!, "group1"));
}
[Fact]
public void CreateGroup_NullGroupId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.CreateGroup(_session, null!));
}
[Fact]
public void UpdateGroup_PutsConfig()
{
// Arrange
var config = new Dictionary<string, string> { ["comment"] = "Updated description" };
_mockClient.Setup(c => c.PutAsync("access/groups/devops", config))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.UpdateGroup(_session, "devops", config);
// Assert
_mockClient.Verify(c => c.PutAsync("access/groups/devops", config), Times.Once);
}
[Fact]
public void UpdateGroup_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateGroup(null!, "group1", new Dictionary<string, string>()));
}
[Fact]
public void UpdateGroup_NullGroupId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateGroup(_session, null!, new Dictionary<string, string>()));
}
[Fact]
public void UpdateGroup_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateGroup(_session, "group1", null!));
}
[Fact]
public void RemoveGroup_CallsDeleteWithCorrectPath()
{
// Arrange
_mockClient.Setup(c => c.DeleteAsync("access/groups/devops"))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.RemoveGroup(_session, "devops");
// Assert
_mockClient.Verify(c => c.DeleteAsync("access/groups/devops"), Times.Once);
}
[Fact]
public void RemoveGroup_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveGroup(null!, "group1"));
}
[Fact]
public void RemoveGroup_NullGroupId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveGroup(_session, null!));
}
// =================================================================
// Domains / Realms
// =================================================================
[Fact]
public void GetDomains_ReturnsDomainArray()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/domains"))
.ReturnsAsync(@"{""data"":[
{""realm"":""pam"",""type"":""pam"",""comment"":""Linux PAM standard authentication"",""default"":0},
{""realm"":""pve"",""type"":""pve"",""comment"":""Proxmox VE authentication server"",""default"":1},
{""realm"":""corp-ldap"",""type"":""ldap"",""comment"":""Corporate LDAP""}
]}");
// Act
var result = _service.GetDomains(_session);
// Assert
Assert.Equal(3, result.Length);
Assert.Equal("pam", result[0].Realm);
Assert.Equal("pam", result[0].Type);
Assert.Equal("pve", result[1].Realm);
Assert.Equal(1, result[1].Default);
Assert.Equal("corp-ldap", result[2].Realm);
Assert.Equal("ldap", result[2].Type);
}
[Fact]
public void GetDomains_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetDomains(null!));
}
[Fact]
public void CreateDomain_PostsConfig()
{
// Arrange
var config = new Dictionary<string, string>
{
["realm"] = "corp-ad",
["type"] = "ad",
["server1"] = "dc01.corp.local",
["domain"] = "corp.local"
};
_mockClient.Setup(c => c.PostAsync("access/domains", config))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.CreateDomain(_session, config);
// Assert
_mockClient.Verify(c => c.PostAsync("access/domains", config), Times.Once);
}
[Fact]
public void CreateDomain_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.CreateDomain(null!, new Dictionary<string, string>()));
}
[Fact]
public void CreateDomain_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.CreateDomain(_session, null!));
}
[Fact]
public void UpdateDomain_PutsConfig()
{
// Arrange
var config = new Dictionary<string, string> { ["comment"] = "Updated AD realm" };
_mockClient.Setup(c => c.PutAsync("access/domains/corp-ad", config))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.UpdateDomain(_session, "corp-ad", config);
// Assert
_mockClient.Verify(c => c.PutAsync("access/domains/corp-ad", config), Times.Once);
}
[Fact]
public void UpdateDomain_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateDomain(null!, "pam", new Dictionary<string, string>()));
}
[Fact]
public void UpdateDomain_NullRealm_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateDomain(_session, null!, new Dictionary<string, string>()));
}
[Fact]
public void UpdateDomain_NullConfig_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.UpdateDomain(_session, "pam", null!));
}
[Fact]
public void RemoveDomain_CallsDeleteWithCorrectPath()
{
// Arrange
_mockClient.Setup(c => c.DeleteAsync("access/domains/corp-ad"))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.RemoveDomain(_session, "corp-ad");
// Assert
_mockClient.Verify(c => c.DeleteAsync("access/domains/corp-ad"), Times.Once);
}
[Fact]
public void RemoveDomain_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveDomain(null!, "pam"));
}
[Fact]
public void RemoveDomain_NullRealm_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.RemoveDomain(_session, null!));
}
// =================================================================
// Password
// =================================================================
[Fact]
public void ChangePassword_PutsCredentials()
{
// Arrange
_mockClient.Setup(c => c.PutAsync("access/password", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.ChangePassword(_session, "deploy@pve", "newSecureP@ss!");
// Assert
_mockClient.Verify(c => c.PutAsync("access/password",
It.Is<Dictionary<string, string>>(d =>
d["userid"] == "deploy@pve" &&
d["password"] == "newSecureP@ss!")),
Times.Once);
}
[Fact]
public void ChangePassword_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.ChangePassword(null!, "user@pve", "pass"));
}
[Fact]
public void ChangePassword_NullUserId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.ChangePassword(_session, null!, "pass"));
}
[Fact]
public void ChangePassword_NullPassword_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.ChangePassword(_session, "user@pve", null!));
}
// =================================================================
// Permissions / ACLs
// =================================================================
[Fact]
public void GetPermissions_ReturnsPermissionsFromObjectResponse()
{
// Arrange — the PVE API returns permissions as a path-keyed object
_mockClient.Setup(c => c.GetAsync("access/permissions"))
.ReturnsAsync(@"{""data"":{
""/"":{ ""Datastore.Audit"":1, ""VM.Audit"":1 },
""/nodes/pve1"":{ ""Sys.Console"":1 }
}}");
// Act
var result = _service.GetPermissions(_session);
// Assert
Assert.Equal(2, result.Length);
Assert.Contains(result, p => p.Path == "/");
Assert.Contains(result, p => p.Path == "/nodes/pve1");
}
[Fact]
public void GetPermissions_WithUserIdFilter_AppendsQueryParam()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/permissions?userid=deploy%40pve"))
.ReturnsAsync(@"{""data"":{""/"":{}}}");
// Act
var result = _service.GetPermissions(_session, userId: "deploy@pve");
// Assert
_mockClient.Verify(c => c.GetAsync("access/permissions?userid=deploy%40pve"), Times.Once);
Assert.Single(result);
}
[Fact]
public void GetPermissions_WithPathFilter_AppendsQueryParam()
{
// Arrange
_mockClient.Setup(c => c.GetAsync("access/permissions?path=%2Fvms%2F100"))
.ReturnsAsync(@"{""data"":{}}");
// Act
var result = _service.GetPermissions(_session, path: "/vms/100");
// Assert
_mockClient.Verify(c => c.GetAsync("access/permissions?path=%2Fvms%2F100"), Times.Once);
Assert.Empty(result);
}
[Fact]
public void GetPermissions_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _service.GetPermissions(null!));
}
[Fact]
public void SetPermission_PutsAclData()
{
// Arrange
_mockClient.Setup(c => c.PutAsync("access/acl", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.SetPermission(_session, "/vms/100", "PVEVMAdmin", users: "deploy@pve", propagate: true);
// Assert
_mockClient.Verify(c => c.PutAsync("access/acl",
It.Is<Dictionary<string, string>>(d =>
d["path"] == "/vms/100" &&
d["roles"] == "PVEVMAdmin" &&
d["users"] == "deploy@pve" &&
d["propagate"] == "1" &&
d["delete"] == "0")),
Times.Once);
}
[Fact]
public void SetPermission_WithGroupAndDelete_PutsCorrectFlags()
{
// Arrange
_mockClient.Setup(c => c.PutAsync("access/acl", It.IsAny<Dictionary<string, string>>()))
.ReturnsAsync(@"{""data"":null}");
// Act
_service.SetPermission(_session, "/", "Administrator", groups: "admins", propagate: false, delete: true);
// Assert
_mockClient.Verify(c => c.PutAsync("access/acl",
It.Is<Dictionary<string, string>>(d =>
d["path"] == "/" &&
d["roles"] == "Administrator" &&
d["groups"] == "admins" &&
d["propagate"] == "0" &&
d["delete"] == "1")),
Times.Once);
}
[Fact]
public void SetPermission_NullSession_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.SetPermission(null!, "/", "Admin"));
}
[Fact]
public void SetPermission_NullPath_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.SetPermission(_session, null!, "Admin"));
}
[Fact]
public void SetPermission_NullRoles_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
_service.SetPermission(_session, "/", null!));
}
}
}