Skip to content

Commit d5ebdd0

Browse files
authored
Merge pull request #50 from itk-dev/test/mutation-exception-assertions
test: assert full exception messages, codes and previous-chains
2 parents 7d72ac7 + dc256f3 commit d5ebdd0

2 files changed

Lines changed: 132 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
(`task test:mutation`), run in CI and reported to the Stryker dashboard
1414
(mutation score badge in README)
1515

16+
### Changed
17+
18+
- Strengthened exception assertions guided by mutation testing: thrown
19+
messages are asserted in full (including dynamic parts), wrap-boundary
20+
exceptions assert code `0` and the chained `$previous` cause, and
21+
invalid JSON from the token endpoint is covered
22+
1623
## [5.0.0] - 2026-06-02
1724

1825
Reworked exception hierarchy and tightened IdP-payload validations. The runtime

tests/Security/OpenIdConfigurationProviderTest.php

Lines changed: 125 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,16 @@ public function testValidateIdTokenFailure(): void
254254
$mockJWT = \Mockery::mock('overload:Firebase\JWT\JWT', MockJWT::class);
255255
$mockJWT->shouldReceive('decode')->andThrow(SignatureInvalidException::class, 'Signature verification failed');
256256

257-
$this->expectException(ValidationException::class);
258-
$this->expectExceptionMessage('ID token validation failed');
259-
260-
$this->provider->validateIdToken('token', self::NONCE);
257+
try {
258+
$this->provider->validateIdToken('token', self::NONCE);
259+
} catch (ValidationException $thrown) {
260+
$this->assertSame('ID token validation failed: Signature verification failed', $thrown->getMessage());
261+
$this->assertSame(0, $thrown->getCode());
262+
$this->assertInstanceOf(SignatureInvalidException::class, $thrown->getPrevious(), 'Original cause must be chained');
263+
264+
return;
265+
}
266+
$this->fail('Expected ValidationException');
261267
}
262268

263269
public function testValidateIdTokenAudience(): void
@@ -270,7 +276,7 @@ public function testValidateIdTokenAudience(): void
270276
$mockJWT->shouldReceive('decode')->andReturn($mockClaims);
271277

272278
$this->expectException(ClaimsException::class);
273-
$this->expectExceptionMessage('ID token has incorrect audience');
279+
$this->expectExceptionMessage('ID token has incorrect audience(s): incorrect aud');
274280

275281
$this->provider->validateIdToken('token', self::NONCE);
276282
}
@@ -285,7 +291,7 @@ public function testValidateIdTokenIssuer(): void
285291
$mockJWT->shouldReceive('decode')->andReturn($mockClaims);
286292

287293
$this->expectException(ClaimsException::class);
288-
$this->expectExceptionMessage('ID token has incorrect issuer');
294+
$this->expectExceptionMessage('ID token has incorrect issuer: incorrect iss');
289295

290296
$this->provider->validateIdToken('token', self::NONCE);
291297
}
@@ -300,7 +306,7 @@ public function testValidateIdTokenNonce(): void
300306
$mockJWT->shouldReceive('decode')->andReturn($mockClaims);
301307

302308
$this->expectException(ClaimsException::class);
303-
$this->expectExceptionMessage('ID token has incorrect nonce');
309+
$this->expectExceptionMessage('ID token has incorrect nonce: incorrect nonce');
304310

305311
$this->provider->validateIdToken('token', self::NONCE);
306312
}
@@ -310,7 +316,7 @@ public function testConstructBadUrl(): void
310316
$mockCacheItemPool = $this->createStub(CacheItemPoolInterface::class);
311317

312318
$this->expectException(BadUrlException::class);
313-
$this->expectExceptionMessage('OpenIDConnectMetadataUrl is invalid');
319+
$this->expectExceptionMessage('OpenIDConnectMetadataUrl is invalid: not-a-valid-url');
314320

315321
new OpenIdConfigurationProvider([
316322
'cacheItemPool' => $mockCacheItemPool,
@@ -323,7 +329,7 @@ public function testConstructHttpUrlNotAllowed(): void
323329
$mockCacheItemPool = $this->createStub(CacheItemPoolInterface::class);
324330

325331
$this->expectException(IllegalSchemeException::class);
326-
$this->expectExceptionMessage('OpenIDConnectMetadataUrl must use https');
332+
$this->expectExceptionMessage('OpenIDConnectMetadataUrl must use https: http://some.url/openid-configuration');
327333

328334
new OpenIdConfigurationProvider([
329335
'cacheItemPool' => $mockCacheItemPool,
@@ -421,10 +427,15 @@ public function testCheckResponseWithErrorStatusCode(): void
421427

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

424-
$this->expectException(IdentityProviderException::class);
425-
$this->expectExceptionMessage('400');
430+
try {
431+
$method->invoke($this->provider, $response, []);
432+
} catch (IdentityProviderException $thrown) {
433+
$this->assertSame('400', $thrown->getMessage());
434+
$this->assertSame(0, $thrown->getCode());
426435

427-
$method->invoke($this->provider, $response, []);
436+
return;
437+
}
438+
$this->fail('Expected IdentityProviderException');
428439
}
429440

430441
public function testCreateResourceOwner(): void
@@ -464,7 +475,7 @@ public function testValidateIdTokenArrayAudienceInvalid(): void
464475
$mockJWT->shouldReceive('decode')->andReturn($mockClaims);
465476

466477
$this->expectException(ClaimsException::class);
467-
$this->expectExceptionMessage('ID token has incorrect audience');
478+
$this->expectExceptionMessage('ID token has incorrect audience(s): wrong_client_1, wrong_client_2');
468479

469480
$this->provider->validateIdToken('token', self::NONCE);
470481
}
@@ -547,10 +558,71 @@ public function testGetIdTokenFailure(): void
547558
'httpClient' => $mockHttpClient,
548559
]);
549560

550-
$this->expectException(CodeException::class);
551-
$this->expectExceptionMessage('Get ID token failed');
561+
try {
562+
$provider->getIdToken('test-code');
563+
} catch (CodeException $thrown) {
564+
$this->assertSame('Get ID token failed: Connection failed', $thrown->getMessage());
565+
$this->assertSame(0, $thrown->getCode());
566+
$this->assertInstanceOf(ClientExceptionInterface::class, $thrown->getPrevious(), 'Original cause must be chained');
552567

553-
$provider->getIdToken('test-code');
568+
return;
569+
}
570+
$this->fail('Expected CodeException');
571+
}
572+
573+
public function testGetIdTokenRejectsInvalidJsonResponse(): void
574+
{
575+
$tokenEndpoint = 'https://azure_b2c_test.b2clogin.com/azure_b2c_test.onmicrosoft.com/oauth2/v2.0/token?p=test-policy';
576+
$openIDConnectMetadataUrl = 'https://some.url/openid-configuration';
577+
578+
$mockConfigResponse = $this->getMockHttpSuccessResponse('/../MockData/mockOpenIDConfiguration.json');
579+
580+
$malformedTokenResponseBody = 'not valid json{{{';
581+
$mockTokenStream = $this->createStub(StreamInterface::class);
582+
$mockTokenStream->method('getContents')->willReturn($malformedTokenResponseBody);
583+
$mockTokenStream->method('__toString')->willReturn($malformedTokenResponseBody);
584+
585+
$mockTokenResponse = $this->createStub(ResponseInterface::class);
586+
$mockTokenResponse->method('getStatusCode')->willReturn(200);
587+
$mockTokenResponse->method('getBody')->willReturn($mockTokenStream);
588+
589+
$mockHttpClient = $this->createStub(ClientInterface::class);
590+
$mockHttpClient->method('request')->willReturnMap([
591+
['GET', $openIDConnectMetadataUrl, [], $mockConfigResponse],
592+
['POST', $tokenEndpoint, ['form_params' => [
593+
'client_id' => self::CLIENT_ID,
594+
'client_secret' => self::CLIENT_SECRET,
595+
'redirect_uri' => self::REDIRECT_URI,
596+
'grant_type' => 'authorization_code',
597+
'code' => 'test-code',
598+
]], $mockTokenResponse],
599+
]);
600+
601+
$mockCacheItem = $this->createStub(CacheItemInterface::class);
602+
$mockCacheItem->method('isHit')->willReturn(false);
603+
604+
$mockCacheItemPool = $this->createStub(CacheItemPoolInterface::class);
605+
$mockCacheItemPool->method('getItem')->willReturn($mockCacheItem);
606+
607+
$provider = new OpenIdConfigurationProvider([
608+
'openIDConnectMetadataUrl' => $openIDConnectMetadataUrl,
609+
'cacheItemPool' => $mockCacheItemPool,
610+
'clientId' => self::CLIENT_ID,
611+
'clientSecret' => self::CLIENT_SECRET,
612+
'redirectUri' => self::REDIRECT_URI,
613+
], [
614+
'httpClient' => $mockHttpClient,
615+
]);
616+
617+
try {
618+
$provider->getIdToken('test-code');
619+
} catch (CodeException $thrown) {
620+
$this->assertSame(0, $thrown->getCode());
621+
$this->assertInstanceOf(\JsonException::class, $thrown->getPrevious(), 'Original cause must be chained');
622+
623+
return;
624+
}
625+
$this->fail('Expected CodeException');
554626
}
555627

556628
public function testGetIdTokenRejectsResponseWithoutStringIdToken(): void
@@ -698,7 +770,7 @@ public function testFetchJsonResourceNon200(): void
698770
]);
699771

700772
$this->expectException(HttpException::class);
701-
$this->expectExceptionMessage('Cannot access json resource');
773+
$this->expectExceptionMessage('Cannot access json resource: https://some.url/openid-configuration');
702774

703775
$provider->getBaseAuthorizationUrl();
704776
}
@@ -728,10 +800,16 @@ public function testFetchJsonResourceClientException(): void
728800
'httpClient' => $mockHttpClient,
729801
]);
730802

731-
$this->expectException(HttpException::class);
732-
$this->expectExceptionMessage('Connection refused');
803+
try {
804+
$provider->getBaseAuthorizationUrl();
805+
} catch (HttpException $thrown) {
806+
$this->assertSame('Connection refused', $thrown->getMessage());
807+
$this->assertSame(0, $thrown->getCode());
808+
$this->assertSame($exception, $thrown->getPrevious(), 'Original cause must be chained');
733809

734-
$provider->getBaseAuthorizationUrl();
810+
return;
811+
}
812+
$this->fail('Expected HttpException');
735813
}
736814

737815
public function testFetchJsonResourceInvalidJson(): void
@@ -764,9 +842,15 @@ public function testFetchJsonResourceInvalidJson(): void
764842
'httpClient' => $mockHttpClient,
765843
]);
766844

767-
$this->expectException(\ItkDev\OpenIdConnect\Exception\JsonException::class);
845+
try {
846+
$provider->getBaseAuthorizationUrl();
847+
} catch (\ItkDev\OpenIdConnect\Exception\JsonException $thrown) {
848+
$this->assertSame(0, $thrown->getCode());
849+
$this->assertInstanceOf(\JsonException::class, $thrown->getPrevious(), 'Original cause must be chained');
768850

769-
$provider->getBaseAuthorizationUrl();
851+
return;
852+
}
853+
$this->fail('Expected JsonException');
770854
}
771855

772856
public function testGetJwtVerificationKeysRejectsJwksMissingKeysArray(): void
@@ -978,10 +1062,16 @@ public function testGetConfigurationCacheInvalidArgument(): void
9781062
'httpClient' => $mockHttpClient,
9791063
]);
9801064

981-
$this->expectException(CacheException::class);
982-
$this->expectExceptionMessage('Invalid cache key');
1065+
try {
1066+
$provider->getBaseAuthorizationUrl();
1067+
} catch (CacheException $thrown) {
1068+
$this->assertSame('Invalid cache key', $thrown->getMessage());
1069+
$this->assertSame(0, $thrown->getCode());
1070+
$this->assertSame($exception, $thrown->getPrevious(), 'Original cause must be chained');
9831071

984-
$provider->getBaseAuthorizationUrl();
1072+
return;
1073+
}
1074+
$this->fail('Expected CacheException');
9851075
}
9861076

9871077
public function testGetJwtVerificationKeysCacheInvalidArgument(): void
@@ -1017,10 +1107,16 @@ public function testGetJwtVerificationKeysCacheInvalidArgument(): void
10171107
'httpClient' => $mockHttpClient,
10181108
]);
10191109

1020-
$this->expectException(CacheException::class);
1021-
$this->expectExceptionMessage('Invalid jwks cache key');
1110+
try {
1111+
$provider->validateIdToken('token', self::NONCE);
1112+
} catch (CacheException $thrown) {
1113+
$this->assertSame('Invalid jwks cache key', $thrown->getMessage());
1114+
$this->assertSame(0, $thrown->getCode());
1115+
$this->assertSame($exception, $thrown->getPrevious(), 'Original cause must be chained');
10221116

1023-
$provider->validateIdToken('token', self::NONCE);
1117+
return;
1118+
}
1119+
$this->fail('Expected CacheException');
10241120
}
10251121

10261122
public function testBase64urlDecodeFailure(): void
@@ -1065,7 +1161,7 @@ public function testBase64urlDecodeFailure(): void
10651161
$mockJWT = \Mockery::mock('overload:Firebase\JWT\JWT', MockJWT::class);
10661162

10671163
$this->expectException(\ItkDev\OpenIdConnect\Exception\DecodeException::class);
1068-
$this->expectExceptionMessage('Error url decoding input');
1164+
$this->expectExceptionMessage('Error url decoding input !!!');
10691165

10701166
$provider->validateIdToken('token', self::NONCE);
10711167
}

0 commit comments

Comments
 (0)