* @license MIT */ namespace Tests\Addons\Unit; use PHPUnit\Framework\TestCase; use Illuminate\Support\Collection; use Pterodactyl\Addons\AdvancedAdmin\Models\ServerPermissionOverride; use Pterodactyl\Addons\AdvancedAdmin\Services\Roles\PermissionResolver; class PermissionResolverTest extends TestCase { private PermissionResolver $resolver; protected function setUp(): void { parent::setUp(); $this->resolver = new PermissionResolver(); } /** Resolution order: server DENY beats role ALLOW */ public function test_server_deny_overrides_role_allow(): void { $override = $this->makeOverride('deny'); $role = $this->makeRole('allow'); $result = $this->resolveWithMocks([$override], [$role], 'file.read'); $this->assertFalse($result, 'Server DENY should override role ALLOW'); } /** Resolution order: server ALLOW beats role DENY */ public function test_server_allow_overrides_role_deny(): void { $override = $this->makeOverride('allow'); $role = $this->makeRole('deny'); $result = $this->resolveWithMocks([$override], [$role], 'file.read'); $this->assertTrue($result, 'Server ALLOW should override role DENY'); } /** Role DENY should deny when no server override is set */ public function test_role_deny_denies_when_no_server_override(): void { $override = $this->makeOverride('unset'); $role = $this->makeRole('deny'); $result = $this->resolveWithMocks([$override], [$role], 'file.read'); $this->assertFalse($result, 'Role DENY should deny when server override is unset'); } /** Role ALLOW should allow when no server override is set */ public function test_role_allow_allows_when_no_server_override(): void { $override = $this->makeOverride('unset'); $role = $this->makeRole('allow'); $result = $this->resolveWithMocks([$override], [$role], 'file.read'); $this->assertTrue($result, 'Role ALLOW should allow when server override is unset'); } /** Default deny when nothing is set */ public function test_default_deny_when_nothing_set(): void { $override = $this->makeOverride('unset'); $result = $this->resolveWithMocks([$override], [], 'file.read'); $this->assertFalse($result, 'Should default to deny when no rules set'); } /** Higher priority role wins on conflict */ public function test_higher_priority_role_wins(): void { $lowRole = $this->makeRole('deny', priority: 10); $highRole = $this->makeRole('allow', priority: 100); $override = $this->makeOverride('unset'); // High priority role should win (sorted desc by priority) $result = $this->resolveWithMocks([$override], [$lowRole, $highRole], 'file.read'); $this->assertTrue($result, 'Higher priority role (allow) should win over lower priority (deny)'); } /** No privilege escalation — deny still blocks even if other role allows */ public function test_explicit_deny_in_any_role_blocks(): void { // If ANY role in resolution order says deny first (by priority), it should deny $denyRole = $this->makeRole('deny', priority: 50); $allowRole = $this->makeRole('allow', priority: 10); $override = $this->makeOverride('unset'); $result = $this->resolveWithMocks([$override], [$denyRole, $allowRole], 'file.read'); $this->assertFalse($result, 'Deny in higher-priority role should block allow in lower role'); } // ─── Helpers ───────────────────────────────────────────────────────────── private function resolveWithMocks(array $overrides, array $roles, string $permission): bool { // Use a test double of the resolver that accepts mock data $resolver = new class extends PermissionResolver { public array $mockOverrides = []; public array $mockRoles = []; public function resolve(int $userId, int $serverId, string $permission): bool { $overridesCollection = collect($this->mockOverrides)->keyBy('permission'); $rolesCollection = collect($this->mockRoles)->sortByDesc('priority')->values(); [$result] = $this->resolveWithSourcePublic($permission, $overridesCollection, $rolesCollection); return $result; } public function resolveWithSourcePublic(string $perm, $overrides, $roles): array { return $this->resolveWithSource($perm, $overrides, $roles); } }; // This tests the private method via reflection for correctness $ref = new \ReflectionMethod(PermissionResolver::class, 'resolveWithSource'); $ref->setAccessible(true); $overridesCol = collect($overrides)->keyBy('permission'); $rolesCol = collect($roles)->sortByDesc('priority')->values(); [$result] = $ref->invoke($this->resolver, $permission, $overridesCol, $rolesCol); return $result; } private function makeOverride(string $value): object { return new class ($value) { public string $permission = 'file.read'; public string $value; public function __construct(string $v) { $this->value = $v; } public function isAllow(): bool { return $this->value === 'allow'; } public function isDeny(): bool { return $this->value === 'deny'; } public function isUnset(): bool { return $this->value === 'unset'; } }; } private function makeRole(string $value, int $priority = 50): object { return new class ($value, $priority) { public int $priority; public string $name = 'Test Role'; private string $val; public function __construct(string $v, int $p) { $this->val = $v; $this->priority = $p; } public function getPermission(string $p): ?string { return $this->val === 'unset' ? null : $this->val; } }; } }