Skip to content

Commit 54bdce5

Browse files
committed
fix(symfony): openapi property path for validation
fixes #7408
1 parent 65e137f commit 54bdce5

File tree

2 files changed

+182
-1
lines changed

2 files changed

+182
-1
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Symfony\Tests\Validator\State;
15+
16+
use ApiPlatform\Metadata\Get;
17+
use ApiPlatform\Metadata\Link;
18+
use ApiPlatform\Metadata\Parameter;
19+
use ApiPlatform\Metadata\Parameters;
20+
use ApiPlatform\Metadata\QueryParameter;
21+
use ApiPlatform\OpenApi\Model\Parameter as ModelParameter;
22+
use ApiPlatform\State\ParameterNotFound;
23+
use ApiPlatform\State\ProviderInterface;
24+
use ApiPlatform\Symfony\Validator\State\ParameterValidatorProvider;
25+
use ApiPlatform\Validator\Exception\ValidationException;
26+
use PHPUnit\Framework\TestCase;
27+
use Symfony\Component\HttpFoundation\Request;
28+
// use Symfony\Component\Validator\Constraint\NotBlank;
29+
use Symfony\Component\Validator\ConstraintViolationList;
30+
use Symfony\Component\Validator\Constraints\NotBlank;
31+
use Symfony\Component\Validator\Validator\ValidatorInterface;
32+
33+
final class ParameterValidatorProviderTest extends TestCase
34+
{
35+
public function testProvideWithoutRequest(): void
36+
{
37+
$validator = $this->createMock(ValidatorInterface::class);
38+
$decorated = $this->createMock(ProviderInterface::class);
39+
$decorated->expects($this->once())->method('provide')->willReturn(new \stdClass());
40+
41+
$provider = new ParameterValidatorProvider($validator, $decorated);
42+
$result = $provider->provide(new Get(), [], []);
43+
44+
$this->assertInstanceOf(\stdClass::class, $result);
45+
}
46+
47+
public function testProvideWithValidationDisabled(): void
48+
{
49+
$validator = $this->createMock(ValidatorInterface::class);
50+
$validator->expects($this->never())->method('validate');
51+
$decorated = $this->createMock(ProviderInterface::class);
52+
$decorated->expects($this->once())->method('provide')->willReturn(new \stdClass());
53+
54+
$operation = (new Get())->withQueryParameterValidationEnabled(false);
55+
$request = new Request();
56+
$request->attributes->set('_api_operation', $operation);
57+
58+
$provider = new ParameterValidatorProvider($validator, $decorated);
59+
$result = $provider->provide($operation, [], ['request' => $request]);
60+
61+
$this->assertInstanceOf(\stdClass::class, $result);
62+
}
63+
64+
public function testProvideWithNoConstraints(): void
65+
{
66+
$validator = $this->createMock(ValidatorInterface::class);
67+
$validator->expects($this->never())->method('validate');
68+
$decorated = $this->createMock(ProviderInterface::class);
69+
$decorated->expects($this->once())->method('provide')->willReturn(new \stdClass());
70+
71+
$operation = new Get(parameters: new Parameters([
72+
'foo' => new QueryParameter(key: 'foo'),
73+
]));
74+
$request = new Request();
75+
$request->attributes->set('_api_operation', $operation);
76+
77+
$provider = new ParameterValidatorProvider($validator, $decorated);
78+
$result = $provider->provide($operation, [], ['request' => $request]);
79+
80+
$this->assertInstanceOf(\stdClass::class, $result);
81+
}
82+
83+
public function testProvideWithValidParameters(): void
84+
{
85+
$constraint = new NotBlank();
86+
$validator = $this->createMock(ValidatorInterface::class);
87+
$validator->expects($this->once())->method('validate')->willReturn(new ConstraintViolationList());
88+
$decorated = $this->createMock(ProviderInterface::class);
89+
$decorated->expects($this->once())->method('provide')->willReturn(new \stdClass());
90+
91+
$operation = new Get(parameters: new Parameters([
92+
'foo' => (new QueryParameter(key: 'foo'))->withConstraints([$constraint])->setValue('bar'),
93+
]));
94+
$request = new Request();
95+
$request->attributes->set('_api_operation', $operation);
96+
97+
$provider = new ParameterValidatorProvider($validator, $decorated);
98+
$result = $provider->provide($operation, [], ['request' => $request]);
99+
100+
$this->assertInstanceOf(\stdClass::class, $result);
101+
}
102+
103+
public function testProvideWithInvalidParameters(): void
104+
{
105+
$this->expectException(ValidationException::class);
106+
107+
$constraint = new NotBlank();
108+
$violationList = new ConstraintViolationList();
109+
$violationList->add($this->createMock(\Symfony\Component\Validator\ConstraintViolationInterface::class));
110+
111+
$validator = $this->createMock(ValidatorInterface::class);
112+
$validator->expects($this->once())->method('validate')->willReturn($violationList);
113+
$decorated = $this->createMock(ProviderInterface::class);
114+
$decorated->expects($this->never())->method('provide');
115+
116+
$operation = new Get(parameters: new Parameters([
117+
'foo' => (new QueryParameter(key: 'foo'))->withConstraints([$constraint])->setValue(new ParameterNotFound()),
118+
]));
119+
$request = new Request();
120+
$request->attributes->set('_api_operation', $operation);
121+
122+
$provider = new ParameterValidatorProvider($validator, $decorated);
123+
$provider->provide($operation, [], ['request' => $request]);
124+
}
125+
126+
public function testProvideWithUriVariables(): void
127+
{
128+
$constraint = new NotBlank();
129+
$validator = $this->createMock(ValidatorInterface::class);
130+
$validator->expects($this->once())->method('validate')->willReturn(new ConstraintViolationList());
131+
$decorated = $this->createMock(ProviderInterface::class);
132+
$decorated->expects($this->once())->method('provide')->willReturn(new \stdClass());
133+
134+
$operation = new Get(uriVariables: [
135+
'id' => (new Link())->withConstraints([$constraint])->setValue('1'),
136+
]);
137+
$request = new Request();
138+
$request->attributes->set('_api_operation', $operation);
139+
140+
$provider = new ParameterValidatorProvider($validator, $decorated);
141+
$result = $provider->provide($operation, ['id' => 1], ['request' => $request]);
142+
143+
$this->assertInstanceOf(\stdClass::class, $result);
144+
}
145+
146+
public function testGetPropertyWithDeepObject(): void
147+
{
148+
$constraint = new NotBlank();
149+
$violationList = new ConstraintViolationList();
150+
$violation = $this->createMock(\Symfony\Component\Validator\ConstraintViolationInterface::class);
151+
$violation->method('getPropertyPath')->willReturn('[bar]');
152+
$violationList->add($violation);
153+
154+
$validator = $this->createMock(ValidatorInterface::class);
155+
$validator->expects($this->once())->method('validate')->willReturn($violationList);
156+
$decorated = $this->createMock(ProviderInterface::class);
157+
$decorated->expects($this->never())->method('provide');
158+
159+
$parameter = (new QueryParameter(key: 'foo'))->withConstraints([$constraint])->setValue(new ParameterNotFound());
160+
$parameter = $parameter->withOpenApi(new ModelParameter(name: 'foo', in: 'query', style: 'deepObject'));
161+
162+
$operation = new Get(parameters: new Parameters([
163+
'foo' => $parameter,
164+
]));
165+
$request = new Request();
166+
$request->attributes->set('_api_operation', $operation);
167+
168+
$provider = new ParameterValidatorProvider($validator, $decorated);
169+
try {
170+
$provider->provide($operation, [], ['request' => $request]);
171+
} catch (ValidationException $e) {
172+
$this->assertEquals('foo[bar]', $e->getConstraintViolationList()->get(0)->getPropertyPath());
173+
}
174+
}
175+
}

src/Symfony/Validator/State/ParameterValidatorProvider.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,13 @@ private function getProperty(Parameter $parameter, ConstraintViolationInterface
118118
$openApi = null;
119119
}
120120

121-
if ('deepObject' === $openApi?->getStyle() && $p = $violation->getPropertyPath()) {
121+
if (\is_array($openApi)) {
122+
foreach ($openApi as $oa) {
123+
if ('deepObject' === $oa->getStyle() && ($oa->getName() === $key || str_starts_with($oa->getName(), $key.'['))) {
124+
return $key.$violation->getPropertyPath();
125+
}
126+
}
127+
} elseif ('deepObject' === $openApi?->getStyle() && $p = $violation->getPropertyPath()) {
122128
return $key.$p;
123129
}
124130

0 commit comments

Comments
 (0)