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