diff --git a/CHANGELOG.md b/CHANGELOG.md index b114a52..584776a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Dev: strengthened CLI login flow tests based on mutation testing + findings — redeeming an unknown token is asserted to throw + `TokenNotFoundException` specifically, both cache entries (token and + reverse username entry) are asserted removed after a token is used, + `encodeKey` asserts the exact namespaced encoding instead of only an + encode/decode roundtrip, and the CLI login URL is asserted to receive + the login token and route. No effect on the published package. - Dev: added a test for `ItkDevOpenIdConnectBundle::getContainerExtension()` asserting the custom extension is created and memoized (same instance on repeated calls), prompted by mutation testing findings. No effect on the diff --git a/tests/Command/UserLoginCommandTest.php b/tests/Command/UserLoginCommandTest.php index 0c339c0..53ee001 100644 --- a/tests/Command/UserLoginCommandTest.php +++ b/tests/Command/UserLoginCommandTest.php @@ -44,13 +44,36 @@ public function testExecuteSuccess(): void $this->stubUrlGenerator ->method('generate') - ->willReturn('https://app.com/login?loginToken=generated-token'); + ->willReturn('https://app.example.org/login?loginToken=generated-token'); $tester = new CommandTester($this->command); $result = $tester->execute(['username' => 'testuser']); $this->assertSame(Command::SUCCESS, $result); - $this->assertStringContainsString('https://app.com/login?loginToken=generated-token', $tester->getDisplay()); + $this->assertStringContainsString('https://app.example.org/login?loginToken=generated-token', $tester->getDisplay()); + } + + public function testExecutePassesTokenAndRouteToUrlGenerator(): void + { + $this->stubCliLoginHelper + ->method('createToken') + ->willReturn('generated-token'); + + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $urlGenerator->expects($this->once()) + ->method('generate') + ->with('cli_login_route', ['loginToken' => 'generated-token'], UrlGeneratorInterface::ABSOLUTE_URL) + ->willReturn('https://app.example.org/login?loginToken=generated-token'); + + $command = new UserLoginCommand( + $this->stubCliLoginHelper, + 'cli_login_route', + $urlGenerator, + $this->stubUserProvider + ); + + $tester = new CommandTester($command); + $this->assertSame(Command::SUCCESS, $tester->execute(['username' => 'testuser'])); } public function testExecuteUserNotFound(): void diff --git a/tests/Util/CliLoginHelperTest.php b/tests/Util/CliLoginHelperTest.php index 2b234ef..ad099c0 100644 --- a/tests/Util/CliLoginHelperTest.php +++ b/tests/Util/CliLoginHelperTest.php @@ -4,6 +4,7 @@ use ItkDev\OpenIdConnectBundle\Exception\CacheException; use ItkDev\OpenIdConnectBundle\Exception\OpenIdConnectBundleExceptionInterface; +use ItkDev\OpenIdConnectBundle\Exception\TokenNotFoundException; use ItkDev\OpenIdConnectBundle\Util\CliLoginHelper; use PHPUnit\Framework\TestCase; use Psr\Cache\CacheItemInterface; @@ -40,7 +41,10 @@ public function testDecodeKeyReturnsInputWhenNotValidBase64(): void public function testThrowExceptionIfTokenDoesNotExist(): void { - $this->expectException(OpenIdConnectBundleExceptionInterface::class); + // TokenNotFoundException (not just the marker interface) is part of + // the public contract: CliLoginTokenAuthenticator catches it + // specifically to distinguish "no such token" from cache failures. + $this->expectException(TokenNotFoundException::class); $cache = new ArrayAdapter(); @@ -79,6 +83,37 @@ public function testTokenIsRemovedAfterUse(): void $cliHelper->getUsername($token); } + public function testBothCacheEntriesAreRemovedAfterUse(): void + { + $cache = new ArrayAdapter(); + + $cliHelper = new CliLoginHelper($cache); + + $testUser = 'test_user'; + $token = $cliHelper->createToken($testUser); + + $this->assertEquals($testUser, $cliHelper->getUsername($token)); + + // The reverse entry (username => token) must be gone too; otherwise + // createToken() would hand out the already-redeemed token again. + $this->assertFalse($cache->hasItem($cliHelper->encodeKey($testUser))); + + $newToken = $cliHelper->createToken($testUser); + $this->assertNotSame($token, $newToken); + $this->assertEquals($testUser, $cliHelper->getUsername($newToken)); + } + + public function testEncodeKeyPrependsNamespace(): void + { + $cache = new ArrayAdapter(); + $cliHelper = new CliLoginHelper($cache); + + // Assert the exact encoding, not just an encode/decode roundtrip: + // the namespace prefix guards against cache key collisions with the + // consuming application, and a roundtrip is blind to losing it. + $this->assertSame(base64_encode('itk-dev-cli-logintest_user'), $cliHelper->encodeKey('test_user')); + } + public function testCreateTokenAndGetUsername(): void { $cache = new ArrayAdapter();