SignedQueryMiddlewareTest::dataProviderQuery()   B
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 217
Code Lines 157

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 157
c 6
b 0
f 0
dl 0
loc 217
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(
32
        array $keys,
33
        string $body,
34
        ?array $parsedBody,
35
        string $signature,
36
        string|int|null $keyName = null,
37
        string $expectExceptionMessage = '',
38
        string $ip = '',
39
    ): void {
40
        $this->process($keys, true, $ip, $body, $parsedBody, $signature, $keyName, $expectExceptionMessage);
41
    }
42
43
    /**
44
     * @dataProvider dataProviderQuery
45
     */
46
    public function testNonRequiredSignedQuery(
47
        array $keys,
48
        string $body,
49
        ?array $parsedBody,
50
        string $signature,
51
    ): void {
52
        $this->process($keys, false, '', $body, $parsedBody, $signature, null, '');
53
    }
54
55
    public static function dataProviderQuery(): iterable
56
    {
57
        $key1 = 'my-secret-1';
58
        $key2 = 'my-secret-2';
59
60
        yield 'simple' => [
61
            [$key1],
62
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
63
            null,
64
            'v1.1577964600.a4d664cd3d9903e4fecf6f9f671ad953586a7faeb16e67c306fd9f29999dfdd7',
65
            0,
66
        ];
67
68
        yield 'simple with key name' => [
69
            ['my custom key name' => $key1],
70
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
71
            null,
72
            'v1.1577964600.a4d664cd3d9903e4fecf6f9f671ad953586a7faeb16e67c306fd9f29999dfdd7',
73
            'my custom key name',
74
        ];
75
76
        yield 'simple but wrong key' => [
77
            [$key2],
78
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
79
            null,
80
            'v1.1577964600.a4d664cd3d9903e4fecf6f9f671ad953586a7faeb16e67c306fd9f29999dfdd7',
81
            null,
82
            'Invalid signed query',
83
        ];
84
85
        yield 'simple with all keys' => [
86
            [$key2, $key1],
87
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
88
            null,
89
            'v1.1577964600.a4d664cd3d9903e4fecf6f9f671ad953586a7faeb16e67c306fd9f29999dfdd7',
90
            1,
91
        ];
92
93
        yield 'simple but slightly in the past' => [
94
            [$key1],
95
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
96
            null,
97
            'v1.1577951100.7d3b639703584e3ea4c68b30a37b56bcf94d19ccdc11c7f05a737c4e7e663a6c',
98
            0,
99
        ];
100
101
        yield 'simple but too much in the past' => [
102
            [$key1],
103
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
104
            null,
105
            'v1.1577951099.' . str_repeat('a', 64),
106
            null,
107
            'Signed query is expired',
108
        ];
109
110
        yield 'simple but slightly in the future' => [
111
            [$key1],
112
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
113
            null,
114
            'v1.1577978100.b6fb50cd1aa3974ec9df0c320bf32ff58a28f6fc2040aa13e529a7ef57212e49',
115
            0,
116
        ];
117
118
        yield 'simple but too much in the future' => [
119
            [$key1],
120
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
121
            null,
122
            'v1.1577978101.' . str_repeat('a', 64),
123
            null,
124
            'Signed query is expired',
125
        ];
126
127
        yield 'batching' => [
128
            [$key1],
129
            '[{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }},{"operationName":"Configuration","variables":{"key":"announcement-active"},"query":"query Configuration($key: String!) { configuration(key: $key)}"}]',
130
            null,
131
            'v1.1577964600.566fafed794d956d662662b0df3d88e5c0a1e52e19111c08cc122f64a54bd8ec',
132
            0,
133
        ];
134
135
        yield 'file upload' => [
136
            [$key1],
137
            '',
138
            [
139
                'operations' => '{"operationName":"CreateImage","variables":{"input":{"file":null}},"query":"mutation CreateImage($input: ImageInput!) { createImage(input: $input) { id }}"}',
140
                'map' => '{"1":["variables.input.file"]}',
141
            ],
142
            'v1.1577964600.69dd1f396016e284afb221966ae5e61323a23222f2ad2a5086e4ba2354f99e58',
143
            0,
144
        ];
145
146
        yield 'file upload will ignore map and uploaded file to sign' => [
147
            [$key1],
148
            '',
149
            [
150
                'operations' => '{"operationName":"CreateImage","variables":{"input":{"file":null}},"query":"mutation CreateImage($input: ImageInput!) { createImage(input: $input) { id }}"}',
151
                'map' => 'different map',
152
                1 => 'fake file',
153
            ],
154
            'v1.1577964600.69dd1f396016e284afb221966ae5e61323a23222f2ad2a5086e4ba2354f99e58',
155
            0,
156
        ];
157
158
        yield 'no header' => [
159
            [$key1],
160
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
161
            null,
162
            '',
163
            null,
164
            'Missing `X-Signature` HTTP header in signed query',
165
        ];
166
167
        yield 'invalid header' => [
168
            [$key1],
169
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
170
            null,
171
            'foo',
172
            null,
173
            'Invalid `X-Signature` HTTP header in signed query',
174
        ];
175
176
        yield 'no graphql operations with invalid signature is rejected' => [
177
            [$key1],
178
            '',
179
            null,
180
            'v1.1577964600.' . str_repeat('a', 64),
181
            null,
182
            'Invalid signed query',
183
        ];
184
185
        yield 'no graphql operations with correct signature is OK (but will be rejected later by GraphQL own validation mechanism)' => [
186
            [$key1],
187
            '',
188
            null,
189
            'v1.1577964600.ff8a9f2bc8090207b824d88251ed8e9d39434607d86e0f0b2837c597d6642c26',
190
            0,
191
            '',
192
        ];
193
194
        yield 'no header, but allowed IPv4' => [
195
            [$key1],
196
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
197
            null,
198
            '',
199
            'no-attribute-at-all',
200
            '',
201
            '1.2.3.4',
202
        ];
203
204
        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' => [
205
            [$key2],
206
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
207
            null,
208
            'v1.1577964600.a4d664cd3d9903e4fecf6f9f671ad953586a7faeb16e67c306fd9f29999dfdd7',
209
            null,
210
            'Invalid signed query',
211
            '1.2.3.4',
212
        ];
213
214
        yield 'no header, even GoogleBot is rejected, because GoogleBot should not forge new requests but only (re)play existing ones' => [
215
            [$key1],
216
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
217
            null,
218
            '',
219
            null,
220
            'Missing `X-Signature` HTTP header in signed query',
221
            '66.249.70.134',
222
        ];
223
224
        yield 'too much in the past, but GoogleBot is allowed to replay old requests' => [
225
            [$key1],
226
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
227
            null,
228
            'v1.1577951099.20177a7face4e05a75c4b2e41bc97a8225f420f5b7bb1709dd5499821dba0807',
229
            0,
230
            '',
231
            '66.249.70.134',
232
        ];
233
234
        yield 'too much in the past and invalid signature, even GoogleBot is rejected, because GoogleBot should not modify queries and their signatures' => [
235
            [$key1],
236
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
237
            null,
238
            'v1.1577951099' . str_repeat('a', 64),
239
            null,
240
            'Invalid `X-Signature` HTTP header in signed query',
241
            '66.249.70.134',
242
        ];
243
244
        yield 'no header, BingBot is allowed, because BingBot does not seem to include our custom header in his request' => [
245
            [$key1],
246
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
247
            null,
248
            '',
249
            'no-attribute-at-all',
250
            '',
251
            '40.77.188.165',
252
        ];
253
254
        yield 'too much in the past, even BingBot is rejected, because BingBot should not have any header at all' => [
255
            [$key1],
256
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
257
            null,
258
            'v1.1577951099.20177a7face4e05a75c4b2e41bc97a8225f420f5b7bb1709dd5499821dba0807',
259
            null,
260
            'Signed query is expired',
261
            '40.77.188.165',
262
        ];
263
264
        yield 'too much in the past and invalid signature, even BingBot is rejected, because BingBot should not have any header at all' => [
265
            [$key1],
266
            '{"operationName":"CurrentUser","variables":{},"query":"query CurrentUser { viewer { id }}',
267
            null,
268
            'v1.1577951099' . str_repeat('a', 64),
269
            null,
270
            'Invalid `X-Signature` HTTP header in signed query',
271
            '40.77.188.165',
272
        ];
273
    }
274
275
    public function testThrowIfNoKeys(): void
276
    {
277
        $this->expectExceptionMessage('Signed queries are required, but no keys are configured');
278
        $this->expectExceptionCode(0);
279
        new SignedQueryMiddleware([], []);
280
    }
281
282
    private function process(
283
        array $keys,
284
        bool $required,
285
        string $ip,
286
        string $body,
287
        ?array $parsedBody,
288
        string $signature,
289
        string|int|null $keyName,
290
        string $expectExceptionMessage,
291
    ): void {
292
        $request = new ServerRequest(['REMOTE_ADDR' => $ip]);
293
        $request = $request->withBody(new CallbackStream(fn () => $body))->withParsedBody($parsedBody);
294
295
        if ($signature) {
296
            $request = $request->withHeader('X-Signature', $signature);
297
        }
298
299
        $handler = $this->createMock(RequestHandlerInterface::class);
300
        $handler->expects($expectExceptionMessage ? self::never() : self::once())
301
            ->method('handle')
302
            ->willReturnCallback(function (ServerRequestInterface $incomingRequest) use ($required, $body, $keyName) {
303
                self::assertSame($body, $incomingRequest->getBody()->getContents(), 'the original body content is still available for next middlewares');
304
                self::assertSame($required ? $keyName : 'no-attribute-at-all', $incomingRequest->getAttribute(SignedQueryMiddleware::class, 'no-attribute-at-all'), 'the name of the key used');
305
306
                return new Response();
307
            });
308
309
        $middleware = new SignedQueryMiddleware($keys, ['1.2.3.4', '2a01:198:603:0::/65'], $required);
310
311
        if ($expectExceptionMessage) {
312
            $this->expectExceptionMessage($expectExceptionMessage);
313
            $this->expectExceptionCode(403);
314
        }
315
316
        $middleware->process($request, $handler);
317
    }
318
}
319