Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
migration. Also fixed the `validateIdToken` example to catch the
marker interface instead of the now-deprecated abstract.

### Tooling

- PHPStan now scans `tests/` in addition to `src/` at level 8, with
`reportIgnoresWithoutComments: true` so unexplained
`@phpstan-ignore` directives fail CI.
- Added `phpstan/phpstan-mockery` to `require-dev` for stubs covering
Mockery's fluent `shouldReceive(...)->andReturn(...)` API.
- Cleaned the 46 pre-existing level-8 issues in `tests/`: dropped the
unused nullable from `$this->provider`, narrowed `validateIdToken`
claim accesses with `object{nonce, aud}` `@var` shapes, replaced
silent `(string)` coercion of `file_get_contents` / `parse_url`
failures with `assertNotFalse` / `assertIsString` boundary guards,
swapped `assertTrue(true)` tautologies for
`expectNotToPerformAssertions`, and replaced the constant-folded
`is_subclass_of` marker check with a `ReflectionClass` lookup so
PHPStan can't fold it into a tautology. `phpstan-baseline.neon`
consequently shrinks to zero and is deleted.

## [4.1.2] - 2026-05-11

- Chained `previous` consistently in `OpenIdConfigurationProvider` catch
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"friendsofphp/php-cs-fixer": "^3.75",
"mockery/mockery": "^1.6.12",
"phpstan/phpstan": "^2.1.41",
"phpstan/phpstan-mockery": "^2.0",
"phpunit/php-code-coverage": "^12",
"phpunit/phpunit": "^12"
},
Expand Down
139 changes: 0 additions & 139 deletions phpstan-baseline.neon

This file was deleted.

2 changes: 1 addition & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
includes:
- phpstan-baseline.neon
- vendor/phpstan/phpstan-mockery/extension.neon

parameters:
level: 8
Expand Down
14 changes: 9 additions & 5 deletions tests/Exception/ExceptionHierarchyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,15 @@ public function testAbstractBaseImplementsMarker(): void
// Catch sites that wrote `catch (ItkOpenIdConnectException $e)` should
// migrate to the marker interface; this assertion guards the marker
// implementation while the deprecation window is open.
$this->assertTrue(
is_subclass_of(
\ItkDev\OpenIdConnect\Exception\ItkOpenIdConnectException::class,
OpenIdConnectExceptionInterface::class,
),
//
// ReflectionClass keeps the check at runtime so PHPStan can't fold it
// into a constant tautology — the value of the test is catching a
// *future* regression that removes the marker from the abstract.
$reflection = new \ReflectionClass(\ItkDev\OpenIdConnect\Exception\ItkOpenIdConnectException::class);
$this->assertContains(
OpenIdConnectExceptionInterface::class,
$reflection->getInterfaceNames(),
'Deprecated abstract must still implement the marker for 5.x BC.',
);
}
}
2 changes: 1 addition & 1 deletion tests/Security/MockJWT.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

class MockJWT
{
public static $leeway;
public static ?int $leeway = null;
}
49 changes: 31 additions & 18 deletions tests/Security/OpenIdConfigurationProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class OpenIdConfigurationProviderTest extends TestCase
private const REDIRECT_URI = 'https://redirect.url';
private const NONCE = '12345678';

private ?OpenIdConfigurationProvider $provider;
private OpenIdConfigurationProvider $provider;

public function setUp(): void
{
Expand Down Expand Up @@ -168,7 +168,9 @@ public function testGetAuthorizationUrl(): void

$authUrl = $this->provider->getAuthorizationUrl(['state' => $state, 'nonce' => $nonce]);
$query = [];
parse_str(parse_url($authUrl, PHP_URL_QUERY), $query);
$queryString = parse_url($authUrl, PHP_URL_QUERY);
$this->assertIsString($queryString, 'Generated authorization URL must have a query string');
parse_str($queryString, $query);

$this->assertSame('openid', $query['scope']);
$this->assertSame('id_token', $query['response_type']);
Expand Down Expand Up @@ -238,6 +240,7 @@ public function testValidateIdTokenSuccess(): void
)
)->andReturn($mockClaims);

/** @var object{nonce: string, aud: string|list<string>} $claims */
$claims = $this->provider->validateIdToken('token', self::NONCE);

$this->assertEquals(self::NONCE, $claims->nonce);
Expand Down Expand Up @@ -370,14 +373,15 @@ public function testGetResourceOwnerDetailsUrl(): void

public function testCheckResponseSuccess(): void
{
$this->expectNotToPerformAssertions();

$response = $this->createStub(ResponseInterface::class);
$response->method('getStatusCode')->willReturn(200);

$method = new \ReflectionMethod(OpenIdConfigurationProvider::class, 'checkResponse');

// Should not throw
$method->invoke($this->provider, $response, ['data' => 'value']);
$this->assertTrue(true);
}

public function testCheckResponseWithErrorString(): void
Expand Down Expand Up @@ -436,6 +440,7 @@ public function testValidateIdTokenArrayAudience(): void

$mockJWT->shouldReceive('decode')->andReturn($mockClaims);

/** @var object{nonce: string, aud: string|list<string>} $claims */
$claims = $this->provider->validateIdToken('token', self::NONCE);

$this->assertEquals(self::NONCE, $claims->nonce);
Expand Down Expand Up @@ -542,10 +547,7 @@ public function testGetIdTokenFailure(): void

public function testGetConfigurationCacheHit(): void
{
$configuration = json_decode(
file_get_contents(__DIR__.'/../MockData/mockOpenIDConfiguration.json'),
true
);
$configuration = $this->loadMockFixture('mockOpenIDConfiguration.json');

$mockCacheItem = $this->createStub(CacheItemInterface::class);
$mockCacheItem->method('isHit')->willReturn(true);
Expand Down Expand Up @@ -730,10 +732,7 @@ public function testGetJwtVerificationKeysCacheHit(): void
{
$openIDConnectMetadataUrl = 'https://some.url/openid-configuration';

$configuration = json_decode(
file_get_contents(__DIR__.'/../MockData/mockOpenIDConfiguration.json'),
true
);
$configuration = $this->loadMockFixture('mockOpenIDConfiguration.json');

$cachedKeys = ['key1' => new Key('public-key-data', 'RS256')];

Expand Down Expand Up @@ -770,6 +769,7 @@ public function testGetJwtVerificationKeysCacheHit(): void
$mockClaims = $this->getMockClaims();
$mockJWT->shouldReceive('decode')->andReturn($mockClaims);

/** @var object{nonce: string} $claims */
$claims = $provider->validateIdToken('token', self::NONCE);
$this->assertEquals(self::NONCE, $claims->nonce);
}
Expand Down Expand Up @@ -804,10 +804,7 @@ public function testGetConfigurationCacheInvalidArgument(): void
public function testGetJwtVerificationKeysCacheInvalidArgument(): void
{
$openIDConnectMetadataUrl = 'https://some.url/openid-configuration';
$configuration = json_decode(
file_get_contents(__DIR__.'/../MockData/mockOpenIDConfiguration.json'),
true
);
$configuration = $this->loadMockFixture('mockOpenIDConfiguration.json');

$configCacheItem = $this->createStub(CacheItemInterface::class);
$configCacheItem->method('isHit')->willReturn(true);
Expand Down Expand Up @@ -892,12 +889,28 @@ public function testBase64urlDecodeFailure(): void
/**
* Get a mock success response with mock date.
*
* @param string $mockResponseDataPath
* Path to the file containing the mock response data
*
* @return ResponseInterface
* A success ("200") response with mock body data
*/
/**
* Load a JSON fixture from tests/MockData and decode it as an associative
* array. Fails the test with an explicit message if the file is missing /
* unreadable / not valid JSON, rather than letting `false` or `null` flow
* silently into the assertion under test.
*
* @return array<string, mixed>
*/
private function loadMockFixture(string $filename): array
{
$path = __DIR__.'/../MockData/'.$filename;
$contents = file_get_contents($path);
$this->assertNotFalse($contents, sprintf('Mock fixture not readable: %s', $path));
$decoded = json_decode($contents, true);
$this->assertIsArray($decoded, sprintf('Mock fixture is not valid JSON: %s', $path));

return $decoded;
}

private function getMockHttpSuccessResponse(string $mockResponseDataPath): ResponseInterface
{
$mockResponseData = file_get_contents(__DIR__.$mockResponseDataPath);
Expand Down
Loading