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

api-platform / core / 17729190580

15 Sep 2025 09:57AM UTC coverage: 22.227% (-0.4%) from 22.578%
17729190580

push

github

web-flow
fix(metadata): compute isWritable during updates (#7383)

fixes #7382

4 of 532 new or added lines in 16 files covered. (0.75%)

167 existing lines in 22 files now uncovered.

11127 of 50061 relevant lines covered (22.23%)

23.49 hits per line

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

0.0
/src/GraphQl/Tests/Serializer/ItemNormalizerTest.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\GraphQl\Tests\Serializer;
15

16
use ApiPlatform\GraphQl\Serializer\ItemNormalizer;
17
use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy;
18
use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\SecuredDummy;
19
use ApiPlatform\Metadata\ApiProperty;
20
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
21
use ApiPlatform\Metadata\IriConverterInterface;
22
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
23
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
24
use ApiPlatform\Metadata\Property\PropertyNameCollection;
25
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
26
use ApiPlatform\Metadata\ResourceClassResolverInterface;
27
use ApiPlatform\Metadata\UrlGeneratorInterface;
28
use PHPUnit\Framework\TestCase;
29
use Prophecy\Argument;
30
use Prophecy\PhpUnit\ProphecyTrait;
31
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
32
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
33
use Symfony\Component\Serializer\SerializerInterface;
34

35
/**
36
 * @author Kévin Dunglas <dunglas@gmail.com>
37
 */
38
class ItemNormalizerTest extends TestCase
39
{
40
    use ProphecyTrait;
41

42
    public function testSupportNormalization(): void
43
    {
44
        $std = new \stdClass();
×
45
        $dummy = new Dummy();
×
46
        $dummy->setDescription('hello');
×
47

48
        $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
×
49
        $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
×
50
        $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
×
51
        $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
×
52

53
        $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
×
54
        $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true)->shouldBeCalled();
×
55
        $resourceClassResolverProphecy->isResourceClass(\stdClass::class)->willReturn(false)->shouldBeCalled();
×
56

57
        $normalizer = new ItemNormalizer(
×
58
            $propertyNameCollectionFactoryProphecy->reveal(),
×
59
            $propertyMetadataFactoryProphecy->reveal(),
×
60
            $iriConverterProphecy->reveal(),
×
61
            $identifiersExtractorProphecy->reveal(),
×
62
            $resourceClassResolverProphecy->reveal()
×
63
        );
×
64

65
        $this->assertTrue($normalizer->supportsNormalization($dummy, ItemNormalizer::FORMAT));
×
66
        $this->assertTrue($normalizer->supportsNormalization($dummy, ItemNormalizer::FORMAT));
×
67
        $this->assertFalse($normalizer->supportsNormalization($std, ItemNormalizer::FORMAT));
×
68

69
        $this->assertTrue($normalizer->supportsDenormalization($dummy, Dummy::class, ItemNormalizer::FORMAT));
×
70
        $this->assertTrue($normalizer->supportsDenormalization($dummy, Dummy::class, ItemNormalizer::FORMAT));
×
71
        $this->assertFalse($normalizer->supportsDenormalization($std, \stdClass::class, ItemNormalizer::FORMAT));
×
72
    }
73

74
    public function testNormalize(): void
75
    {
76
        $dummy = new Dummy();
×
77
        $dummy->setName('hello');
×
78

79
        $propertyNameCollection = new PropertyNameCollection(['name']);
×
80
        $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
×
NEW
81
        $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn($propertyNameCollection);
×
82

83
        $propertyMetadata = (new ApiProperty())->withReadable(true);
×
84
        $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
×
NEW
85
        $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn($propertyMetadata);
×
86

87
        $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
×
88
        $iriConverterProphecy->getIriFromResource($dummy, UrlGeneratorInterface::ABS_URL, Argument::any(), Argument::type('array'))->willReturn('/dummies/1');
×
89

90
        $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
×
91
        $identifiersExtractorProphecy->getIdentifiersFromItem($dummy, Argument::any())->willReturn(['id' => 1])->shouldBeCalled();
×
92

93
        $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
×
94
        $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class);
×
95
        $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class);
×
96
        $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
×
97

98
        $serializerProphecy = $this->prophesize(SerializerInterface::class);
×
99
        $serializerProphecy->willImplement(NormalizerInterface::class);
×
100
        $serializerProphecy->normalize('hello', ItemNormalizer::FORMAT, Argument::type('array'))->willReturn('hello');
×
101

102
        $normalizer = new ItemNormalizer(
×
103
            $propertyNameCollectionFactoryProphecy->reveal(),
×
104
            $propertyMetadataFactoryProphecy->reveal(),
×
105
            $iriConverterProphecy->reveal(),
×
106
            $identifiersExtractorProphecy->reveal(),
×
107
            $resourceClassResolverProphecy->reveal()
×
108
        );
×
109
        $normalizer->setSerializer($serializerProphecy->reveal());
×
110

111
        $expected = [
×
112
            'name' => 'hello',
×
113
            ItemNormalizer::ITEM_RESOURCE_CLASS_KEY => Dummy::class,
×
114
            ItemNormalizer::ITEM_IDENTIFIERS_KEY => [
×
115
                'id' => 1,
×
116
            ],
×
117
        ];
×
118
        $this->assertEquals($expected, $normalizer->normalize($dummy, ItemNormalizer::FORMAT, [
×
119
            'resources' => [],
×
120
            'resource_class' => Dummy::class,
×
121
        ]));
×
122
    }
123

124
    public function testNormalizeWithUnsafeCacheProperty(): void
125
    {
126
        $securedDummyWithOwnerOnlyPropertyAllowed = new SecuredDummy();
×
127
        $securedDummyWithOwnerOnlyPropertyAllowed->setTitle('hello');
×
128
        $securedDummyWithOwnerOnlyPropertyAllowed->setOwnerOnlyProperty('ownerOnly');
×
129
        $securedDummyWithoutOwnerOnlyPropertyAllowed = clone $securedDummyWithOwnerOnlyPropertyAllowed;
×
130
        $securedDummyWithoutOwnerOnlyPropertyAllowed->setTitle('hello from secured dummy');
×
131

132
        $propertyNameCollection = new PropertyNameCollection(['title', 'ownerOnlyProperty']);
×
133
        $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
×
NEW
134
        $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, Argument::type('array'))->willReturn($propertyNameCollection);
×
135

136
        $unsecuredPropertyMetadata = (new ApiProperty())->withReadable(true);
×
137
        $securedPropertyMetadata = (new ApiProperty())->withReadable(true)->withSecurity('object == null or object.getOwner() == user');
×
138
        $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
×
NEW
139
        $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', Argument::type('array'))->willReturn($unsecuredPropertyMetadata);
×
NEW
140
        $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', Argument::type('array'))->willReturn($securedPropertyMetadata);
×
141

142
        $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
×
143
        $iriConverterProphecy->getIriFromResource($securedDummyWithOwnerOnlyPropertyAllowed, UrlGeneratorInterface::ABS_URL, Argument::any(), Argument::type('array'))->willReturn('/dummies/1');
×
144
        $iriConverterProphecy->getIriFromResource($securedDummyWithoutOwnerOnlyPropertyAllowed, UrlGeneratorInterface::ABS_URL, Argument::any(), Argument::type('array'))->willReturn('/dummies/2');
×
145

146
        $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
×
147
        $identifiersExtractorProphecy->getIdentifiersFromItem($securedDummyWithOwnerOnlyPropertyAllowed, Argument::any())->willReturn(['id' => 1])->shouldBeCalled();
×
148
        $identifiersExtractorProphecy->getIdentifiersFromItem($securedDummyWithoutOwnerOnlyPropertyAllowed, Argument::any())->willReturn(['id' => 2])->shouldBeCalled();
×
149

150
        $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
×
151
        $resourceClassResolverProphecy->getResourceClass($securedDummyWithOwnerOnlyPropertyAllowed, null)->willReturn(SecuredDummy::class);
×
152
        $resourceClassResolverProphecy->getResourceClass($securedDummyWithoutOwnerOnlyPropertyAllowed, null)->willReturn(SecuredDummy::class);
×
153
        $resourceClassResolverProphecy->getResourceClass(null, SecuredDummy::class)->willReturn(SecuredDummy::class);
×
154
        $resourceClassResolverProphecy->isResourceClass(SecuredDummy::class)->willReturn(true);
×
155

156
        $serializerProphecy = $this->prophesize(SerializerInterface::class);
×
157
        $serializerProphecy->willImplement(NormalizerInterface::class);
×
158
        $serializerProphecy->normalize('hello', ItemNormalizer::FORMAT, Argument::type('array'))->willReturn('hello');
×
159
        $serializerProphecy->normalize('hello from secured dummy', ItemNormalizer::FORMAT, Argument::type('array'))->willReturn('hello from secured dummy');
×
160
        $serializerProphecy->normalize('ownerOnly', ItemNormalizer::FORMAT, Argument::type('array'))->willReturn('ownerOnly');
×
161

162
        $resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class);
×
163
        $resourceAccessCheckerProphecy->isGranted(
×
164
            SecuredDummy::class,
×
165
            'object == null or object.getOwner() == user',
×
166
            Argument::type('array')
×
167
        )->will(function (array $args) {
×
168
            return 'hello' === $args[2]['object']->getTitle(); // Allow access only for securedDummyWithOwnerOnlyPropertyAllowed
×
169
        });
×
170

171
        $normalizer = new ItemNormalizer(
×
172
            $propertyNameCollectionFactoryProphecy->reveal(),
×
173
            $propertyMetadataFactoryProphecy->reveal(),
×
174
            $iriConverterProphecy->reveal(),
×
175
            $identifiersExtractorProphecy->reveal(),
×
176
            $resourceClassResolverProphecy->reveal(),
×
177
            null,
×
178
            null,
×
179
            null,
×
180
            null,
×
181
            null,
×
182
            $resourceAccessCheckerProphecy->reveal()
×
183
        );
×
184
        $normalizer->setSerializer($serializerProphecy->reveal());
×
185

186
        $expected = [
×
187
            'title' => 'hello',
×
188
            'ownerOnlyProperty' => 'ownerOnly',
×
189
            ItemNormalizer::ITEM_RESOURCE_CLASS_KEY => SecuredDummy::class,
×
190
            ItemNormalizer::ITEM_IDENTIFIERS_KEY => [
×
191
                'id' => 1,
×
192
            ],
×
193
        ];
×
194
        $this->assertEquals($expected, $normalizer->normalize($securedDummyWithOwnerOnlyPropertyAllowed, ItemNormalizer::FORMAT, [
×
195
            'resources' => [],
×
196
            'resource_class' => SecuredDummy::class,
×
197
        ]));
×
198

199
        $expected = [
×
200
            'title' => 'hello from secured dummy',
×
201
            ItemNormalizer::ITEM_RESOURCE_CLASS_KEY => SecuredDummy::class,
×
202
            ItemNormalizer::ITEM_IDENTIFIERS_KEY => [
×
203
                'id' => 2,
×
204
            ],
×
205
        ];
×
206
        $this->assertEquals($expected, $normalizer->normalize($securedDummyWithoutOwnerOnlyPropertyAllowed, ItemNormalizer::FORMAT, [
×
207
            'resources' => [],
×
208
            'resource_class' => SecuredDummy::class,
×
209
        ]));
×
210
    }
211

212
    public function testNormalizeNoResolverData(): void
213
    {
214
        $dummy = new Dummy();
×
215
        $dummy->setName('hello');
×
216

217
        $propertyNameCollection = new PropertyNameCollection(['name']);
×
218
        $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
×
NEW
219
        $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn($propertyNameCollection);
×
220

221
        $propertyMetadata = (new ApiProperty())->withWritable(true)->withReadable(true);
×
222
        $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
×
NEW
223
        $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn($propertyMetadata);
×
224

225
        $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
×
226
        $iriConverterProphecy->getIriFromResource($dummy, UrlGeneratorInterface::ABS_URL, Argument::any(), Argument::type('array'))->willReturn('/dummies/1');
×
227

228
        $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
×
229

230
        $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
×
231
        $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class);
×
232
        $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class);
×
233
        $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
×
234

235
        $serializerProphecy = $this->prophesize(SerializerInterface::class);
×
236
        $serializerProphecy->willImplement(NormalizerInterface::class);
×
237
        $serializerProphecy->normalize('hello', ItemNormalizer::FORMAT, Argument::type('array'))->willReturn('hello');
×
238

239
        $normalizer = new ItemNormalizer(
×
240
            $propertyNameCollectionFactoryProphecy->reveal(),
×
241
            $propertyMetadataFactoryProphecy->reveal(),
×
242
            $iriConverterProphecy->reveal(),
×
243
            $identifiersExtractorProphecy->reveal(),
×
244
            $resourceClassResolverProphecy->reveal()
×
245
        );
×
246
        $normalizer->setSerializer($serializerProphecy->reveal());
×
247

248
        $expected = [
×
249
            'name' => 'hello',
×
250
        ];
×
251
        $this->assertEquals($expected, $normalizer->normalize($dummy, ItemNormalizer::FORMAT, [
×
252
            'resources' => [],
×
253
            'no_resolver_data' => true,
×
254
        ]));
×
255
    }
256

257
    public function testDenormalize(): void
258
    {
259
        $context = ['resource_class' => Dummy::class, 'api_allow_update' => true];
×
260

261
        $propertyNameCollection = new PropertyNameCollection(['name']);
×
262
        $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
×
NEW
263
        $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn($propertyNameCollection)->shouldBeCalled();
×
264

265
        $propertyMetadata = (new ApiProperty())->withWritable(true)->withReadable(true);
×
266
        $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
×
NEW
267
        $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn($propertyMetadata)->shouldBeCalled();
×
268

269
        $iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
×
270

271
        $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
×
272

273
        $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
×
274
        $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class);
×
275
        $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
×
276

277
        $serializerProphecy = $this->prophesize(SerializerInterface::class);
×
278
        $serializerProphecy->willImplement(DenormalizerInterface::class);
×
279

280
        $normalizer = new ItemNormalizer(
×
281
            $propertyNameCollectionFactoryProphecy->reveal(),
×
282
            $propertyMetadataFactoryProphecy->reveal(),
×
283
            $iriConverterProphecy->reveal(),
×
284
            $identifiersExtractorProphecy->reveal(),
×
285
            $resourceClassResolverProphecy->reveal()
×
286
        );
×
287
        $normalizer->setSerializer($serializerProphecy->reveal());
×
288

289
        $this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['name' => 'hello'], Dummy::class, ItemNormalizer::FORMAT, $context));
×
290
    }
291
}
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

© 2026 Coveralls, Inc