19
19
use Symfony \Component \HttpKernel \ControllerMetadata \ArgumentMetadata ;
20
20
use Symfony \Component \HttpKernel \Exception \HttpException ;
21
21
use Symfony \Component \Serializer \Encoder \JsonEncoder ;
22
+ use Symfony \Component \Serializer \Encoder \XmlEncoder ;
22
23
use Symfony \Component \Serializer \Exception \PartialDenormalizationException ;
23
24
use Symfony \Component \Serializer \Normalizer \ObjectNormalizer ;
24
25
use Symfony \Component \Serializer \Serializer ;
25
26
use Symfony \Component \Validator \ConstraintViolation ;
26
27
use Symfony \Component \Validator \ConstraintViolationList ;
28
+ use Symfony \Component \Validator \Constraints as Assert ;
27
29
use Symfony \Component \Validator \Exception \ValidationFailedException ;
28
30
use Symfony \Component \Validator \Validator \ValidatorInterface ;
31
+ use Symfony \Component \Validator \ValidatorBuilder ;
29
32
30
33
class RequestPayloadValueResolverTest extends TestCase
31
34
{
@@ -181,10 +184,197 @@ public function testRequestInputValidationPassed()
181
184
182
185
$ this ->assertEquals ($ payload , $ resolver ->resolve ($ request , $ argument )[0 ]);
183
186
}
187
+
188
+ /**
189
+ * @dataProvider provideMatchedFormatContext
190
+ */
191
+ public function testAcceptFormatPassed (mixed $ acceptFormat , string $ contentType , string $ content )
192
+ {
193
+ $ encoders = ['json ' => new JsonEncoder (), 'xml ' => new XmlEncoder ()];
194
+ $ serializer = new Serializer ([new ObjectNormalizer ()], $ encoders );
195
+ $ validator = (new ValidatorBuilder ())->getValidator ();
196
+ $ resolver = new RequestPayloadValueResolver ($ serializer , $ validator );
197
+
198
+ $ request = Request::create ('/ ' , 'POST ' , server: ['CONTENT_TYPE ' => $ contentType ], content: $ content );
199
+
200
+ $ argument = new ArgumentMetadata ('valid ' , RequestPayload::class, false , false , null , false , [
201
+ MapRequestPayload::class => new MapRequestPayload (acceptFormat: $ acceptFormat ),
202
+ ]);
203
+
204
+ $ resolved = $ resolver ->resolve ($ request , $ argument );
205
+
206
+ $ this ->assertCount (1 , $ resolved );
207
+ $ this ->assertEquals (new RequestPayload (50 ), $ resolved [0 ]);
208
+ }
209
+
210
+ public function provideMatchedFormatContext (): iterable
211
+ {
212
+ yield 'configure with json as string, sends json ' => [
213
+ 'acceptFormat ' => 'json ' ,
214
+ 'contentType ' => 'application/json ' ,
215
+ 'content ' => '{"price": 50} ' ,
216
+ ];
217
+
218
+ yield 'configure with json as array, sends json ' => [
219
+ 'acceptFormat ' => ['json ' ],
220
+ 'contentType ' => 'application/json ' ,
221
+ 'content ' => '{"price": 50} ' ,
222
+ ];
223
+
224
+ yield 'configure with xml as string, sends xml ' => [
225
+ 'acceptFormat ' => 'xml ' ,
226
+ 'contentType ' => 'application/xml ' ,
227
+ 'content ' => '<?xml version="1.0"?><request><price>50</price></request> ' ,
228
+ ];
229
+
230
+ yield 'configure with xml as array, sends xml ' => [
231
+ 'acceptFormat ' => ['xml ' ],
232
+ 'contentType ' => 'application/xml ' ,
233
+ 'content ' => '<?xml version="1.0"?><request><price>50</price></request> ' ,
234
+ ];
235
+
236
+ yield 'configure with json or xml, sends json ' => [
237
+ 'acceptFormat ' => ['json ' , 'xml ' ],
238
+ 'contentType ' => 'application/json ' ,
239
+ 'content ' => '{"price": 50} ' ,
240
+ ];
241
+
242
+ yield 'configure with json or xml, sends xml ' => [
243
+ 'acceptFormat ' => ['json ' , 'xml ' ],
244
+ 'contentType ' => 'application/xml ' ,
245
+ 'content ' => '<?xml version="1.0"?><request><price>50</price></request> ' ,
246
+ ];
247
+ }
248
+
249
+ /**
250
+ * @dataProvider provideMismatchedFormatContext
251
+ */
252
+ public function testAcceptFormatNotPassed (mixed $ acceptFormat , string $ contentType , string $ content , string $ expectedExceptionMessage )
253
+ {
254
+ $ serializer = new Serializer ([new ObjectNormalizer ()]);
255
+ $ validator = (new ValidatorBuilder ())->getValidator ();
256
+ $ resolver = new RequestPayloadValueResolver ($ serializer , $ validator );
257
+
258
+ $ request = Request::create ('/ ' , 'POST ' , server: ['CONTENT_TYPE ' => $ contentType ], content: $ content );
259
+
260
+ $ argument = new ArgumentMetadata ('valid ' , RequestPayload::class, false , false , null , false , [
261
+ MapRequestPayload::class => new MapRequestPayload (acceptFormat: $ acceptFormat ),
262
+ ]);
263
+
264
+ try {
265
+ $ resolver ->resolve ($ request , $ argument );
266
+
267
+ $ this ->fail (sprintf ('Expected "%s" to be thrown. ' , HttpException::class));
268
+ } catch (HttpException $ e ) {
269
+ $ this ->assertSame (415 , $ e ->getStatusCode ());
270
+ $ this ->assertSame ($ expectedExceptionMessage , $ e ->getMessage ());
271
+ }
272
+ }
273
+
274
+ public function provideMismatchedFormatContext (): iterable
275
+ {
276
+ yield 'configure with json as string, sends xml ' => [
277
+ 'acceptFormat ' => 'json ' ,
278
+ 'contentType ' => 'application/xml ' ,
279
+ 'content ' => '<?xml version="1.0"?><request><price>50</price></request> ' ,
280
+ 'expectedExceptionMessage ' => 'Unsupported format, expects "json", but "xml" given. ' ,
281
+ ];
282
+
283
+ yield 'configure with json as array, sends xml ' => [
284
+ 'acceptFormat ' => ['json ' ],
285
+ 'contentType ' => 'application/xml ' ,
286
+ 'content ' => '<?xml version="1.0"?><request><price>50</price></request> ' ,
287
+ 'expectedExceptionMessage ' => 'Unsupported format, expects "json", but "xml" given. ' ,
288
+ ];
289
+
290
+ yield 'configure with xml as string, sends json ' => [
291
+ 'acceptFormat ' => 'xml ' ,
292
+ 'contentType ' => 'application/json ' ,
293
+ 'content ' => '{"price": 50} ' ,
294
+ 'expectedExceptionMessage ' => 'Unsupported format, expects "xml", but "json" given. ' ,
295
+ ];
296
+
297
+ yield 'configure with xml as array, sends json ' => [
298
+ 'acceptFormat ' => ['xml ' ],
299
+ 'contentType ' => 'application/json ' ,
300
+ 'content ' => '{"price": 50} ' ,
301
+ 'expectedExceptionMessage ' => 'Unsupported format, expects "xml", but "json" given. ' ,
302
+ ];
303
+
304
+ yield 'configure with json or xml, sends jsonld ' => [
305
+ 'acceptFormat ' => ['json ' , 'xml ' ],
306
+ 'contentType ' => 'application/ld+json ' ,
307
+ 'content ' => '{"@context": "https://schema.org", "@type": "FakeType", "price": 50} ' ,
308
+ 'expectedExceptionMessage ' => 'Unsupported format, expects "json", "xml", but "jsonld" given. ' ,
309
+ ];
310
+ }
311
+
312
+ /**
313
+ * @dataProvider provideValidationGroupsOnManyTypes
314
+ */
315
+ public function testValidationGroupsPassed (mixed $ groups )
316
+ {
317
+ $ input = ['price ' => '50 ' , 'title ' => 'A long title, so the validation passes ' ];
318
+
319
+ $ payload = new RequestPayload (50 );
320
+ $ payload ->title = 'A long title, so the validation passes ' ;
321
+
322
+ $ serializer = new Serializer ([new ObjectNormalizer ()]);
323
+ $ validator = (new ValidatorBuilder ())->enableAnnotationMapping ()->getValidator ();
324
+ $ resolver = new RequestPayloadValueResolver ($ serializer , $ validator );
325
+
326
+ $ request = Request::create ('/ ' , 'POST ' , $ input );
327
+
328
+ $ argument = new ArgumentMetadata ('valid ' , RequestPayload::class, false , false , null , false , [
329
+ MapRequestPayload::class => new MapRequestPayload (validationGroups: $ groups ),
330
+ ]);
331
+
332
+ $ resolved = $ resolver ->resolve ($ request , $ argument );
333
+
334
+ $ this ->assertCount (1 , $ resolved );
335
+ $ this ->assertEquals ($ payload , $ resolved [0 ]);
336
+ }
337
+
338
+ /**
339
+ * @dataProvider provideValidationGroupsOnManyTypes
340
+ */
341
+ public function testValidationGroupsNotPassed (mixed $ groups )
342
+ {
343
+ $ input = ['price ' => '50 ' , 'title ' => 'Too short ' ];
344
+
345
+ $ serializer = new Serializer ([new ObjectNormalizer ()]);
346
+ $ validator = (new ValidatorBuilder ())->enableAnnotationMapping ()->getValidator ();
347
+ $ resolver = new RequestPayloadValueResolver ($ serializer , $ validator );
348
+
349
+ $ argument = new ArgumentMetadata ('valid ' , RequestPayload::class, false , false , null , false , [
350
+ MapRequestPayload::class => new MapRequestPayload (validationGroups: $ groups ),
351
+ ]);
352
+ $ request = Request::create ('/ ' , 'POST ' , $ input );
353
+
354
+ try {
355
+ $ resolver ->resolve ($ request , $ argument );
356
+ $ this ->fail (sprintf ('Expected "%s" to be thrown. ' , HttpException::class));
357
+ } catch (HttpException $ e ) {
358
+ $ validationFailedException = $ e ->getPrevious ();
359
+ $ this ->assertInstanceOf (ValidationFailedException::class, $ validationFailedException );
360
+ $ this ->assertSame ('title ' , $ validationFailedException ->getViolations ()[0 ]->getPropertyPath ());
361
+ $ this ->assertSame ('This value is too short. It should have 10 characters or more. ' , $ validationFailedException ->getViolations ()[0 ]->getMessage ());
362
+ }
363
+ }
364
+
365
+ public function provideValidationGroupsOnManyTypes (): iterable
366
+ {
367
+ yield 'validation group as string ' => ['strict ' ];
368
+
369
+ yield 'validation group as array ' => [['strict ' ]];
370
+
371
+ yield 'validation group as GroupSequence ' => [new Assert \GroupSequence (['strict ' ])];
372
+ }
184
373
}
185
374
186
375
class RequestPayload
187
376
{
377
+ #[Assert \Length(min: 10 , groups: ['strict ' ])]
188
378
public string $ title ;
189
379
190
380
public function __construct (public readonly float $ price )
0 commit comments