|
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