From d659df7efbef4964950d479322ff1b86fc437df4 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 25 Sep 2025 10:40:42 +0200 Subject: [PATCH] fix(symfony): openapi property path for validation fixes #7408 --- .../State/ParameterValidatorProviderTest.php | 173 ++++++++++++++++++ .../State/ParameterValidatorProvider.php | 8 +- 2 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Tests/Validator/State/ParameterValidatorProviderTest.php diff --git a/src/Symfony/Tests/Validator/State/ParameterValidatorProviderTest.php b/src/Symfony/Tests/Validator/State/ParameterValidatorProviderTest.php new file mode 100644 index 0000000000..a8b3810ab6 --- /dev/null +++ b/src/Symfony/Tests/Validator/State/ParameterValidatorProviderTest.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Tests\Validator\State; + +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Parameters; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter as ModelParameter; +use ApiPlatform\State\ParameterNotFound; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Symfony\Validator\State\ParameterValidatorProvider; +use ApiPlatform\Validator\Exception\ValidationException; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\Validator\ValidatorInterface; + +final class ParameterValidatorProviderTest extends TestCase +{ + public function testProvideWithoutRequest(): void + { + $validator = $this->createMock(ValidatorInterface::class); + $decorated = $this->createMock(ProviderInterface::class); + $decorated->expects($this->once())->method('provide')->willReturn(new \stdClass()); + + $provider = new ParameterValidatorProvider($validator, $decorated); + $result = $provider->provide(new Get(), [], []); + + $this->assertInstanceOf(\stdClass::class, $result); + } + + public function testProvideWithValidationDisabled(): void + { + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->never())->method('validate'); + $decorated = $this->createMock(ProviderInterface::class); + $decorated->expects($this->once())->method('provide')->willReturn(new \stdClass()); + + $operation = (new Get())->withQueryParameterValidationEnabled(false); + $request = new Request(); + $request->attributes->set('_api_operation', $operation); + + $provider = new ParameterValidatorProvider($validator, $decorated); + $result = $provider->provide($operation, [], ['request' => $request]); + + $this->assertInstanceOf(\stdClass::class, $result); + } + + public function testProvideWithNoConstraints(): void + { + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->never())->method('validate'); + $decorated = $this->createMock(ProviderInterface::class); + $decorated->expects($this->once())->method('provide')->willReturn(new \stdClass()); + + $operation = new Get(parameters: new Parameters([ + 'foo' => new QueryParameter(key: 'foo'), + ])); + $request = new Request(); + $request->attributes->set('_api_operation', $operation); + + $provider = new ParameterValidatorProvider($validator, $decorated); + $result = $provider->provide($operation, [], ['request' => $request]); + + $this->assertInstanceOf(\stdClass::class, $result); + } + + public function testProvideWithValidParameters(): void + { + $constraint = new NotBlank(); + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->once())->method('validate')->willReturn(new ConstraintViolationList()); + $decorated = $this->createMock(ProviderInterface::class); + $decorated->expects($this->once())->method('provide')->willReturn(new \stdClass()); + + $operation = new Get(parameters: new Parameters([ + 'foo' => (new QueryParameter(key: 'foo'))->withConstraints([$constraint])->setValue('bar'), + ])); + $request = new Request(); + $request->attributes->set('_api_operation', $operation); + + $provider = new ParameterValidatorProvider($validator, $decorated); + $result = $provider->provide($operation, [], ['request' => $request]); + + $this->assertInstanceOf(\stdClass::class, $result); + } + + public function testProvideWithInvalidParameters(): void + { + $this->expectException(ValidationException::class); + + $constraint = new NotBlank(); + $violationList = new ConstraintViolationList(); + $violationList->add($this->createMock(\Symfony\Component\Validator\ConstraintViolationInterface::class)); + + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->once())->method('validate')->willReturn($violationList); + $decorated = $this->createMock(ProviderInterface::class); + $decorated->expects($this->never())->method('provide'); + + $operation = new Get(parameters: new Parameters([ + 'foo' => (new QueryParameter(key: 'foo'))->withConstraints([$constraint])->setValue(new ParameterNotFound()), + ])); + $request = new Request(); + $request->attributes->set('_api_operation', $operation); + + $provider = new ParameterValidatorProvider($validator, $decorated); + $provider->provide($operation, [], ['request' => $request]); + } + + public function testProvideWithUriVariables(): void + { + $constraint = new NotBlank(); + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->once())->method('validate')->willReturn(new ConstraintViolationList()); + $decorated = $this->createMock(ProviderInterface::class); + $decorated->expects($this->once())->method('provide')->willReturn(new \stdClass()); + + $operation = new Get(uriVariables: [ + 'id' => (new Link())->withConstraints([$constraint])->setValue('1'), + ]); + $request = new Request(); + $request->attributes->set('_api_operation', $operation); + + $provider = new ParameterValidatorProvider($validator, $decorated); + $result = $provider->provide($operation, ['id' => 1], ['request' => $request]); + + $this->assertInstanceOf(\stdClass::class, $result); + } + + public function testGetPropertyWithDeepObject(): void + { + $constraint = new NotBlank(); + $violationList = new ConstraintViolationList(); + $violation = $this->createMock(\Symfony\Component\Validator\ConstraintViolationInterface::class); + $violation->method('getPropertyPath')->willReturn('[bar]'); + $violationList->add($violation); + + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->once())->method('validate')->willReturn($violationList); + $decorated = $this->createMock(ProviderInterface::class); + $decorated->expects($this->never())->method('provide'); + + $parameter = (new QueryParameter(key: 'foo'))->withConstraints([$constraint])->setValue(new ParameterNotFound()); + $parameter = $parameter->withOpenApi(new ModelParameter(name: 'foo', in: 'query', style: 'deepObject')); + + $operation = new Get(parameters: new Parameters([ + 'foo' => $parameter, + ])); + $request = new Request(); + $request->attributes->set('_api_operation', $operation); + + $provider = new ParameterValidatorProvider($validator, $decorated); + try { + $provider->provide($operation, [], ['request' => $request]); + } catch (ValidationException $e) { + $this->assertEquals('foo[bar]', $e->getConstraintViolationList()->get(0)->getPropertyPath()); + } + } +} diff --git a/src/Symfony/Validator/State/ParameterValidatorProvider.php b/src/Symfony/Validator/State/ParameterValidatorProvider.php index 4cf9a300fb..6e86c43812 100644 --- a/src/Symfony/Validator/State/ParameterValidatorProvider.php +++ b/src/Symfony/Validator/State/ParameterValidatorProvider.php @@ -118,7 +118,13 @@ private function getProperty(Parameter $parameter, ConstraintViolationInterface $openApi = null; } - if ('deepObject' === $openApi?->getStyle() && $p = $violation->getPropertyPath()) { + if (\is_array($openApi)) { + foreach ($openApi as $oa) { + if ('deepObject' === $oa->getStyle() && ($oa->getName() === $key || str_starts_with($oa->getName(), $key.'['))) { + return $key.$violation->getPropertyPath(); + } + } + } elseif ('deepObject' === $openApi?->getStyle() && $p = $violation->getPropertyPath()) { return $key.$p; }