Passed
Push — master ( b414ce...c17b3d )
by Adrien
12:38
created

php$0 ➔ testEmptyRequestIsValid()   A

Complexity

Conditions 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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 testCanUploadFileWithStandardServer(): void
205
    {
206
        $query = 'mutation TestUpload($text: String, $file: Upload) {
207
    testUpload(text: $text, file: $file)
208
}';
209
        $variables = [
210
            'text' => 'foo bar',
211
            'file' => null,
212
        ];
213
        $map = [
214
            1 => ['variables.file'],
215
        ];
216
        $files = [
217
            1 => new PsrUploadedFileStub('image.jpg', 'image/jpeg'),
218
        ];
219
220
        $request = $this->createRequest($query, $variables, $map, $files, 'TestUpload');
221
222
        $processedRequest = $this->middleware->processRequest($request);
223
224
        $server = $this->createServer();
225
226
        /** @var ExecutionResult $response */
227
        $response = $server->executePsrRequest($processedRequest);
228
229
        $expected = ['testUpload' => 'Uploaded file was image.jpg (image/jpeg) with description: foo bar'];
230
        self::assertSame($expected, $response->data);
231
    }
232
233
    /**
234
     * @param mixed[] $variables
235
     * @param string[][] $map
236
     * @param UploadedFile[] $files
237
     */
238
    private function createRequest(string $query, array $variables, array $map, array $files, string $operation): ServerRequestInterface
239
    {
240
        $request = new ServerRequest();
241
        $request = $request
242
            ->withMethod('POST')
243
            ->withHeader('content-type', ['multipart/form-data; boundary=----WebKitFormBoundarySl4GaqVa1r8GtAbn'])
244
            ->withParsedBody([
245
                'operations' => json_encode([
246
                    'query' => $query,
247
                    'variables' => $variables,
248
                    'operationName' => $operation,
249
                ]),
250
                'map' => json_encode($map),
251
            ])
252
            ->withUploadedFiles($files);
253
254
        return $request;
255
    }
256
257
    private function createServer(): StandardServer
258
    {
259
        $all = DebugFlag::INCLUDE_DEBUG_MESSAGE
260
            | DebugFlag::INCLUDE_TRACE
261
            | DebugFlag::RETHROW_INTERNAL_EXCEPTIONS
262
            | DebugFlag::RETHROW_UNSAFE_EXCEPTIONS;
263
264
        return new StandardServer([
265
            'debugFlag' => $all,
266
            'schema' => new Schema([
267
                'query' => new ObjectType([
268
                    'name' => 'Query',
269
                ]),
270
                'mutation' => new ObjectType([
271
                    'name' => 'Mutation',
272
                    'fields' => [
273
                        'testUpload' => [
274
                            'type' => Type::string(),
275
                            'args' => [
276
                                'text' => Type::string(),
277
                                'file' => new UploadType(),
278
                            ],
279
                            'resolve' => function ($root, array $args): string {
280
                                /** @var UploadedFileInterface $file */
281
                                $file = $args['file'];
282
                                $this->assertInstanceOf(UploadedFileInterface::class, $file);
283
284
                                // Do something more interesting with the file
285
                                // $file->moveTo('some/folder/in/my/project');
286
287
                                return 'Uploaded file was ' . $file->getClientFilename() . ' (' . $file->getClientMediaType() . ') with description: ' . $args['text'];
288
                            },
289
                        ],
290
                    ],
291
                ]),
292
            ]),
293
        ]);
294
    }
295
}
296