Completed
Push — master ( 038254...115ca0 )
by Alejandro
06:27
created

errorIsReturnedWhenNoValidAuthIsProvided()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 15
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 23
cc 1
rs 9.7666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ShlinkioTest\Shlink\Rest\Middleware;
6
7
use Exception;
8
use Fig\Http\Message\RequestMethodInterface;
9
use PHPUnit\Framework\TestCase;
10
use Prophecy\Argument;
11
use Prophecy\Prophecy\ObjectProphecy;
12
use Psr\Container\ContainerExceptionInterface;
13
use Psr\Http\Message\ResponseInterface;
14
use Psr\Http\Message\ServerRequestInterface;
15
use Psr\Http\Server\MiddlewareInterface;
16
use Psr\Http\Server\RequestHandlerInterface;
17
use Psr\Log\LoggerInterface;
18
use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
19
use Shlinkio\Shlink\Rest\Authentication\Plugin\AuthenticationPluginInterface;
20
use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPlugin;
21
use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPluginInterface;
22
use Shlinkio\Shlink\Rest\Exception\NoAuthenticationException;
23
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
24
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
25
use Shlinkio\Shlink\Rest\Util\RestUtils;
26
use Throwable;
27
use Zend\Diactoros\Response;
28
use Zend\Diactoros\ServerRequest;
29
use Zend\Expressive\Router\Route;
30
use Zend\Expressive\Router\RouteResult;
31
32
use function implode;
33
use function sprintf;
34
use function Zend\Stratigility\middleware;
35
36
class AuthenticationMiddlewareTest extends TestCase
37
{
38
    /** @var AuthenticationMiddleware */
39
    private $middleware;
40
    /** @var ObjectProphecy */
41
    private $requestToPlugin;
42
    /** @var ObjectProphecy */
43
    private $logger;
44
45
    public function setUp(): void
46
    {
47
        $this->requestToPlugin = $this->prophesize(RequestToHttpAuthPluginInterface::class);
48
        $this->logger = $this->prophesize(LoggerInterface::class);
49
50
        $this->middleware = new AuthenticationMiddleware(
51
            $this->requestToPlugin->reveal(),
52
            [AuthenticateAction::class],
53
            $this->logger->reveal()
54
        );
55
    }
56
57
    /**
58
     * @test
59
     * @dataProvider provideWhitelistedRequests
60
     */
61
    public function someWhiteListedSituationsFallbackToNextMiddleware(ServerRequestInterface $request): void
62
    {
63
        $handler = $this->prophesize(RequestHandlerInterface::class);
64
        $handle = $handler->handle($request)->willReturn(new Response());
65
        $fromRequest = $this->requestToPlugin->fromRequest(Argument::any())->willReturn(
66
            $this->prophesize(AuthenticationPluginInterface::class)->reveal()
67
        );
68
69
        $this->middleware->process($request, $handler->reveal());
70
71
        $handle->shouldHaveBeenCalledOnce();
72
        $fromRequest->shouldNotHaveBeenCalled();
73
    }
74
75
    public function provideWhitelistedRequests(): iterable
76
    {
77
        $dummyMiddleware = $this->getDummyMiddleware();
78
79
        yield 'with no route result' => [new ServerRequest()];
80
        yield 'with failure route result' => [(new ServerRequest())->withAttribute(
81
            RouteResult::class,
82
            RouteResult::fromRouteFailure([RequestMethodInterface::METHOD_GET])
83
        )];
84
        yield 'with whitelisted route' => [(new ServerRequest())->withAttribute(
85
            RouteResult::class,
86
            RouteResult::fromRoute(
87
                new Route('foo', $dummyMiddleware, Route::HTTP_METHOD_ANY, AuthenticateAction::class)
0 ignored issues
show
Bug introduced by
Zend\Expressive\Router\Route::HTTP_METHOD_ANY of type null is incompatible with the type array expected by parameter $methods of Zend\Expressive\Router\Route::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

87
                new Route('foo', $dummyMiddleware, /** @scrutinizer ignore-type */ Route::HTTP_METHOD_ANY, AuthenticateAction::class)
Loading history...
88
            )
89
        )];
90
        yield 'with OPTIONS method' => [(new ServerRequest())->withAttribute(
91
            RouteResult::class,
92
            RouteResult::fromRoute(new Route('bar', $dummyMiddleware), [])
93
        )->withMethod(RequestMethodInterface::METHOD_OPTIONS)];
94
    }
95
96
    /**
97
     * @test
98
     * @dataProvider provideExceptions
99
     */
100
    public function errorIsReturnedWhenNoValidAuthIsProvided(Throwable $e): void
101
    {
102
        $request = (new ServerRequest())->withAttribute(
103
            RouteResult::class,
104
            RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), [])
105
        );
106
        $fromRequest = $this->requestToPlugin->fromRequest(Argument::any())->willThrow($e);
107
        $logWarning = $this->logger->warning('Invalid or no authentication provided. {e}', ['e' => $e])->will(
108
            function () {
109
            }
110
        );
111
112
        /** @var Response\JsonResponse $response */
113
        $response = $this->middleware->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
114
        $payload = $response->getPayload();
115
116
        $this->assertEquals(RestUtils::INVALID_AUTHORIZATION_ERROR, $payload['error']);
117
        $this->assertEquals(sprintf(
118
            'Expected one of the following authentication headers, but none were provided, ["%s"]',
119
            implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS)
120
        ), $payload['message']);
121
        $fromRequest->shouldHaveBeenCalledOnce();
122
        $logWarning->shouldHaveBeenCalledOnce();
123
    }
124
125
    public function provideExceptions(): iterable
126
    {
127
        $containerException = new class extends Exception implements ContainerExceptionInterface {
128
        };
129
130
        yield 'container exception' => [$containerException];
131
        yield 'authentication exception' => [NoAuthenticationException::fromExpectedTypes([])];
132
    }
133
134
    /** @test */
135
    public function errorIsReturnedWhenVerificationFails(): void
136
    {
137
        $request = (new ServerRequest())->withAttribute(
138
            RouteResult::class,
139
            RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), [])
140
        );
141
        $e = VerifyAuthenticationException::withError('the_error', 'the_message');
142
        $plugin = $this->prophesize(AuthenticationPluginInterface::class);
143
144
        $verify = $plugin->verify($request)->willThrow($e);
145
        $fromRequest = $this->requestToPlugin->fromRequest(Argument::any())->willReturn($plugin->reveal());
146
        $logWarning = $this->logger->warning('Authentication verification failed. {e}', ['e' => $e])->will(
147
            function () {
148
            }
149
        );
150
151
        /** @var Response\JsonResponse $response */
152
        $response = $this->middleware->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
153
        $payload = $response->getPayload();
154
155
        $this->assertEquals('the_error', $payload['error']);
156
        $this->assertEquals('the_message', $payload['message']);
157
        $verify->shouldHaveBeenCalledOnce();
158
        $fromRequest->shouldHaveBeenCalledOnce();
159
        $logWarning->shouldHaveBeenCalledOnce();
160
    }
161
162
    /** @test */
163
    public function updatedResponseIsReturnedWhenVerificationPasses(): void
164
    {
165
        $newResponse = new Response();
166
        $request = (new ServerRequest())->withAttribute(
167
            RouteResult::class,
168
            RouteResult::fromRoute(new Route('bar', $this->getDummyMiddleware()), [])
169
        );
170
        $plugin = $this->prophesize(AuthenticationPluginInterface::class);
171
172
        $verify = $plugin->verify($request)->will(function () {
173
        });
174
        $update = $plugin->update($request, Argument::type(ResponseInterface::class))->willReturn($newResponse);
175
        $fromRequest = $this->requestToPlugin->fromRequest(Argument::any())->willReturn($plugin->reveal());
176
177
        $handler = $this->prophesize(RequestHandlerInterface::class);
178
        $handle = $handler->handle($request)->willReturn(new Response());
179
        $response = $this->middleware->process($request, $handler->reveal());
180
181
        $this->assertSame($response, $newResponse);
182
        $verify->shouldHaveBeenCalledOnce();
183
        $update->shouldHaveBeenCalledOnce();
184
        $handle->shouldHaveBeenCalledOnce();
185
        $fromRequest->shouldHaveBeenCalledOnce();
186
    }
187
188
    private function getDummyMiddleware(): MiddlewareInterface
189
    {
190
        return middleware(function () {
191
            return new Response\EmptyResponse();
192
        });
193
    }
194
}
195