|
21 | 21 | use ApiPlatform\State\Provider\DeserializeProvider; |
22 | 22 | use ApiPlatform\State\ProviderInterface; |
23 | 23 | use ApiPlatform\State\SerializerContextBuilderInterface; |
| 24 | +use ApiPlatform\Validator\Exception\ValidationException; |
24 | 25 | use PHPUnit\Framework\Attributes\DataProvider; |
25 | 26 | use PHPUnit\Framework\Attributes\IgnoreDeprecations; |
26 | 27 | use PHPUnit\Framework\TestCase; |
27 | 28 | use Symfony\Component\HttpFoundation\Request; |
28 | 29 | use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; |
| 30 | +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; |
| 31 | +use Symfony\Component\Serializer\Exception\PartialDenormalizationException; |
29 | 32 | use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; |
30 | 33 | use Symfony\Component\Serializer\SerializerInterface; |
| 34 | +use Symfony\Component\Validator\Constraints\Type; |
31 | 35 |
|
32 | 36 | class DeserializeProviderTest extends TestCase |
33 | 37 | { |
@@ -203,6 +207,135 @@ public function testDeserializeSetsObjectToPopulateWhenContextIsTrue(): void |
203 | 207 | $provider->provide($operation, ['id' => 1], ['request' => $request]); |
204 | 208 | } |
205 | 209 |
|
| 210 | + #[IgnoreDeprecations] |
| 211 | + public function testDeserializeUsesExceptionMessageWhenCanUseMessageForUser(): void |
| 212 | + { |
| 213 | + $operation = new Post(deserialize: true, class: \stdClass::class); |
| 214 | + $decorated = $this->createStub(ProviderInterface::class); |
| 215 | + $decorated->method('provide')->willReturn(null); |
| 216 | + |
| 217 | + $exception = NotNormalizableValueException::createForUnexpectedDataType( |
| 218 | + 'The data must belong to a backed enumeration of type Suit.', |
| 219 | + 'invalid', |
| 220 | + ['string'], |
| 221 | + 'status', |
| 222 | + true, |
| 223 | + ); |
| 224 | + $partialException = new PartialDenormalizationException('Denormalization failed.', [$exception]); |
| 225 | + |
| 226 | + $serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); |
| 227 | + $serializerContextBuilder->method('createFromRequest')->willReturn([]); |
| 228 | + $serializer = $this->createMock(SerializerInterface::class); |
| 229 | + $serializer->method('deserialize')->willThrowException($partialException); |
| 230 | + |
| 231 | + $provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder); |
| 232 | + $request = new Request(content: '{"status":"invalid"}'); |
| 233 | + $request->headers->set('CONTENT_TYPE', 'application/json'); |
| 234 | + $request->attributes->set('input_format', 'json'); |
| 235 | + |
| 236 | + try { |
| 237 | + $provider->provide($operation, [], ['request' => $request]); |
| 238 | + $this->fail('Expected ValidationException'); |
| 239 | + } catch (ValidationException $e) { |
| 240 | + $violations = $e->getConstraintViolationList(); |
| 241 | + $this->assertCount(1, $violations); |
| 242 | + $this->assertSame('The data must belong to a backed enumeration of type Suit.', $violations[0]->getMessage()); |
| 243 | + $this->assertSame('The data must belong to a backed enumeration of type Suit.', $violations[0]->getMessageTemplate()); |
| 244 | + $this->assertSame('status', $violations[0]->getPropertyPath()); |
| 245 | + $this->assertSame((string) Type::INVALID_TYPE_ERROR, $violations[0]->getCode()); |
| 246 | + } |
| 247 | + } |
| 248 | + |
| 249 | + /** |
| 250 | + * Simulates Symfony 8.1 BackedEnumNormalizer behavior (symfony/serializer PR #62574): |
| 251 | + * when a value has the right type but is not a valid enum case, the exception |
| 252 | + * is created with expectedTypes=null and a user-friendly message listing valid values. |
| 253 | + */ |
| 254 | + #[IgnoreDeprecations] |
| 255 | + public function testDeserializeUsesExceptionMessageWhenExpectedTypesIsNull(): void |
| 256 | + { |
| 257 | + $operation = new Post(deserialize: true, class: \stdClass::class); |
| 258 | + $decorated = $this->createStub(ProviderInterface::class); |
| 259 | + $decorated->method('provide')->willReturn(null); |
| 260 | + |
| 261 | + $ctor = new \ReflectionMethod(NotNormalizableValueException::class, '__construct'); |
| 262 | + if ($ctor->getNumberOfParameters() <= 3) { |
| 263 | + $this->markTestSkipped('NotNormalizableValueException does not support extended constructor parameters.'); |
| 264 | + } |
| 265 | + |
| 266 | + $exception = new NotNormalizableValueException( |
| 267 | + "The data must be one of the following values: 'hearts', 'diamonds', 'clubs', 'spades'", |
| 268 | + 0, |
| 269 | + null, |
| 270 | + null, |
| 271 | + null, |
| 272 | + 'suit', |
| 273 | + true, |
| 274 | + ); |
| 275 | + $partialException = new PartialDenormalizationException('Denormalization failed.', [$exception]); |
| 276 | + |
| 277 | + $serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); |
| 278 | + $serializerContextBuilder->method('createFromRequest')->willReturn([]); |
| 279 | + $serializer = $this->createMock(SerializerInterface::class); |
| 280 | + $serializer->method('deserialize')->willThrowException($partialException); |
| 281 | + |
| 282 | + $provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder); |
| 283 | + $request = new Request(content: '{"suit":"invalid"}'); |
| 284 | + $request->headers->set('CONTENT_TYPE', 'application/json'); |
| 285 | + $request->attributes->set('input_format', 'json'); |
| 286 | + |
| 287 | + try { |
| 288 | + $provider->provide($operation, [], ['request' => $request]); |
| 289 | + $this->fail('Expected ValidationException'); |
| 290 | + } catch (ValidationException $e) { |
| 291 | + $violations = $e->getConstraintViolationList(); |
| 292 | + $this->assertCount(1, $violations); |
| 293 | + $this->assertSame("The data must be one of the following values: 'hearts', 'diamonds', 'clubs', 'spades'", $violations[0]->getMessage()); |
| 294 | + $this->assertSame("The data must be one of the following values: 'hearts', 'diamonds', 'clubs', 'spades'", $violations[0]->getMessageTemplate()); |
| 295 | + $this->assertSame('suit', $violations[0]->getPropertyPath()); |
| 296 | + $this->assertSame((string) Type::INVALID_TYPE_ERROR, $violations[0]->getCode()); |
| 297 | + } |
| 298 | + } |
| 299 | + |
| 300 | + #[IgnoreDeprecations] |
| 301 | + public function testDeserializeUsesTypeMessageWhenCannotUseMessageForUser(): void |
| 302 | + { |
| 303 | + $operation = new Post(deserialize: true, class: \stdClass::class); |
| 304 | + $decorated = $this->createStub(ProviderInterface::class); |
| 305 | + $decorated->method('provide')->willReturn(null); |
| 306 | + |
| 307 | + $exception = NotNormalizableValueException::createForUnexpectedDataType( |
| 308 | + 'Internal error detail', |
| 309 | + 42, |
| 310 | + ['string'], |
| 311 | + 'name', |
| 312 | + false, |
| 313 | + ); |
| 314 | + $partialException = new PartialDenormalizationException('Denormalization failed.', [$exception]); |
| 315 | + |
| 316 | + $serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); |
| 317 | + $serializerContextBuilder->method('createFromRequest')->willReturn([]); |
| 318 | + $serializer = $this->createMock(SerializerInterface::class); |
| 319 | + $serializer->method('deserialize')->willThrowException($partialException); |
| 320 | + |
| 321 | + $provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder); |
| 322 | + $request = new Request(content: '{"name":42}'); |
| 323 | + $request->headers->set('CONTENT_TYPE', 'application/json'); |
| 324 | + $request->attributes->set('input_format', 'json'); |
| 325 | + |
| 326 | + try { |
| 327 | + $provider->provide($operation, [], ['request' => $request]); |
| 328 | + $this->fail('Expected ValidationException'); |
| 329 | + } catch (ValidationException $e) { |
| 330 | + $violations = $e->getConstraintViolationList(); |
| 331 | + $this->assertCount(1, $violations); |
| 332 | + $this->assertStringContainsString('string', $violations[0]->getMessage()); |
| 333 | + $this->assertSame('name', $violations[0]->getPropertyPath()); |
| 334 | + $this->assertSame((string) Type::INVALID_TYPE_ERROR, $violations[0]->getCode()); |
| 335 | + $this->assertArrayNotHasKey('hint', $violations[0]->getParameters()); |
| 336 | + } |
| 337 | + } |
| 338 | + |
206 | 339 | public function testDeserializeDoesNotSetObjectToPopulateWhenContextIsFalse(): void |
207 | 340 | { |
208 | 341 | $objectToPopulate = new \stdClass(); |
|
0 commit comments