• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 15069419398

16 May 2025 01:19PM UTC coverage: 21.832%. First build
15069419398

Pull #6960

github

web-flow
Merge 88ff9fb4b into b6080d419
Pull Request #6960: feat(json-schema): mutualize json schema between formats

0 of 470 new or added lines in 25 files covered. (0.0%)

11116 of 50915 relevant lines covered (21.83%)

29.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
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\OpenApi\Tests\Serializer;
15

16
use ApiPlatform\JsonSchema\DefinitionNameFactory;
17
use ApiPlatform\JsonSchema\SchemaFactory;
18
use ApiPlatform\Metadata\ApiProperty;
19
use ApiPlatform\Metadata\ApiResource;
20
use ApiPlatform\Metadata\Delete;
21
use ApiPlatform\Metadata\Get;
22
use ApiPlatform\Metadata\GetCollection;
23
use ApiPlatform\Metadata\HttpOperation;
24
use ApiPlatform\Metadata\Operations;
25
use ApiPlatform\Metadata\Post;
26
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
27
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
28
use ApiPlatform\Metadata\Property\PropertyNameCollection;
29
use ApiPlatform\Metadata\Put;
30
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
31
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
32
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
33
use ApiPlatform\Metadata\Resource\ResourceNameCollection;
34
use ApiPlatform\OpenApi\Factory\OpenApiFactory;
35
use ApiPlatform\OpenApi\Model\Components;
36
use ApiPlatform\OpenApi\Model\Info;
37
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
38
use ApiPlatform\OpenApi\Model\Parameter;
39
use ApiPlatform\OpenApi\Model\Paths;
40
use ApiPlatform\OpenApi\Model\Schema;
41
use ApiPlatform\OpenApi\Model\Server;
42
use ApiPlatform\OpenApi\OpenApi;
43
use ApiPlatform\OpenApi\Options;
44
use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer;
45
use ApiPlatform\OpenApi\Tests\Fixtures\Dummy;
46
use ApiPlatform\State\ApiResource\Error;
47
use ApiPlatform\State\Pagination\PaginationOptions;
48
use ApiPlatform\Validator\Exception\ValidationException;
49
use PHPUnit\Framework\TestCase;
50
use Prophecy\Argument;
51
use Prophecy\PhpUnit\ProphecyTrait;
52
use Psr\Container\ContainerInterface;
53
use Symfony\Component\Serializer\Encoder\JsonEncoder;
54
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
55
use Symfony\Component\Serializer\Serializer;
56
use Symfony\Component\TypeInfo\Type;
57

58
class OpenApiNormalizerTest extends TestCase
59
{
60
    use ProphecyTrait;
61

62
    private const OPERATION_FORMATS = [
63
        'input_formats' => ['jsonld' => ['application/ld+json']],
64
        'output_formats' => ['jsonld' => ['application/ld+json']],
65
    ];
66

67
    public function testNormalizeWithSchemas(): void
68
    {
69
        $openApi = new OpenApi(new Info('My API', '1.0.0', 'An amazing API'), [new Server('https://example.com')], new Paths(), new Components(new \ArrayObject(['z' => new Schema(), 'b' => new Schema()])));
×
70
        $encoders = [new JsonEncoder()];
×
71
        $normalizers = [new ObjectNormalizer()];
×
72

73
        $serializer = new Serializer($normalizers, $encoders);
×
74
        $normalizers[0]->setSerializer($serializer);
×
75

76
        $normalizer = new OpenApiNormalizer($normalizers[0]);
×
77

78
        $array = $normalizer->normalize($openApi);
×
79

80
        $this->assertSame(array_keys($array['components']['schemas']), ['b', 'z']);
×
81
    }
82

83
    public function testNormalizeWithEmptySchemas(): void
84
    {
85
        $openApi = new OpenApi(new Info('My API', '1.0.0', 'An amazing API'), [new Server('https://example.com')], new Paths(), new Components(new \ArrayObject()));
×
86
        $encoders = [new JsonEncoder()];
×
87
        $normalizers = [new ObjectNormalizer()];
×
88

89
        $serializer = new Serializer($normalizers, $encoders);
×
90
        $normalizers[0]->setSerializer($serializer);
×
91

92
        $normalizer = new OpenApiNormalizer($normalizers[0]);
×
93

94
        $array = $normalizer->normalize($openApi);
×
95
        $this->assertCount(0, $array['components']['schemas']);
×
96
    }
97

98
    public function testNormalize(): void
99
    {
100
        $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class);
×
101
        $resourceNameCollectionFactoryProphecy->create()->shouldBeCalled()->willReturn(new ResourceNameCollection([Dummy::class, 'Zorro']));
×
102
        $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
×
103
        $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id', 'name', 'description', 'dummyDate']));
×
104
        $propertyNameCollectionFactoryProphecy->create('Zorro', Argument::any())->shouldBeCalled()->willReturn(new PropertyNameCollection(['id']));
×
105

106
        $baseOperation = (new HttpOperation())->withTypes(['http://schema.example.com/Dummy'])
×
107
                                              ->withInputFormats(self::OPERATION_FORMATS['input_formats'])->withOutputFormats(self::OPERATION_FORMATS['output_formats'])
×
108
                                              ->withClass(Dummy::class)
×
109
                                              ->withShortName('Dummy')
×
110
                                              ->withDescription('This is a dummy.');
×
111

112
        $dummyMetadata = new ResourceMetadataCollection(Dummy::class, [
×
113
            (new ApiResource())->withOperations(new Operations(
×
114
                [
×
115
                    'get' => (new Get())->withUriTemplate('/dummies/{id}')->withOperation($baseOperation),
×
116
                    'put' => (new Put())->withUriTemplate('/dummies/{id}')->withOperation($baseOperation),
×
117
                    'delete' => (new Delete())->withUriTemplate('/dummies/{id}')->withOperation($baseOperation),
×
118
                    'get_collection' => (new GetCollection())->withUriTemplate('/dummies')->withOperation($baseOperation),
×
119
                    'post' => (new Post())->withUriTemplate('/dummies')->withOpenapi(new OpenApiOperation(
×
120
                        security: [],
×
121
                        servers: ['url' => '/test'],
×
122
                    ))->withOperation($baseOperation),
×
123
                ]
×
124
            )),
×
125
        ]);
×
126

127
        $zorroBaseOperation = (new HttpOperation())
×
128
            ->withTypes(['http://schema.example.com/Zorro'])
×
129
            ->withInputFormats(self::OPERATION_FORMATS['input_formats'])->withOutputFormats(self::OPERATION_FORMATS['output_formats'])
×
130
            ->withClass('Zorro')
×
131
            ->withShortName('Zorro')
×
132
            ->withDescription('This is zorro.');
×
133

134
        $zorroMetadata = new ResourceMetadataCollection(Dummy::class, [
×
135
            (new ApiResource())->withOperations(new Operations(
×
136
                [
×
137
                    'get' => (new Get())->withUriTemplate('/zorros/{id}')->withOperation($zorroBaseOperation),
×
138
                    'get_collection' => (new GetCollection())->withUriTemplate('/zorros')->withOperation($zorroBaseOperation),
×
139
                ]
×
140
            )),
×
141
        ]);
×
142

143
        $resourceCollectionMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
×
144
        $resourceCollectionMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata);
×
145
        $resourceCollectionMetadataFactoryProphecy->create('Zorro')->shouldBeCalled()->willReturn($zorroMetadata);
×
146
        $resourceCollectionMetadataFactoryProphecy->create(Error::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Error::class, []));
×
147
        $resourceCollectionMetadataFactoryProphecy->create(ValidationException::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(ValidationException::class, []));
×
148

149
        $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
×
150
        $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::any())->shouldBeCalled()->willReturn(
×
151
            (new ApiProperty())
×
152
                ->withNativeType(Type::int())
×
153
                ->withDescription('This is an id.')
×
154
                ->withReadable(true)
×
155
                ->withWritable(false)
×
156
                ->withIdentifier(true)
×
157
                ->withSchema(['type' => 'integer', 'description' => 'This is an id.', 'readOnly' => true])
×
158
        );
×
159
        $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->shouldBeCalled()->willReturn(
×
160
            (new ApiProperty())
×
161
                ->withNativeType(Type::string())
×
162
                ->withDescription('This is a name.')
×
163
                ->withReadable(true)
×
164
                ->withWritable(true)
×
165
                ->withReadableLink(true)
×
166
                ->withWritableLink(true)
×
167
                ->withRequired(false)
×
168
                ->withIdentifier(false)
×
169
                ->withSchema(['type' => 'string', 'description' => 'This is a name.', 'minLength' => 3, 'maxLength' => 20, 'pattern' => '^dummyPattern$'])
×
170
        );
×
171
        $propertyMetadataFactoryProphecy->create(Dummy::class, 'description', Argument::any())->shouldBeCalled()->willReturn(
×
172
            (new ApiProperty())
×
173
                ->withNativeType(Type::string())
×
174
                ->withDescription('This is an initializable but not writable property.')
×
175
                ->withReadable(true)
×
176
                ->withWritable(false)
×
177
                ->withReadableLink(true)
×
178
                ->withWritableLink(true)
×
179
                ->withRequired(false)
×
180
                ->withIdentifier(false)
×
181
                ->withSchema(['type' => 'string', 'readOnly' => true, 'description' => 'This is an initializable but not writable property.'])
×
182
        );
×
183
        $propertyMetadataFactoryProphecy->create(Dummy::class, 'dummyDate', Argument::any())->shouldBeCalled()->willReturn(
×
184
            (new ApiProperty())
×
185
                ->withNativeType(Type::nullable(Type::object(\DateTime::class)))
×
186
                ->withDescription('This is a \DateTimeInterface object.')
×
187
                ->withReadable(true)
×
188
                ->withWritable(true)
×
189
                ->withReadableLink(true)
×
190
                ->withWritableLink(true)
×
191
                ->withRequired(false)
×
192
                ->withIdentifier(false)
×
193
                ->withSchema(['type' => 'string', 'format' => 'date-time', 'description' => 'This is a \DateTimeInterface object.'])
×
194
        );
×
195

196
        $propertyMetadataFactoryProphecy->create('Zorro', 'id', Argument::any())->shouldBeCalled()->willReturn(
×
197
            (new ApiProperty())
×
198
                ->withNativeType(Type::int())
×
199
                ->withDescription('This is an id.')
×
200
                ->withReadable(true)
×
201
                ->withWritable(false)
×
202
                ->withIdentifier(true)
×
203
                ->withSchema(['type' => 'integer', 'description' => 'This is an id.', 'readOnly' => true])
×
204
        );
×
205

206
        $filterLocatorProphecy = $this->prophesize(ContainerInterface::class);
×
207
        $resourceMetadataFactory = $resourceCollectionMetadataFactoryProphecy->reveal();
×
208
        $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal();
×
209
        $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal();
×
210

NEW
211
        $definitionNameFactory = new DefinitionNameFactory();
×
212

213
        $schemaFactory = new SchemaFactory(
×
214
            resourceMetadataFactory: $resourceMetadataFactory,
×
215
            propertyNameCollectionFactory: $propertyNameCollectionFactory,
×
216
            propertyMetadataFactory: $propertyMetadataFactory,
×
217
            definitionNameFactory: $definitionNameFactory,
×
218
        );
×
219

220
        $factory = new OpenApiFactory(
×
221
            $resourceNameCollectionFactoryProphecy->reveal(),
×
222
            $resourceMetadataFactory,
×
223
            $propertyNameCollectionFactory,
×
224
            $propertyMetadataFactory,
×
225
            $schemaFactory,
×
226
            $filterLocatorProphecy->reveal(),
×
227
            [],
×
228
            new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [
×
229
                'header' => [
×
230
                    'type' => 'header',
×
231
                    'name' => 'Authorization',
×
232
                ],
×
233
                'query' => [
×
234
                    'type' => 'query',
×
235
                    'name' => 'key',
×
236
                ],
×
237
            ]),
×
238
            new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination')
×
239
        );
×
240

241
        $openApi = $factory(['base_url' => '/app_dev.php/']);
×
242

243
        $pathItem = $openApi->getPaths()->getPath('/dummies/{id}');
×
244
        $operation = $pathItem->getGet();
×
245

246
        $openApi->getPaths()->addPath('/dummies/{id}', $pathItem->withGet(
×
247
            $operation->withParameters(array_merge(
×
248
                $operation->getParameters(),
×
249
                [new Parameter('fields', 'query', 'Fields to remove of the output')]
×
250
            ))
×
251
        ));
×
252

253
        $openApi = $openApi->withInfo((new Info('New Title', 'v2', 'Description of my custom API'))->withExtensionProperty('info-key', 'Info value'));
×
254
        $openApi = $openApi->withExtensionProperty('key', 'Custom x-key value');
×
255
        $openApi = $openApi->withExtensionProperty('x-value', 'Custom x-value value');
×
256

257
        $encoders = [new JsonEncoder()];
×
258
        $normalizers = [new ObjectNormalizer()];
×
259

260
        $serializer = new Serializer($normalizers, $encoders);
×
261
        $normalizers[0]->setSerializer($serializer);
×
262

263
        $normalizer = new OpenApiNormalizer($normalizers[0]);
×
264

265
        $openApiAsArray = $normalizer->normalize($openApi);
×
266

267
        // Just testing normalization specifics
268
        $this->assertSame($openApiAsArray['x-key'], 'Custom x-key value');
×
269
        $this->assertSame($openApiAsArray['x-value'], 'Custom x-value value');
×
270
        $this->assertSame($openApiAsArray['info']['x-info-key'], 'Info value');
×
271
        $this->assertArrayNotHasKey('extensionProperties', $openApiAsArray);
×
272
        // this key is null, should not be in the output
273
        $this->assertArrayNotHasKey('termsOfService', $openApiAsArray['info']);
×
274
        $this->assertArrayNotHasKey('paths', $openApiAsArray['paths']);
×
275
        $this->assertArrayHasKey('/dummies/{id}', $openApiAsArray['paths']);
×
276
        $this->assertArrayNotHasKey('servers', $openApiAsArray['paths']['/dummies/{id}']['get']);
×
277
        $this->assertArrayNotHasKey('security', $openApiAsArray['paths']['/dummies/{id}']['get']);
×
278

279
        // Security can be disabled per-operation using an empty array
280
        $this->assertEquals([], $openApiAsArray['paths']['/dummies']['post']['security']);
×
281
        $this->assertEquals(['url' => '/test'], $openApiAsArray['paths']['/dummies']['post']['servers']);
×
282

283
        // Make sure things are sorted
284
        $this->assertSame(array_keys($openApiAsArray['paths']), ['/dummies', '/dummies/{id}', '/zorros', '/zorros/{id}']);
×
285
        // Test name converter doesn't rename this property
286
        $this->assertArrayHasKey('requestBody', $openApiAsArray['paths']['/dummies']['post']);
×
287
    }
288
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc