SignedQueryMiddlewareTest::process()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 35
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 16
c 4
b 0
f 0
dl 0
loc 35
rs 9.4222
cc 5
nc 4
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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