Completed
Push — develop ( 098751...1bc9e0 )
by Alejandro
16s queued 12s
created

provideRequestsWithoutApiKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ShlinkioTest\Shlink\Rest\Middleware;
6
7
use Fig\Http\Message\RequestMethodInterface;
8
use Laminas\Diactoros\Response;
9
use Laminas\Diactoros\ServerRequest;
10
use Laminas\Diactoros\ServerRequestFactory;
11
use Mezzio\Router\Route;
12
use Mezzio\Router\RouteResult;
13
use PHPUnit\Framework\TestCase;
14
use Prophecy\Argument;
15
use Prophecy\PhpUnit\ProphecyTrait;
16
use Prophecy\Prophecy\ObjectProphecy;
17
use Psr\Http\Message\ServerRequestInterface;
18
use Psr\Http\Server\MiddlewareInterface;
19
use Psr\Http\Server\RequestHandlerInterface;
20
use Shlinkio\Shlink\Rest\Action\HealthAction;
21
use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
22
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
23
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
24
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
25
26
use function Laminas\Stratigility\middleware;
27
28
class AuthenticationMiddlewareTest extends TestCase
29
{
30
    use ProphecyTrait;
31
32
    private AuthenticationMiddleware $middleware;
33
    private ObjectProphecy $apiKeyService;
34
    private ObjectProphecy $handler;
35
36
    public function setUp(): void
37
    {
38
        $this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
39
        $this->middleware = new AuthenticationMiddleware($this->apiKeyService->reveal(), [HealthAction::class]);
40
        $this->handler = $this->prophesize(RequestHandlerInterface::class);
41
    }
42
43
    /**
44
     * @test
45
     * @dataProvider provideWhitelistedRequests
46
     */
47
    public function someWhiteListedSituationsFallbackToNextMiddleware(ServerRequestInterface $request): void
48
    {
49
        $handle = $this->handler->handle($request)->willReturn(new Response());
50
        $checkApiKey = $this->apiKeyService->check(Argument::any());
51
52
        $this->middleware->process($request, $this->handler->reveal());
53
54
        $handle->shouldHaveBeenCalledOnce();
55
        $checkApiKey->shouldNotHaveBeenCalled();
56
    }
57
58
    public function provideWhitelistedRequests(): iterable
59
    {
60
        $dummyMiddleware = $this->getDummyMiddleware();
61
62
        yield 'with no route result' => [new ServerRequest()];
63
        yield 'with failure route result' => [(new ServerRequest())->withAttribute(
64
            RouteResult::class,
65
            RouteResult::fromRouteFailure([RequestMethodInterface::METHOD_GET]),
66
        )];
67
        yield 'with whitelisted route' => [(new ServerRequest())->withAttribute(
68
            RouteResult::class,
69
            RouteResult::fromRoute(
70
                new Route('foo', $dummyMiddleware, Route::HTTP_METHOD_ANY, HealthAction::class),
71
            ),
72
        )];
73
        yield 'with OPTIONS method' => [(new ServerRequest())->withAttribute(
74
            RouteResult::class,
75
            RouteResult::fromRoute(new Route('bar', $dummyMiddleware), []),
76
        )->withMethod(RequestMethodInterface::METHOD_OPTIONS)];
77
    }
78
79
    /**
80
     * @test
81
     * @dataProvider provideRequestsWithoutApiKey
82
     */
83
    public function throwsExceptionWhenNoApiKeyIsProvided(ServerRequestInterface $request): void
84
    {
85
        $this->apiKeyService->check(Argument::any())->shouldNotBeCalled();
86
        $this->handler->handle($request)->shouldNotBeCalled();
87
        $this->expectException(MissingAuthenticationException::class);
88
        $this->expectExceptionMessage(
89
            'Expected one of the following authentication headers, ["X-Api-Key"], but none were provided',
90
        );
91
92
        $this->middleware->process($request, $this->handler->reveal());
93
    }
94
95
    public function provideRequestsWithoutApiKey(): iterable
96
    {
97
        $baseRequest = ServerRequestFactory::fromGlobals()->withAttribute(
98
            RouteResult::class,
99
            RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), []),
100
        );
101
102
        yield 'no api key' => [$baseRequest];
103
        yield 'empty api key' => [$baseRequest->withHeader('X-Api-Key', '')];
104
    }
105
106
    /** @test */
107
    public function throwsExceptionWhenProvidedApiKeyIsInvalid(): void
108
    {
109
        $apiKey = 'abc123';
110
        $request = ServerRequestFactory::fromGlobals()
111
            ->withAttribute(
112
                RouteResult::class,
113
                RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), []),
114
            )
115
            ->withHeader('X-Api-Key', $apiKey);
116
117
        $this->apiKeyService->check($apiKey)->willReturn(false)->shouldBeCalledOnce();
118
        $this->handler->handle($request)->shouldNotBeCalled();
119
        $this->expectException(VerifyAuthenticationException::class);
120
        $this->expectExceptionMessage('Provided API key does not exist or is invalid');
121
122
        $this->middleware->process($request, $this->handler->reveal());
123
    }
124
125
    /** @test */
126
    public function validApiKeyFallsBackToNextMiddleware(): void
127
    {
128
        $apiKey = 'abc123';
129
        $request = ServerRequestFactory::fromGlobals()
130
            ->withAttribute(
131
                RouteResult::class,
132
                RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), []),
133
            )
134
            ->withHeader('X-Api-Key', $apiKey);
135
136
        $handle = $this->handler->handle($request)->willReturn(new Response());
137
        $checkApiKey = $this->apiKeyService->check($apiKey)->willReturn(true);
138
139
        $this->middleware->process($request, $this->handler->reveal());
140
141
        $handle->shouldHaveBeenCalledOnce();
142
        $checkApiKey->shouldHaveBeenCalledOnce();
143
    }
144
145
    private function getDummyMiddleware(): MiddlewareInterface
146
    {
147
        return middleware(fn () => new Response\EmptyResponse());
148
    }
149
}
150