Skip to content

Commit 89614bc

Browse files
authored
Merge pull request #1 from dmstr/feature/unit-tests
✅ Unit-Tests (KeycloakUser, KeycloakUserProvider) + CI
2 parents 4a9b420 + d43bcff commit 89614bc

7 files changed

Lines changed: 233 additions & 0 deletions

File tree

.github/workflows/tests.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# file generated with AI assistance: Claude Code - 2026-06-13 23:14:54 UTC
2+
name: Tests
3+
4+
on:
5+
push:
6+
branches: [main, master, "feature/**"]
7+
pull_request:
8+
9+
jobs:
10+
phpunit:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
php: ["8.3", "8.4"]
16+
name: PHPUnit (PHP ${{ matrix.php }})
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: shivammathur/setup-php@v2
20+
with:
21+
php-version: ${{ matrix.php }}
22+
coverage: none
23+
- run: composer install --no-interaction --no-progress
24+
- run: vendor/bin/phpunit

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# file generated with AI assistance: Claude Code - 2026-06-13 23:14:54 UTC
2+
/vendor/
3+
/composer.lock
4+
/.phpunit.cache/

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
"symfony/security-bundle": "^7.0",
1616
"lexik/jwt-authentication-bundle": "^3.0"
1717
},
18+
"require-dev": {
19+
"phpunit/phpunit": "^12.0"
20+
},
1821
"autoload": {
1922
"psr-4": {
2023
"Dmstr\\KeycloakSecurity\\": "src/"

docker-compose.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# file generated with AI assistance: Claude Code - 2026-06-13 23:14:54 UTC
2+
# CLI-only test runner for this bundle — no MySQL/FPM required.
3+
# Usage (on host):
4+
# docker compose run --rm php
5+
# Runs `composer install` then PHPUnit against tests/.
6+
services:
7+
php:
8+
build:
9+
dockerfile_inline: |
10+
FROM php:8.4-cli-alpine
11+
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
12+
working_dir: /repo
13+
volumes:
14+
- .:/repo
15+
command: sh -c "composer install --no-interaction --no-progress && vendor/bin/phpunit"

phpunit.dist.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!-- file generated with AI assistance: Claude Code - 2026-06-13 23:14:54 UTC -->
3+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
5+
bootstrap="vendor/autoload.php"
6+
colors="true"
7+
cacheDirectory=".phpunit.cache">
8+
<testsuites>
9+
<testsuite name="default">
10+
<directory>tests</directory>
11+
</testsuite>
12+
</testsuites>
13+
<source>
14+
<include>
15+
<directory>src</directory>
16+
</include>
17+
</source>
18+
</phpunit>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
// file generated with AI assistance: Claude Code - 2026-06-13 23:14:54 UTC
3+
4+
declare(strict_types=1);
5+
6+
namespace Dmstr\KeycloakSecurity\Tests\Security;
7+
8+
use Dmstr\KeycloakSecurity\Security\KeycloakUser;
9+
use Dmstr\KeycloakSecurity\Security\KeycloakUserProvider;
10+
use PHPUnit\Framework\TestCase;
11+
12+
/**
13+
* Unit tests for {@see KeycloakUserProvider}.
14+
*
15+
* Exercises the realm_access.roles → ROLE_* mapping (uppercasing, filtering of
16+
* Keycloak-internal roles) and claim extraction via the public
17+
* loadUserByIdentifierAndPayload() entry point.
18+
*/
19+
final class KeycloakUserProviderTest extends TestCase
20+
{
21+
private KeycloakUserProvider $provider;
22+
23+
protected function setUp(): void
24+
{
25+
$this->provider = new KeycloakUserProvider();
26+
}
27+
28+
public function testMapsRealmRolesToUppercasedSymfonyRoles(): void
29+
{
30+
$user = $this->provider->loadUserByIdentifierAndPayload('alice', [
31+
'realm_access' => ['roles' => ['admin', 'editor']],
32+
]);
33+
34+
$roles = array_values($user->getRoles());
35+
self::assertContains('ROLE_ADMIN', $roles);
36+
self::assertContains('ROLE_EDITOR', $roles);
37+
self::assertContains('ROLE_USER', $roles);
38+
}
39+
40+
public function testFiltersKeycloakInternalRoles(): void
41+
{
42+
$user = $this->provider->loadUserByIdentifierAndPayload('alice', [
43+
'realm_access' => ['roles' => [
44+
'admin',
45+
'default-roles-myrealm',
46+
'offline_access',
47+
'uma_authorization',
48+
]],
49+
]);
50+
51+
// Only ROLE_ADMIN survives the filter; ROLE_USER is added by the user.
52+
self::assertEqualsCanonicalizing(['ROLE_ADMIN', 'ROLE_USER'], array_values($user->getRoles()));
53+
}
54+
55+
public function testExtractsEmailAndName(): void
56+
{
57+
$user = $this->provider->loadUserByIdentifierAndPayload('alice', [
58+
'email' => 'alice@example.com',
59+
'given_name' => 'Alice',
60+
'family_name' => 'Anderson',
61+
]);
62+
63+
self::assertInstanceOf(KeycloakUser::class, $user);
64+
self::assertSame('alice', $user->getUserIdentifier());
65+
self::assertSame('alice@example.com', $user->getEmail());
66+
self::assertSame('Alice', $user->getGivenName());
67+
self::assertSame('Anderson', $user->getFamilyName());
68+
}
69+
70+
public function testMissingClaimsDefaultToEmpty(): void
71+
{
72+
$user = $this->provider->loadUserByIdentifierAndPayload('alice', []);
73+
74+
self::assertSame('', $user->getEmail());
75+
self::assertSame('', $user->getGivenName());
76+
self::assertSame('', $user->getFamilyName());
77+
self::assertSame(['ROLE_USER'], array_values($user->getRoles()));
78+
}
79+
80+
public function testLoadUserByIdentifierFallbackHasNoRealmRoles(): void
81+
{
82+
$user = $this->provider->loadUserByIdentifier('alice');
83+
84+
self::assertSame('alice', $user->getUserIdentifier());
85+
self::assertSame(['ROLE_USER'], array_values($user->getRoles()));
86+
}
87+
88+
public function testRefreshUserReturnsSameInstance(): void
89+
{
90+
$user = new KeycloakUser('alice', ['ROLE_ADMIN']);
91+
self::assertSame($user, $this->provider->refreshUser($user));
92+
}
93+
94+
public function testSupportsOnlyKeycloakUserClass(): void
95+
{
96+
self::assertTrue($this->provider->supportsClass(KeycloakUser::class));
97+
self::assertFalse($this->provider->supportsClass(\stdClass::class));
98+
}
99+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
// file generated with AI assistance: Claude Code - 2026-06-13 23:14:54 UTC
3+
4+
declare(strict_types=1);
5+
6+
namespace Dmstr\KeycloakSecurity\Tests\Security;
7+
8+
use Dmstr\KeycloakSecurity\Security\KeycloakUser;
9+
use PHPUnit\Framework\TestCase;
10+
11+
/**
12+
* Unit tests for {@see KeycloakUser} — the immutable DTO wrapping a Keycloak
13+
* JWT identity. Verifies the mandatory ROLE_USER guarantee, deduplication and
14+
* the plain getters.
15+
*/
16+
final class KeycloakUserTest extends TestCase
17+
{
18+
public function testGetRolesAlwaysAddsRoleUser(): void
19+
{
20+
$user = new KeycloakUser('alice', ['ROLE_ADMIN']);
21+
22+
$roles = array_values($user->getRoles());
23+
self::assertContains('ROLE_ADMIN', $roles);
24+
self::assertContains('ROLE_USER', $roles);
25+
}
26+
27+
public function testGetRolesDeduplicatesRoleUser(): void
28+
{
29+
$user = new KeycloakUser('alice', ['ROLE_USER', 'ROLE_ADMIN']);
30+
31+
self::assertSame(
32+
['ROLE_USER', 'ROLE_ADMIN'],
33+
array_values($user->getRoles()),
34+
'ROLE_USER must not be duplicated when already present.'
35+
);
36+
}
37+
38+
public function testEmptyRolesYieldOnlyRoleUser(): void
39+
{
40+
self::assertSame(['ROLE_USER'], array_values((new KeycloakUser('alice'))->getRoles()));
41+
}
42+
43+
public function testGettersExposeConstructorValues(): void
44+
{
45+
$user = new KeycloakUser('alice', [], 'alice@example.com', 'Alice', 'Anderson');
46+
47+
self::assertSame('alice', $user->getUserIdentifier());
48+
self::assertSame('alice@example.com', $user->getEmail());
49+
self::assertSame('Alice', $user->getGivenName());
50+
self::assertSame('Anderson', $user->getFamilyName());
51+
}
52+
53+
public function testGettersDefaultToEmptyStrings(): void
54+
{
55+
$user = new KeycloakUser('alice');
56+
57+
self::assertSame('', $user->getEmail());
58+
self::assertSame('', $user->getGivenName());
59+
self::assertSame('', $user->getFamilyName());
60+
}
61+
62+
public function testEraseCredentialsIsNoop(): void
63+
{
64+
$user = new KeycloakUser('alice', ['ROLE_ADMIN'], 'alice@example.com');
65+
$user->eraseCredentials();
66+
67+
self::assertSame('alice', $user->getUserIdentifier());
68+
self::assertSame('alice@example.com', $user->getEmail());
69+
}
70+
}

0 commit comments

Comments
 (0)