SignedQueryMiddlewareTest::dataProviderQuery()   B
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 188
Code Lines 138

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 138
c 5
b 0
f 0
dl 0
loc 188
rs 8
cc 1
nc 1
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace EcodevTests\Felix\Middleware;
6
7
use Cake\Chronos\Chronos;
8
use Ecodev\Felix\Middleware\SignedQueryMiddleware;
9
use Laminas\Diactoros\CallbackStream;
10
use Laminas\Diactoros\Response;
11
use Laminas\Diactoros\ServerRequest;
12
use PHPUnit\Framework\TestCase;
13
use Psr\Http\Message\ServerRequestInterface;
14
use Psr\Http\Server\RequestHandlerInterface;
15
16
class SignedQueryMiddlewareTest extends TestCase
17
{
18
    protected function setUp(): void
19
    {
20
        Chronos::setTestNow((new Chronos('2020-01-02T12:30', 'Europe/Zurich')));
21
    }
22
23
    protected function tearDown(): void
24
    {
25
        Chronos::setTestNow();
26
    }
27
28
    /**
29
     * @dataProvider dataProviderQuery
30
     */
31
    public function testRequiredSignedQuery(array $keys, string $body, ?array $parsedBody, string $signature, string $expectExceptionMessage = '', string $ip = ''): void
32
    {
33
        $this->process($keys, true, $ip, $body, $parsedBody, $signature, $expectExceptionMessage);
34
    }
35
36
    /**
37
     * @dataProvider dataProviderQuery
38
     */
39
    public function testNonRequiredSignedQuery(array $keys, string $body, ?array $parsedBody, string $signature): void
40
    {
41
        $this->process($keys, false, '', $body, $parsedBody, $signature, '');
42
    }
43
44
    public static function dataProviderQuery(): iterable
45
    {
46
        $key1 = 'my-secret-1';
47
        $key2 = 'my-secret-2';
48
49
        yield 'simple' => [
50
            [$key1],
51
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
52
            null,
53
            'v1.1577964600.a4d664cd3d9903e4fecf6f9f671ad953586a7faeb16e67c306fd9f29999dfdd7',
54
        ];
55
56
        yield 'simple but wrong key' => [
57
            [$key2],
58
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
59
            null,
60
            'v1.1577964600.a4d664cd3d9903e4fecf6f9f671ad953586a7faeb16e67c306fd9f29999dfdd7',
61
            'Invalid signed query',
62
        ];
63
64
        yield 'simple with all keys' => [
65
            [$key2, $key1],
66
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
67
            null,
68
            'v1.1577964600.a4d664cd3d9903e4fecf6f9f671ad953586a7faeb16e67c306fd9f29999dfdd7',
69
        ];
70
71
        yield 'simple but slightly in the past' => [
72
            [$key1],
73
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
74
            null,
75
            'v1.1577951100.7d3b639703584e3ea4c68b30a37b56bcf94d19ccdc11c7f05a737c4e7e663a6c',
76
        ];
77
78
        yield 'simple but too much in the past' => [
79
            [$key1],
80
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
81
            null,
82
            'v1.1577951099.' . str_repeat('a', 64),
83
            'Signed query is expired',
84
        ];
85
86
        yield 'simple but slightly in the future' => [
87
            [$key1],
88
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
89
            null,
90
            'v1.1577978100.b6fb50cd1aa3974ec9df0c320bf32ff58a28f6fc2040aa13e529a7ef57212e49',
91
        ];
92
93
        yield 'simple but too much in the future' => [
94
            [$key1],
95
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
96
            null,
97
            'v1.1577978101.' . str_repeat('a', 64),
98
            'Signed query is expired',
99
        ];
100
101
        yield 'batching' => [
102
            [$key1],
103
            '[{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }},{"operationName":"Configuration","variables":{"key":"announcement-active"},"query":"query Configuration($key: String!) { configuration(key: $key)}"}]',
104
            null,
105
            'v1.1577964600.566fafed794d956d662662b0df3d88e5c0a1e52e19111c08cc122f64a54bd8ec',
106
107
        ];
108
109
        yield 'file upload' => [
110
            [$key1],
111
            '',
112
            [
113
                'operations' => '{"operationName":"CreateImage","variables":{"input":{"file":null}},"query":"mutation CreateImage($input: ImageInput!) { createImage(input: $input) { id }}"}',
114
                'map' => '{"1":["variables.input.file"]}',
115
            ],
116
            'v1.1577964600.69dd1f396016e284afb221966ae5e61323a23222f2ad2a5086e4ba2354f99e58',
117
        ];
118
119
        yield 'file upload will ignore map and uploaded file to sign' => [
120
            [$key1],
121
            '',
122
            [
123
                'operations' => '{"operationName":"CreateImage","variables":{"input":{"file":null}},"query":"mutation CreateImage($input: ImageInput!) { createImage(input: $input) { id }}"}',
124
                'map' => 'different map',
125
                1 => 'fake file',
126
            ],
127
            'v1.1577964600.69dd1f396016e284afb221966ae5e61323a23222f2ad2a5086e4ba2354f99e58',
128
        ];
129
130
        yield 'no header' => [
131
            [$key1],
132
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
133
            null,
134
            '',
135
            'Missing `X-Signature` HTTP header in signed query',
136
        ];
137
138
        yield 'invalid header' => [
139
            [$key1],
140
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
141
            null,
142
            'foo',
143
            'Invalid `X-Signature` HTTP header in signed query',
144
        ];
145
146
        yield 'no graphql operations with invalid signature is rejected' => [
147
            [$key1],
148
            '',
149
            null,
150
            'v1.1577964600.' . str_repeat('a', 64),
151
            'Invalid signed query',
152
        ];
153
154
        yield 'no graphql operations with correct signature is OK (but will be rejected later by GraphQL own validation mechanism)' => [
155
            [$key1],
156
            '',
157
            null,
158
            'v1.1577964600.ff8a9f2bc8090207b824d88251ed8e9d39434607d86e0f0b2837c597d6642c26',
159
            '',
160
        ];
161
162
        yield 'no header, but allowed IPv4' => [
163
            [$key1],
164
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
165
            null,
166
            '',
167
            '',
168
            '1.2.3.4',
169
        ];
170
171
        yield 'simple but wrong key will still error even if IP is allowed, because we want to be able to test signature even when allowed' => [
172
            [$key2],
173
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
174
            null,
175
            'v1.1577964600.a4d664cd3d9903e4fecf6f9f671ad953586a7faeb16e67c306fd9f29999dfdd7',
176
            'Invalid signed query',
177
            '1.2.3.4',
178
        ];
179
180
        yield 'no header, even GoogleBot is rejected, because GoogleBot should not forge new requests but only (re)play existing ones' => [
181
            [$key1],
182
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
183
            null,
184
            '',
185
            'Missing `X-Signature` HTTP header in signed query',
186
            '66.249.70.134',
187
        ];
188
189
        yield 'too much in the past, but GoogleBot is allowed to replay old requests' => [
190
            [$key1],
191
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
192
            null,
193
            'v1.1577951099.20177a7face4e05a75c4b2e41bc97a8225f420f5b7bb1709dd5499821dba0807',
194
            '',
195
            '66.249.70.134',
196
        ];
197
198
        yield 'too much in the past and invalid signature, even GoogleBot is rejected, because GoogleBot should not modify queries and their signatures' => [
199
            [$key1],
200
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
201
            null,
202
            'v1.1577951099' . str_repeat('a', 64),
203
            'Invalid `X-Signature` HTTP header in signed query',
204
            '66.249.70.134',
205
        ];
206
207
        yield 'no header, BingBot is allowed, because BingBot does not seem to include our custom header in his request' => [
208
            [$key1],
209
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
210
            null,
211
            '',
212
            '',
213
            '40.77.188.165',
214
        ];
215
216
        yield 'too much in the past, even BingBot is rejected, because BingBot should not have any header at all' => [
217
            [$key1],
218
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
219
            null,
220
            'v1.1577951099.20177a7face4e05a75c4b2e41bc97a8225f420f5b7bb1709dd5499821dba0807',
221
            'Signed query is expired',
222
            '40.77.188.165',
223
        ];
224
225
        yield 'too much in the past and invalid signature, even BingBot is rejected, because BingBot should not have any header at all' => [
226
            [$key1],
227
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
228
            null,
229
            'v1.1577951099' . str_repeat('a', 64),
230
            'Invalid `X-Signature` HTTP header in signed query',
231
            '40.77.188.165',
232
        ];
233
    }
234
235
    public function testThrowIfNoKeys(): void
236
    {
237
        $this->expectExceptionMessage('Signed queries are required, but no keys are configured');
238
        $this->expectExceptionCode(0);
239
        new SignedQueryMiddleware([], []);
240
    }
241
242
    private function process(array $keys, bool $required, string $ip, string $body, ?array $parsedBody, string $signature, string $expectExceptionMessage): void
243
    {
244
        $request = new ServerRequest(['REMOTE_ADDR' => $ip]);
245
        $request = $request->withBody(new CallbackStream(fn () => $body))->withParsedBody($parsedBody);
246
247
        if ($signature) {
248
            $request = $request->withHeader('X-Signature', $signature);
249
        }
250
251
        $handler = $this->createMock(RequestHandlerInterface::class);
252
        $handler->expects($expectExceptionMessage ? self::never() : self::once())
253
            ->method('handle')
254
            ->willReturnCallback(function (ServerRequestInterface $incomingRequest) use ($body) {
255
                self::assertSame($body, $incomingRequest->getBody()->getContents(), 'the original body content is still available for next middlewares');
256
257
                return new Response();
258
            });
259
260
        $middleware = new SignedQueryMiddleware($keys, ['1.2.3.4', '2a01:198:603:0::/65'], $required);
261
262
        if ($expectExceptionMessage) {
263
            $this->expectExceptionMessage($expectExceptionMessage);
264
            $this->expectExceptionCode(403);
265
        }
266
267
        $middleware->process($request, $handler);
268
    }
269
}
270