Passed
Push — master ( 6859a9...73a630 )
by Adrien
11:45
created

php$0 ➔ testMissingUploadedFileShouldThrow()   A

Complexity

Conditions 1

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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