apiKeyService = $this->prophesize(ApiKeyServiceInterface::class); $this->middleware = new AuthenticationMiddleware( $this->apiKeyService->reveal(), [HealthAction::class], ['with_query_api_key'], ); $this->handler = $this->prophesize(RequestHandlerInterface::class); } /** * @test * @dataProvider provideRequestsWithoutAuth */ public function someSituationsFallbackToNextMiddleware(ServerRequestInterface $request): void { $handle = $this->handler->handle($request)->willReturn(new Response()); $checkApiKey = $this->apiKeyService->check(Argument::any()); $this->middleware->process($request, $this->handler->reveal()); $handle->shouldHaveBeenCalledOnce(); $checkApiKey->shouldNotHaveBeenCalled(); } public function provideRequestsWithoutAuth(): iterable { $dummyMiddleware = $this->getDummyMiddleware(); yield 'no route result' => [new ServerRequest()]; yield 'failure route result' => [(new ServerRequest())->withAttribute( RouteResult::class, RouteResult::fromRouteFailure([RequestMethodInterface::METHOD_GET]), )]; yield 'route without API key required' => [(new ServerRequest())->withAttribute( RouteResult::class, RouteResult::fromRoute( new Route('foo', $dummyMiddleware, Route::HTTP_METHOD_ANY, HealthAction::class), ), )]; yield 'OPTIONS method' => [(new ServerRequest())->withAttribute( RouteResult::class, RouteResult::fromRoute(new Route('bar', $dummyMiddleware), []), )->withMethod(RequestMethodInterface::METHOD_OPTIONS)]; } /** * @test * @dataProvider provideRequestsWithoutApiKey */ public function throwsExceptionWhenNoApiKeyIsProvided( ServerRequestInterface $request, string $expectedMessage, ): void { $this->apiKeyService->check(Argument::any())->shouldNotBeCalled(); $this->handler->handle($request)->shouldNotBeCalled(); $this->expectException(MissingAuthenticationException::class); $this->expectExceptionMessage($expectedMessage); $this->middleware->process($request, $this->handler->reveal()); } public function provideRequestsWithoutApiKey(): iterable { $baseRequest = fn (string $routeName) => ServerRequestFactory::fromGlobals()->withAttribute( RouteResult::class, RouteResult::fromRoute(new Route($routeName, $this->getDummyMiddleware()), []), ); $apiKeyMessage = 'Expected one of the following authentication headers, ["X-Api-Key"], but none were provided'; $queryMessage = 'Expected authentication to be provided in "apiKey" query param'; yield 'no api key in header' => [$baseRequest('bar'), $apiKeyMessage]; yield 'empty api key in header' => [$baseRequest('bar')->withHeader('X-Api-Key', ''), $apiKeyMessage]; yield 'no api key in query' => [$baseRequest('with_query_api_key'), $queryMessage]; yield 'empty api key in query' => [ $baseRequest('with_query_api_key')->withQueryParams(['apiKey' => '']), $queryMessage, ]; } /** @test */ public function throwsExceptionWhenProvidedApiKeyIsInvalid(): void { $apiKey = 'abc123'; $request = ServerRequestFactory::fromGlobals() ->withAttribute( RouteResult::class, RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), []), ) ->withHeader('X-Api-Key', $apiKey); $this->apiKeyService->check($apiKey)->willReturn(new ApiKeyCheckResult())->shouldBeCalledOnce(); $this->handler->handle($request)->shouldNotBeCalled(); $this->expectException(VerifyAuthenticationException::class); $this->expectExceptionMessage('Provided API key does not exist or is invalid'); $this->middleware->process($request, $this->handler->reveal()); } /** @test */ public function validApiKeyFallsBackToNextMiddleware(): void { $apiKey = ApiKey::create(); $key = $apiKey->toString(); $request = ServerRequestFactory::fromGlobals() ->withAttribute( RouteResult::class, RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), []), ) ->withHeader('X-Api-Key', $key); $handle = $this->handler->handle($request->withAttribute(ApiKey::class, $apiKey))->willReturn(new Response()); $checkApiKey = $this->apiKeyService->check($key)->willReturn(new ApiKeyCheckResult($apiKey)); $this->middleware->process($request, $this->handler->reveal()); $handle->shouldHaveBeenCalledOnce(); $checkApiKey->shouldHaveBeenCalledOnce(); } private function getDummyMiddleware(): MiddlewareInterface { return middleware(fn () => new Response\EmptyResponse()); } }