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

api-platform / core / 6665412360

27 Oct 2023 09:27AM UTC coverage: 37.477% (-0.02%) from 37.494%
6665412360

push

github

web-flow
fix(graphql): item resolver inheritance error  (#5910)

* fix(graphql): improve condition to allow inheritance

* test: graphql inheritance

---------

Co-authored-by: soyuka <soyuka@users.noreply.github.com>

13 of 13 new or added lines in 2 files covered. (100.0%)

10316 of 27526 relevant lines covered (37.48%)

20.61 hits per line

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

0.0
/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.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\Resolver\Factory;
15

16
use ApiPlatform\GraphQl\Resolver\Factory\ItemResolverFactory;
17
use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface;
18
use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface;
19
use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface;
20
use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface;
21
use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\ChildFoo;
22
use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy;
23
use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\ParentFoo;
24
use ApiPlatform\Metadata\GraphQl\Query;
25
use GraphQL\Type\Definition\ResolveInfo;
26
use PHPUnit\Framework\TestCase;
27
use Prophecy\PhpUnit\ProphecyTrait;
28
use Prophecy\Prophecy\ObjectProphecy;
29
use Psr\Container\ContainerInterface;
30

31
/**
32
 * @author Alan Poulain <contact@alanpoulain.eu>
33
 * @author Kévin Dunglas <dunglas@gmail.com>
34
 */
35
class ItemResolverFactoryTest extends TestCase
36
{
37
    use ProphecyTrait;
38

39
    private ItemResolverFactory $itemResolverFactory;
40
    private ObjectProphecy $readStageProphecy;
41
    private ObjectProphecy $securityStageProphecy;
42
    private ObjectProphecy $securityPostDenormalizeStageProphecy;
43
    private ObjectProphecy $serializeStageProphecy;
44
    private ObjectProphecy $queryResolverLocatorProphecy;
45

46
    /**
47
     * {@inheritdoc}
48
     */
49
    protected function setUp(): void
50
    {
51
        $this->readStageProphecy = $this->prophesize(ReadStageInterface::class);
×
52
        $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class);
×
53
        $this->securityPostDenormalizeStageProphecy = $this->prophesize(SecurityPostDenormalizeStageInterface::class);
×
54
        $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class);
×
55
        $this->queryResolverLocatorProphecy = $this->prophesize(ContainerInterface::class);
×
56

57
        $this->itemResolverFactory = new ItemResolverFactory(
×
58
            $this->readStageProphecy->reveal(),
×
59
            $this->securityStageProphecy->reveal(),
×
60
            $this->securityPostDenormalizeStageProphecy->reveal(),
×
61
            $this->serializeStageProphecy->reveal(),
×
62
            $this->queryResolverLocatorProphecy->reveal()
×
63
        );
×
64
    }
65

66
    /**
67
     * @dataProvider itemResourceProvider
68
     */
69
    public function testResolve(?string $resourceClass, string $determinedResourceClass, ?object $readStageItem): void
70
    {
71
        $rootClass = 'rootClass';
×
72
        $operationName = 'item_query';
×
73
        $operation = (new Query())->withName($operationName);
×
74
        $source = ['source'];
×
75
        $args = ['args'];
×
76
        $info = $this->prophesize(ResolveInfo::class)->reveal();
×
77
        $info->fieldName = 'field';
×
78
        $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false];
×
79

80
        $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem);
×
81

82
        $this->securityStageProphecy->__invoke($determinedResourceClass, $operation, $resolverContext + [
×
83
            'extra_variables' => [
×
84
                'object' => $readStageItem,
×
85
            ],
×
86
        ])->shouldBeCalled();
×
87
        $this->securityPostDenormalizeStageProphecy->__invoke($determinedResourceClass, $operation, $resolverContext + [
×
88
            'extra_variables' => [
×
89
                'object' => $readStageItem,
×
90
                'previous_object' => $readStageItem,
×
91
            ],
×
92
        ])->shouldBeCalled();
×
93

94
        $serializeStageData = ['serialized'];
×
95
        $this->serializeStageProphecy->__invoke($readStageItem, $determinedResourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData);
×
96

97
        $this->assertSame($serializeStageData, ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info));
×
98
    }
99

100
    public static function itemResourceProvider(): array
101
    {
102
        return [
×
103
            'nominal' => [\stdClass::class, \stdClass::class, new \stdClass()],
×
104
            'null item' => [\stdClass::class, \stdClass::class, null],
×
105
            'null resource class' => [null, \stdClass::class, new \stdClass()],
×
106
        ];
×
107
    }
108

109
    public function testResolveNested(): void
110
    {
111
        $source = ['nested' => ['already_serialized']];
×
112
        $info = $this->prophesize(ResolveInfo::class)->reveal();
×
113
        $info->fieldName = 'nested';
×
114

115
        $this->assertEquals(['already_serialized'], ($this->itemResolverFactory)('resourceClass')($source, [], null, $info));
×
116
    }
117

118
    public function testResolveNestedNullValue(): void
119
    {
120
        $source = ['nestedNullValue' => null];
×
121
        $info = $this->prophesize(ResolveInfo::class)->reveal();
×
122
        $info->fieldName = 'nestedNullValue';
×
123

124
        $this->assertNull(($this->itemResolverFactory)('resourceClass')($source, [], null, $info));
×
125
    }
126

127
    public function testResolveBadReadStageItem(): void
128
    {
129
        $resourceClass = \stdClass::class;
×
130
        $rootClass = 'rootClass';
×
131
        $operationName = 'item_query';
×
132
        $operation = (new Query())->withName($operationName);
×
133
        $source = ['source'];
×
134
        $args = ['args'];
×
135
        $info = $this->prophesize(ResolveInfo::class)->reveal();
×
136
        $info->fieldName = 'field';
×
137
        $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false];
×
138

139
        $readStageItem = [];
×
140
        $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem);
×
141

142
        $this->expectException(\LogicException::class);
×
143
        $this->expectExceptionMessage('Item from read stage should be a nullable object.');
×
144

145
        ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info);
×
146
    }
147

148
    public function testResolveNoResourceNoItem(): void
149
    {
150
        $resourceClass = null;
×
151
        $rootClass = 'rootClass';
×
152
        $operationName = 'item_query';
×
153
        $operation = (new Query())->withName($operationName);
×
154
        $source = ['source'];
×
155
        $args = ['args'];
×
156
        $info = $this->prophesize(ResolveInfo::class)->reveal();
×
157
        $info->fieldName = 'field';
×
158
        $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false];
×
159

160
        $readStageItem = null;
×
161
        $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem);
×
162

163
        $this->expectException(\UnexpectedValueException::class);
×
164
        $this->expectExceptionMessage('Resource class cannot be determined.');
×
165

166
        ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info);
×
167
    }
168

169
    public function testResolveBadItem(): void
170
    {
171
        $resourceClass = Dummy::class;
×
172
        $rootClass = 'rootClass';
×
173
        $operationName = 'item_query';
×
174
        $operation = (new Query())->withName($operationName);
×
175
        $source = ['source'];
×
176
        $args = ['args'];
×
177
        $info = $this->prophesize(ResolveInfo::class)->reveal();
×
178
        $info->fieldName = 'field';
×
179
        $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false];
×
180

181
        $readStageItem = new \stdClass();
×
182
        $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem);
×
183

184
        $this->expectException(\UnexpectedValueException::class);
×
185
        $this->expectExceptionMessage('Resolver only handles items of class Dummy but retrieved item is of class stdClass.');
×
186

187
        ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info);
×
188
    }
189

190
    public function testResolveCustom(): void
191
    {
192
        $resourceClass = \stdClass::class;
×
193
        $rootClass = 'rootClass';
×
194
        $operationName = 'custom_query';
×
195
        $operation = (new Query())->withResolver('query_resolver_id')->withName($operationName);
×
196
        $source = ['source'];
×
197
        $args = ['args'];
×
198
        $info = $this->prophesize(ResolveInfo::class)->reveal();
×
199
        $info->fieldName = 'field';
×
200
        $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false];
×
201

202
        $readStageItem = new \stdClass();
×
203
        $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem);
×
204

205
        $customItem = new \stdClass();
×
206
        $customItem->field = 'foo';
×
207
        $this->queryResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): \stdClass => $customItem);
×
208

209
        $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [
×
210
            'extra_variables' => [
×
211
                'object' => $customItem,
×
212
            ],
×
213
        ])->shouldBeCalled();
×
214
        $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [
×
215
            'extra_variables' => [
×
216
                'object' => $customItem,
×
217
                'previous_object' => $customItem,
×
218
            ],
×
219
        ])->shouldBeCalled();
×
220

221
        $serializeStageData = ['serialized'];
×
222
        $this->serializeStageProphecy->__invoke($customItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData);
×
223

224
        $this->assertSame($serializeStageData, ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info));
×
225
    }
226

227
    public function testResolveCustomBadItem(): void
228
    {
229
        $resourceClass = \stdClass::class;
×
230
        $rootClass = 'rootClass';
×
231
        $operationName = 'custom_query';
×
232
        $operation = (new Query())->withResolver('query_resolver_id')->withName($operationName);
×
233
        $source = ['source'];
×
234
        $args = ['args'];
×
235
        $info = $this->prophesize(ResolveInfo::class)->reveal();
×
236
        $info->fieldName = 'field';
×
237
        $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false];
×
238

239
        $readStageItem = new \stdClass();
×
240
        $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem);
×
241

242
        $customItem = new Dummy();
×
243
        $this->queryResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): Dummy => $customItem);
×
244

245
        $this->expectException(\UnexpectedValueException::class);
×
246
        $this->expectExceptionMessage('Custom query resolver "query_resolver_id" has to return an item of class stdClass but returned an item of class Dummy.');
×
247

248
        ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info);
×
249
    }
250

251
    public function testResolveInheritedClass(): void
252
    {
253
        $resourceClass = ParentFoo::class;
×
254
        $rootClass = $resourceClass;
×
255
        $operationName = 'custom_query';
×
256
        $operation = (new Query())->withName($operationName);
×
257
        $source = ['source'];
×
258
        $args = ['args'];
×
259
        $info = $this->prophesize(ResolveInfo::class)->reveal();
×
260
        $info->fieldName = 'field';
×
261
        $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false];
×
262

263
        $readStageItem = new ChildFoo();
×
264
        $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem);
×
265

266
        ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info);
×
267
    }
268
}
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