anonymous//tests/UploadMiddlewareTest.php$0   A
last analyzed

Complexity

Total Complexity 2

Size/Duplication

Total Lines 12
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
dl 0
loc 12
rs 10
c 7
b 0
f 0
wmc 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQLTests\Upload;
6
7
use GraphQL\Error\DebugFlag;
8
use GraphQL\Error\InvariantViolation;
9
use GraphQL\Executor\ExecutionResult;
10
use GraphQL\Server\RequestError;
11
use GraphQL\Server\StandardServer;
12
use GraphQL\Type\Definition\ObjectType;
13
use GraphQL\Type\Definition\Type;
14
use GraphQL\Type\Schema;
15
use GraphQL\Upload\UploadMiddleware;
16
use GraphQL\Upload\UploadType;
17
use GraphQL\Upload\Utility;
18
use GraphQLTests\Upload\Psr7\PsrUploadedFileStub;
19
use Laminas\Diactoros\Response;
20
use Laminas\Diactoros\ServerRequest;
21
use Laminas\Diactoros\UploadedFile;
22
use PHPUnit\Framework\TestCase;
23
use Psr\Http\Message\ResponseInterface;
24
use Psr\Http\Message\ServerRequestInterface;
25
use Psr\Http\Message\UploadedFileInterface;
26
use Psr\Http\Server\RequestHandlerInterface;
27
use stdClass;
28
29
final class UploadMiddlewareTest extends TestCase
30
{
31
    public function testParsesMultipartRequest(): void
32
    {
33
        $query = '{my query}';
34
        $variables = [
35
            'test' => 1,
36
            'test2' => 2,
37
            'uploads' => [
38
                0 => null,
39
                1 => null,
40
            ],
41
        ];
42
        $map = [
43
            1 => ['variables.uploads.0'],
44
            2 => ['variables.uploads.1'],
45
        ];
46
47
        $file1 = new PsrUploadedFileStub('image.jpg', 'image/jpeg');
48
        $file2 = new PsrUploadedFileStub('foo.txt', 'text/plain');
49
        $files = [
50
            1 => $file1,
51
            2 => $file2,
52
        ];
53
54
        $request = $this->createRequest($query, $variables, $map, $files, 'op');
55
        $processedRequest = $this->getProcessedRequest($request);
56
57
        $variables['uploads'] = [
58
            0 => $file1,
59
            1 => $file2,
60
        ];
61
62
        self::assertSame('application/json', $processedRequest->getHeader('content-type')[0], 'request should have been transformed as application/json');
63
        self::assertSame($variables, $processedRequest->getParsedBody()['variables'], 'uploaded files should have been injected into variables');
64
    }
65
66
    public function testEmptyRequestIsValid(): void
67
    {
68
        $request = $this->createRequest('{my query}', [], [], [], 'op');
69
        $processedRequest = $this->getProcessedRequest($request);
70
71
        self::assertSame('application/json', $processedRequest->getHeader('content-type')[0], 'request should have been transformed as application/json');
72
        self::assertSame([], $processedRequest->getParsedBody()['variables'], 'variables should still be empty');
73
    }
74
75
    public function testNonMultipartRequestAreNotTouched(): void
76
    {
77
        $request = new ServerRequest();
78
        $processedRequest = $this->getProcessedRequest($request);
79
80
        self::assertSame($request, $processedRequest, 'request should have been transformed as application/json');
81
    }
82
83
    public function testEmptyRequestShouldThrow(): void
84
    {
85
        $request = new ServerRequest();
86
        $request = $request
87
            ->withHeader('content-type', ['multipart/form-data'])
88
            ->withParsedBody([]);
89
90
        $this->expectException(InvariantViolation::class);
91
        $this->expectExceptionMessage('PSR-7 request is expected to provide parsed body for "multipart/form-data" requests but got empty array');
92
        $this->getProcessedRequest($request);
93
    }
94
95
    public function testNullRequestShouldThrow(): void
96
    {
97
        $request = new ServerRequest();
98
        $request = $request
99
            ->withHeader('content-type', ['multipart/form-data'])
100
            ->withParsedBody(null);
101
102
        $this->expectException(InvariantViolation::class);
103
        $this->expectExceptionMessage('PSR-7 request is expected to provide parsed body for "multipart/form-data" requests but got null');
104
        $this->getProcessedRequest($request);
105
    }
106
107
    public function testInvalidRequestShouldThrow(): void
108
    {
109
        $request = new ServerRequest();
110
        $request = $request
111
            ->withHeader('content-type', ['multipart/form-data'])
112
            ->withParsedBody(new stdClass());
113
114
        $this->expectException(RequestError::class);
115
        $this->expectExceptionMessage('GraphQL Server expects JSON object or array, but got {}');
116
        $this->getProcessedRequest($request);
117
    }
118
119
    public function testOtherContentTypeShouldNotBeTouched(): void
120
    {
121
        $request = new ServerRequest();
122
        $request = $request
123
            ->withHeader('content-type', ['application/json'])
124
            ->withParsedBody(new stdClass());
125
126
        $processedRequest = $this->getProcessedRequest($request);
127
        self::assertSame($request, $processedRequest);
128
    }
129
130
    public function testRequestWithoutMapShouldThrow(): void
131
    {
132
        $request = $this->createRequest('{my query}', [], [], [], 'op');
133
134
        // Remove the map
135
        $body = $request->getParsedBody();
136
        unset($body['map']);
137
        $request = $request->withParsedBody($body);
138
139
        $this->expectException(RequestError::class);
140
        $this->expectExceptionMessage('The request must define a `map`');
141
        $this->getProcessedRequest($request);
142
    }
143
144
    public function testRequestWithMapThatIsNotArrayShouldThrow(): void
145
    {
146
        $request = $this->createRequest('{my query}', [], [], [], 'op');
147
148
        // Replace map with json that is valid but no array
149
        $body = $request->getParsedBody();
150
        $body['map'] = json_encode('foo');
151
        $request = $request->withParsedBody($body);
152
153
        $this->expectException(RequestError::class);
154
        $this->expectExceptionMessage('The `map` key must be a JSON encoded array');
155
        $this->getProcessedRequest($request);
156
    }
157
158
    public function testRequestWithMapThatIsNotValidJsonShouldThrow(): void
159
    {
160
        $request = $this->createRequest('{my query}', [], [], [], 'op');
161
162
        // Replace map with invalid json
163
        $body = $request->getParsedBody();
164
        $body['map'] = 'this is not json';
165
        $request = $request->withParsedBody($body);
166
167
        $this->expectException(RequestError::class);
168
        $this->expectExceptionMessage('The `map` key must be a JSON encoded array');
169
        $this->getProcessedRequest($request);
170
    }
171
172
    public function testRequestWithTooBigPostSizeShouldReturnHttpError413WithMessage(): void
173
    {
174
        $postMaxSize = Utility::getPostMaxSize();
175
        $contentLength = (string) ($postMaxSize * 2);
176
        $request = $this->createRequest('{my query}', [], [], [], 'op', ['CONTENT_LENGTH' => $contentLength]);
177
178
        $contentLength = Utility::toMebibyte($contentLength);
179
        $postMaxSize = Utility::toMebibyte($postMaxSize);
180
181
        $response = $this->getResponse($request);
182
        self::assertSame(413, $response->getStatusCode());
183
        self::assertSame('{"message":"The server `post_max_size` is configured to accept ' . $postMaxSize . ', but received ' . $contentLength . '"}', $response->getBody()->getContents());
184
    }
185
186
    public function testRequestWithSmallerPostSizeShouldBeOK(): void
187
    {
188
        $postMaxSize = Utility::getPostMaxSize();
189
        $contentLength = (string) $postMaxSize;
190
        $request = $this->createRequest('{my query}', [], [], [], 'op', ['CONTENT_LENGTH' => $contentLength]);
191
192
        $processedRequest = $this->getProcessedRequest($request);
193
194
        self::assertSame('application/json', $processedRequest->getHeader('content-type')[0], 'request should have been transformed as application/json');
195
        self::assertSame([], $processedRequest->getParsedBody()['variables'], 'variables should still be empty');
196
197
    }
198
199
    public function testMissingUploadedFileShouldThrow(): void
200
    {
201
        $query = '{my query}';
202
        $variables = [
203
            'uploads' => [
204
                0 => null,
205
                1 => null,
206
            ],
207
        ];
208
        $map = [
209
            1 => ['variables.uploads.0'],
210
            2 => ['variables.uploads.1'],
211
        ];
212
213
        $file1 = new PsrUploadedFileStub('image.jpg', 'image/jpeg');
214
        $files = [
215
            1 => $file1,
216
        ];
217
218
        $request = $this->createRequest($query, $variables, $map, $files, 'op');
219
220
        $this->expectException(RequestError::class);
221
        $this->expectExceptionMessage('GraphQL query declared an upload in `variables.uploads.1`, but no corresponding file were actually uploaded');
222
        $this->getProcessedRequest($request);
223
    }
224
225
    public function testCanUploadFileWithStandardServer(): void
226
    {
227
        $query = 'mutation TestUpload($text: String, $file: Upload) {
228
    testUpload(text: $text, file: $file)
229
}';
230
        $variables = [
231
            'text' => 'foo bar',
232
            'file' => null,
233
        ];
234
        $map = [
235
            1 => ['variables.file'],
236
        ];
237
        $files = [
238
            1 => new PsrUploadedFileStub('image.jpg', 'image/jpeg'),
239
        ];
240
241
        $request = $this->createRequest($query, $variables, $map, $files, 'TestUpload');
242
243
        $processedRequest = $this->getProcessedRequest($request);
244
245
        $server = $this->createServer();
246
247
        /** @var ExecutionResult $response */
248
        $response = $server->executePsrRequest($processedRequest);
249
250
        $expected = ['testUpload' => 'Uploaded file was image.jpg (image/jpeg) with description: foo bar'];
251
        self::assertSame($expected, $response->data);
252
    }
253
254
    private function getProcessedRequest(ServerRequestInterface $request): ServerRequestInterface
255
    {
256
        $result = $this->process($request);
257
        self::assertInstanceOf(ServerRequestInterface::class, $result);
258
259
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type Psr\Http\Message\ResponseInterface which is incompatible with the type-hinted return Psr\Http\Message\ServerRequestInterface.
Loading history...
260
    }
261
262
    private function getResponse(ServerRequestInterface $request): ResponseInterface
263
    {
264
        $result = $this->process($request);
265
        self::assertInstanceOf(ResponseInterface::class, $result);
266
267
        return $result;
268
    }
269
270
    private function process(ServerRequestInterface $request): ResponseInterface|ServerRequestInterface
271
    {
272
        $defaultResponse = new Response\EmptyResponse();
273
        $handler = new class($defaultResponse) implements RequestHandlerInterface {
274
            public ServerRequestInterface $processedRequest;
275
276
            public function __construct(
277
                private readonly ResponseInterface $response,
278
            ) {}
279
280
            public function handle(ServerRequestInterface $request): ResponseInterface
281
            {
282
                $this->processedRequest = $request;
283
284
                return $this->response;
285
            }
286
        };
287
288
        $middleware = new UploadMiddleware();
289
290
        $actualResponse = $middleware->process($request, $handler);
291
292
        return $actualResponse === $defaultResponse ? $handler->processedRequest : $actualResponse;
0 ignored issues
show
introduced by
The condition $actualResponse === $defaultResponse is always false.
Loading history...
293
    }
294
295
    /**
296
     * @param mixed[] $variables
297
     * @param string[][] $map
298
     * @param UploadedFile[] $files
299
     * @param mixed[] $serverParams
300
     */
301
    private function createRequest(
302
        string $query,
303
        array $variables,
304
        array $map,
305
        array $files,
306
        string $operation,
307
        array $serverParams = [],
308
    ): ServerRequestInterface {
309
        $request = new ServerRequest($serverParams);
310
        $request = $request
311
            ->withMethod('POST')
312
            ->withHeader('content-type', ['multipart/form-data; boundary=----WebKitFormBoundarySl4GaqVa1r8GtAbn'])
313
            ->withParsedBody([
314
                'operations' => json_encode([
315
                    'query' => $query,
316
                    'variables' => $variables,
317
                    'operationName' => $operation,
318
                ]),
319
                'map' => json_encode($map),
320
            ])
321
            ->withUploadedFiles($files);
322
323
        return $request;
324
    }
325
326
    private function createServer(): StandardServer
327
    {
328
        $all = DebugFlag::INCLUDE_DEBUG_MESSAGE
329
            | DebugFlag::INCLUDE_TRACE
330
            | DebugFlag::RETHROW_INTERNAL_EXCEPTIONS
331
            | DebugFlag::RETHROW_UNSAFE_EXCEPTIONS;
332
333
        return new StandardServer([
334
            'debugFlag' => $all,
335
            'schema' => new Schema([
336
                'query' => new ObjectType([
337
                    'name' => 'Query',
338
                    'fields' => [],
339
                ]),
340
                'mutation' => new ObjectType([
341
                    'name' => 'Mutation',
342
                    'fields' => [
343
                        'testUpload' => [
344
                            'type' => Type::string(),
345
                            'args' => [
346
                                'text' => Type::string(),
347
                                'file' => new UploadType(),
348
                            ],
349
                            'resolve' => function ($root, array $args): string {
350
                                /** @var UploadedFileInterface $file */
351
                                $file = $args['file'];
352
                                $this->assertInstanceOf(UploadedFileInterface::class, $file);
353
354
                                // Do something more interesting with the file
355
                                // $file->moveTo('some/folder/in/my/project');
356
357
                                return 'Uploaded file was ' . $file->getClientFilename() . ' (' . $file->getClientMediaType() . ') with description: ' . $args['text'];
358
                            },
359
                        ],
360
                    ],
361
                ]),
362
            ]),
363
        ]);
364
    }
365
}
366