From 650557163d55122b2f05c2532f5499cd84aed258 Mon Sep 17 00:00:00 2001 From: Andrew Stowell Date: Wed, 10 Jun 2026 17:11:56 -0400 Subject: [PATCH] WIP: AI Test cleanup, fixing all the tests, next up RejectListener. Added tests for GetTotpTrait, also fixed it to work as intended. --- src/Trait/GetTotpTrait.php | 28 +++++++-- tests/Unit/Listener/RejectListenerTest.php | 40 +++++++++---- tests/Unit/Trait/GetTotpTraitTest.php | 70 ++++++++++++++++++++++ 3 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 tests/Unit/Trait/GetTotpTraitTest.php diff --git a/src/Trait/GetTotpTrait.php b/src/Trait/GetTotpTrait.php index 87e4d27..bd865cf 100644 --- a/src/Trait/GetTotpTrait.php +++ b/src/Trait/GetTotpTrait.php @@ -4,12 +4,16 @@ declare(strict_types=1); namespace App\Trait; use App\ConfigBag; +use OTPHP\Exception\InvalidProvisioningUriException; use OTPHP\Factory; use OTPHP\TOTPInterface; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Contracts\Service\Attribute\Required; trait GetTotpTrait { + use HasLoggerTrait; + protected readonly ConfigBag $config; #[Required] @@ -18,12 +22,24 @@ trait GetTotpTrait { } protected function getTotp(): TOTPInterface { - $otp = Factory::loadFromProvisioningUri( - $this->config->totpUri(), $this->config->clock() - ); - if ($otp instanceof TOTPInterface) { - return $otp; + try { + $otp = Factory::loadFromProvisioningUri( + $this->config->totpUri(), $this->config->clock() + ); + if ($otp instanceof TOTPInterface) { + return $otp; + } + } catch (InvalidProvisioningUriException $uriException) { + /* always present a pretty error publicly */ + $this->logger->emergency( + 'System is unusable, invalid TOTP_URI configured: {exception}', + ['exception' => $uriException] + ); + throw new HttpException( + Response::HTTP_INTERNAL_SERVER_ERROR, + 'Internal Server Error', + $uriException, + ); } - throw new HttpException(500, 'Internal Server Exception'); } } diff --git a/tests/Unit/Listener/RejectListenerTest.php b/tests/Unit/Listener/RejectListenerTest.php index 23ac810..013428a 100644 --- a/tests/Unit/Listener/RejectListenerTest.php +++ b/tests/Unit/Listener/RejectListenerTest.php @@ -5,8 +5,10 @@ namespace App\Tests\Unit\Listener; use App\ConfigBag; use App\Listener\RejectListener; +use App\Utilities; use OTPHP\TOTP; use PHPUnit\Framework\TestCase; +use Psr\Cache\CacheItemPoolInterface; use Psr\Clock\ClockInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; @@ -19,13 +21,14 @@ use Twig\Environment; final class RejectListenerTest extends TestCase { private function createEvent(Request $request): RequestEvent { - $kernel = $this->createMock(HttpKernelInterface::class); + $kernel = $this->createStub(HttpKernelInterface::class); return new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); } private function createConfigBag(bool $teapot = false): ConfigBag { - $clock = $this->createMock(ClockInterface::class); - $utilities = $this->createMock(\App\Utilities::class); + $clock = $this->createStub(ClockInterface::class); + $cache = $this->createStub(CacheItemPoolInterface::class); + $utilities = new Utilities($clock, $cache); $totp = TOTP::generate($clock); $totp->setLabel('Test'); @@ -44,21 +47,26 @@ final class RejectListenerTest extends TestCase { private function createLimiter(int $remainingTokens): LimiterInterface { $limit = $this->createMock(RateLimit::class); - $limit->method('getRemainingTokens')->willReturn($remainingTokens); + $limit->expects($this->once())->method('getRemainingTokens') + ->willReturn($remainingTokens); $limiter = $this->createMock(LimiterInterface::class); - $limiter->method('consume')->with(0)->willReturn($limit); + $limiter->expects($this->once())->method('consume')->with(0) + ->willReturn($limit); return $limiter; } + /** when not blocked, expect no response */ public function testOnKernelRequestWhenNotBlocked(): void { $twig = $this->createMock(Environment::class); + $twig->expects($this->never())->method('render'); $config = $this->createConfigBag(); $rateLimiter = $this->createMock(RateLimiterFactoryInterface::class); - $rateLimiter->method('create')->willReturn($this->createLimiter(5)); + $rateLimiter->expects($this->once())->method('create') + ->willReturn($this->createLimiter(5)); $listener = new RejectListener($config, $twig, $rateLimiter); - $listener->setLogger($this->createMock(LoggerInterface::class)); + $listener->setLogger($this->createStub(LoggerInterface::class)); $request = Request::create('https://example.com/'); $event = $this->createEvent($request); @@ -67,16 +75,19 @@ final class RejectListenerTest extends TestCase { self::assertNull($event->getResponse()); } + /** when blocked, expect too-many response */ public function testOnKernelRequestWhenBlocked(): void { $twig = $this->createMock(Environment::class); - $twig->method('render')->with('error.html.twig')->willReturn('blocked'); + $twig->expects($this->once())->method('render') + ->with('error.html.twig')->willReturn('blocked'); $config = $this->createConfigBag(); $rateLimiter = $this->createMock(RateLimiterFactoryInterface::class); - $rateLimiter->method('create')->willReturn($this->createLimiter(0)); + $rateLimiter->expects($this->once())->method('create') + ->willReturn($this->createLimiter(0)); $listener = new RejectListener($config, $twig, $rateLimiter); - $listener->setLogger($this->createMock(LoggerInterface::class)); + $listener->setLogger($this->createStub(LoggerInterface::class)); $request = Request::create('https://example.com/'); $event = $this->createEvent($request); @@ -88,16 +99,19 @@ final class RejectListenerTest extends TestCase { self::assertSame('blocked', $response->getContent()); } + /** when blocked with teapot expect teapot response */ public function testOnKernelRequestWhenBlockedWithTeapot(): void { $twig = $this->createMock(Environment::class); - $twig->method('render')->with('error.html.twig')->willReturn('teapot'); + $twig->expects($this->once())->method('render') + ->with('error.html.twig')->willReturn('teapot'); $config = $this->createConfigBag(true); $rateLimiter = $this->createMock(RateLimiterFactoryInterface::class); - $rateLimiter->method('create')->willReturn($this->createLimiter(0)); + $rateLimiter->expects($this->once())->method('create') + ->willReturn($this->createLimiter(0)); $listener = new RejectListener($config, $twig, $rateLimiter); - $listener->setLogger($this->createMock(LoggerInterface::class)); + $listener->setLogger($this->createStub(LoggerInterface::class)); $request = Request::create('https://example.com/'); $event = $this->createEvent($request); diff --git a/tests/Unit/Trait/GetTotpTraitTest.php b/tests/Unit/Trait/GetTotpTraitTest.php new file mode 100644 index 0000000..32f5051 --- /dev/null +++ b/tests/Unit/Trait/GetTotpTraitTest.php @@ -0,0 +1,70 @@ +createStub(ClockInterface::class); + $cache = $this->createStub(CacheItemPoolInterface::class); + $utilities = new Utilities($clock, $cache); + + return new ConfigBag( + $utilities, + $clock, + 3600, + $totpUri, + 1800, + true, + 'Error!', + 'Teapot!', + 'Too Many!' + ); + } + + /** will create the totp instance from the config */ + public function testGetTotpFromConfig(): void { + $clock = $this->createStub(ClockInterface::class); + $totp = TOTP::generate($clock); + $totp->setLabel('test'); + + $config = $this->createConfigBag($totp->getProvisioningUri()); + $this->setConfig($config); + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->never())->method($this->anything()); + $this->setLogger($logger); + + $this->assertInstanceOf(TOTPInterface::class, $this->getTotp()); + $this->assertSame($totp->getProvisioningUri(), $this->getTotp()->getProvisioningUri()); + /* uri should match, but it will not be the same object instances */ + $this->assertNotSame($totp, $this->getTotp()); + } + + /** will emit pretty error, if config is invalid */ + public function testGetTotpInvalidConfig(): void { + $config = $this->createConfigBag('otpauth://invalid-totp-uri'); + $this->setConfig($config); + + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->once())->method('emergency'); + $this->setLogger($logger); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Internal Server Error'); + + $this->getTotp(); + } +}