Completed
Push — master ( 3ad203...a37316 )
by Adrien
01:50
created

TypesTest::testFieldWithArrayArgumentMustThrow()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQLTests\Doctrine;
6
7
use DateTime;
8
use Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain;
9
use Doctrine\ORM\Tools\SchemaValidator;
10
use GraphQL\Doctrine\Types;
11
use GraphQL\Type\Definition\BooleanType;
12
use GraphQL\Type\Definition\InputObjectType;
13
use GraphQL\Type\Definition\ObjectType;
14
use GraphQL\Type\Definition\Type;
15
use GraphQL\Type\Schema;
16
use GraphQLTests\Doctrine\Blog\Model\Post;
17
use GraphQLTests\Doctrine\Blog\Model\User;
18
use GraphQLTests\Doctrine\Blog\Types\CustomType;
19
use GraphQLTests\Doctrine\Blog\Types\DateTimeType;
20
use GraphQLTests\Doctrine\Blog\Types\PostStatusType;
21
use stdClass;
22
23
class TypesTest extends \PHPUnit\Framework\TestCase
24
{
25
    use EntityManagerTrait;
26
27
    /**
28
     * @var Types
29
     */
30
    private $types;
31
32
    public function setUp(): void
33
    {
34
        $this->setUpEntityManager();
35
36
        $mapping = [
37
            DateTime::class => DateTimeType::class,
38
            stdClass::class => CustomType::class,
39
        ];
40
        $this->types = new Types($this->entityManager, $mapping);
41
    }
42
43
    public function testBlogMapping(): void
44
    {
45
        $validator = new SchemaValidator($this->entityManager);
46
        $errors = $validator->validateMapping();
47
48
        self::assertEmpty($errors, 'doctrine annotations should be valid');
49
    }
50
51
    public function testGraphQLSchemaFromDocumentationMustBeValid(): void
52
    {
53
        $schema = new Schema([
54
            'query' => new ObjectType([
55
                'name' => 'query',
56
                'fields' => [
57
                    'users' => [
58
                        'type' => Type::listOf($this->types->get(User::class)), // Use automated ObjectType for output
59
                        'resolve' => function ($root, $args): void {
2 ignored issues
show
Unused Code introduced by
The parameter $args is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

59
                        'resolve' => function ($root, /** @scrutinizer ignore-unused */ $args): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $root is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

59
                        'resolve' => function (/** @scrutinizer ignore-unused */ $root, $args): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
60
                            // call to repository...
61
                        },
62
                    ],
63
                    'posts' => [
64
                        'type' => Type::listOf($this->types->get(Post::class)), // Use automated ObjectType for output
65
                        'resolve' => function ($root, $args): void {
2 ignored issues
show
Unused Code introduced by
The parameter $args is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

65
                        'resolve' => function ($root, /** @scrutinizer ignore-unused */ $args): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $root is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

65
                        'resolve' => function (/** @scrutinizer ignore-unused */ $root, $args): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
66
                            // call to repository...
67
                        },
68
                    ],
69
                ],
70
            ]),
71
            'mutation' => new ObjectType([
72
                'name' => 'mutation',
73
                'fields' => [
74
                    'createUser' => [
75
                        'type' => Type::nonNull($this->types->get(User::class)),
76
                        'args' => [
77
                            'input' => Type::nonNull($this->types->getInput(User::class)), // Use automated InputObjectType for input
78
                        ],
79
                        'resolve' => function ($root, $args): void {
2 ignored issues
show
Unused Code introduced by
The parameter $root is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

79
                        'resolve' => function (/** @scrutinizer ignore-unused */ $root, $args): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $args is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

79
                        'resolve' => function ($root, /** @scrutinizer ignore-unused */ $args): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
80
                            // create new user and flush...
81
                        },
82
                    ],
83
                    'updateUser' => [
84
                        'type' => Type::nonNull($this->types->get(User::class)),
85
                        'args' => [
86
                            'id' => Type::nonNull(Type::id()), // Use standard API when needed
87
                            'input' => $this->types->getInput(User::class),
88
                        ],
89
                        'resolve' => function ($root, $args): void {
2 ignored issues
show
Unused Code introduced by
The parameter $args is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

89
                        'resolve' => function ($root, /** @scrutinizer ignore-unused */ $args): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $root is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

89
                        'resolve' => function (/** @scrutinizer ignore-unused */ $root, $args): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
90
                            // update existing user and flush...
91
                        },
92
                    ],
93
                    'createPost' => [
94
                        'type' => Type::nonNull($this->types->get(Post::class)),
95
                        'args' => [
96
                            'input' => Type::nonNull($this->types->getInput(Post::class)), // Use automated InputObjectType for input
97
                        ],
98
                        'resolve' => function ($root, $args): void {
2 ignored issues
show
Unused Code introduced by
The parameter $args is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

98
                        'resolve' => function ($root, /** @scrutinizer ignore-unused */ $args): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $root is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

98
                        'resolve' => function (/** @scrutinizer ignore-unused */ $root, $args): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
99
                            // create new post and flush...
100
                        },
101
                    ],
102
                    'updatePost' => [
103
                        'type' => Type::nonNull($this->types->get(Post::class)),
104
                        'args' => [
105
                            'id' => Type::nonNull(Type::id()), // Use standard API when needed
106
                            'input' => $this->types->getInput(Post::class),
107
                        ],
108
                        'resolve' => function ($root, $args): void {
2 ignored issues
show
Unused Code introduced by
The parameter $root is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

108
                        'resolve' => function (/** @scrutinizer ignore-unused */ $root, $args): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $args is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

108
                        'resolve' => function ($root, /** @scrutinizer ignore-unused */ $args): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
109
                            // update existing post and flush...
110
                        },
111
                    ],
112
                ],
113
            ]),
114
        ]);
115
116
        $schema->assertValid();
117
        self::assertTrue(true, 'passed validation successfully');
118
    }
119
120
    public function testCanGetUserDefinedScalarTypes(): void
121
    {
122
        $bool = $this->types->get(BooleanType::class);
123
        $status = $this->types->get(PostStatusType::class);
124
125
        self::assertInstanceOf(BooleanType::class, $bool, 'must be a instance of bool');
126
        self::assertInstanceOf(PostStatusType::class, $status, 'must be an instance of post status');
127
128
        self::assertSame($bool, $this->types->get(BooleanType::class), 'must returns the same instance of bool');
129
        self::assertSame($status, $this->types->get(PostStatusType::class), 'must returns the same instance of post status');
130
    }
131
132
    public function testCanGetUserMappedTypes(): void
133
    {
134
        $type = $this->types->get(stdClass::class);
135
136
        self::assertInstanceOf(CustomType::class, $type, 'must be a instance of CustomType');
137
        self::assertSame($type, $this->types->get('customName'));
138
    }
139
140
    public function testCanGetTypesWithBackslashPrefix(): void
141
    {
142
        $type = $this->types->get(stdClass::class);
143
        self::assertSame($type, $this->types->get('\stdClass'));
144
    }
145
146
    public function testCanGetOutputTypes(): void
147
    {
148
        $userType = $this->types->get(User::class);
149
150
        $this->assertObjectType('data/UserOutput.php', $userType);
151
        self::assertSame($userType, $this->types->get(User::class), 'must returns the same instance of user type');
152
153
        $postType = $this->types->get(Post::class);
154
        $this->assertObjectType('data/PostOutput.php', $postType);
155
        self::assertSame($postType, $this->types->get(Post::class), 'must returns the same instance of post type');
156
    }
157
158
    public function testCanGetInputTypes(): void
159
    {
160
        $userType = $this->types->getInput(User::class);
161
        $this->assertInputType('data/UserInput.php', $userType);
162
        self::assertSame($userType, $this->types->getInput(User::class), 'must returns the same instance of user type');
163
164
        $postType = $this->types->getInput(Post::class);
165
        $this->assertInputType('data/PostInput.php', $postType);
166
        self::assertSame($postType, $this->types->getInput(Post::class), 'must returns the same instance of post type');
167
    }
168
169
    private function assertType(string $expectedFile, Type $type, bool $assertArgs): void
170
    {
171
        $fields = [];
172
        foreach ($type->getFields() as $field) {
0 ignored issues
show
Bug introduced by
The method getFields() does not exist on GraphQL\Type\Definition\Type. It seems like you code against a sub-type of GraphQL\Type\Definition\Type such as GraphQL\Type\Definition\InterfaceType or GraphQL\Type\Definition\ObjectType or GraphQL\Type\Definition\InputObjectType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

172
        foreach ($type->/** @scrutinizer ignore-call */ getFields() as $field) {
Loading history...
173
            $data = [
174
                'name' => $field->name,
175
                'type' => $field->getType()->toString(),
176
                'description' => $field->description,
177
            ];
178
179
            if ($assertArgs) {
180
                $args = [];
181
                foreach ($field->args as $arg) {
182
                    $args[] = [
183
                        'name' => $arg->name,
184
                        'type' => $arg->getType()->toString(),
185
                        'description' => $arg->description,
186
                        'defaultValue' => $arg->defaultValue,
187
                    ];
188
                }
189
                $data['args'] = $args;
190
            } else {
191
                $data['defaultValue'] = $field->defaultValue;
192
            }
193
194
            $fields[] = $data;
195
        }
196
197
        $actual = [
198
            'name' => $type->name,
199
            'description' => $type->description,
200
            'fields' => $fields,
201
        ];
202
203
        $expected = require $expectedFile;
204
        self::assertEquals($expected, $actual, 'Should equals expectation from: ' . $expectedFile);
205
    }
206
207
    private function assertInputType(string $expectedFile, InputObjectType $type): void
208
    {
209
        $this->assertType($expectedFile, $type, false);
210
    }
211
212
    private function assertObjectType(string $expectedFile, ObjectType $type): void
213
    {
214
        $this->assertType($expectedFile, $type, true);
215
    }
216
217
    public function testNonPublicGetterMustBeIgnored(): void
218
    {
219
        $actual = $this->types->get(Blog\Model\Special\IgnoredGetter::class);
220
        $this->assertObjectType('data/IgnoredGetter.php', $actual);
221
    }
222
223
    public function testCanDeclareArrayOfEntity(): void
224
    {
225
        $actual = $this->types->get(Blog\Model\Special\ArrayOfEntity::class);
226
        $this->assertObjectType('data/ArrayOfEntity.php', $actual);
227
    }
228
229
    public function testDefaultValuesInput(): void
230
    {
231
        $actual = $this->types->getInput(Blog\Model\Special\DefaultValue::class);
232
        $this->assertInputType('data/DefaultValueInput.php', $actual);
233
    }
234
235
    public function testDefaultValuesPartialInput(): void
236
    {
237
        $actual = $this->types->getPartialInput(Blog\Model\Special\DefaultValue::class);
238
        $this->assertInputType('data/DefaultValuePartialInput.php', $actual);
239
    }
240
241
    public function testDefaultValuesOutput(): void
242
    {
243
        $actual = $this->types->get(Blog\Model\Special\DefaultValue::class);
244
        $this->assertObjectType('data/DefaultValue.php', $actual);
245
    }
246
247
    public function testFieldWithoutTypeMustThrow(): void
248
    {
249
        $this->expectExceptionMessage('Could not find type for method `GraphQLTests\Doctrine\Blog\Model\Special\NoType::getWithoutTypeHint()`. Either type hint the return value, or specify the type with `@API\Field` annotation.');
250
        $type = $this->types->get(Blog\Model\Special\NoType::class);
251
        $type->getFields();
252
    }
253
254
    public function testFieldReturningCollectionWithoutTypeMustThrow(): void
255
    {
256
        $this->expectExceptionMessage('The method `GraphQLTests\Doctrine\Blog\Model\Special\NoTypeCollection::getFoos()` is type hinted with a return type of `Doctrine\Common\Collections\Collection`, but the entity contained in that collection could not be automatically detected. Either fix the type hint, fix the doctrine mapping, or specify the type with `@API\Field` annotation.');
257
        $type = $this->types->get(Blog\Model\Special\NoTypeCollection::class);
258
        $type->getFields();
259
    }
260
261
    public function testCannotGetInvalidType(): void
262
    {
263
        $this->expectExceptionMessage('Given class name `DateTimeImmutable` is not a Doctrine entity. Either register a custom GraphQL type for `DateTimeImmutable` when instantiating `GraphQL\Doctrine\Types`, or change the usage of that class to something else.');
264
        $this->types->get(\DateTimeImmutable::class);
265
    }
266
267
    public function testArgumentWithoutTypeMustThrow(): void
268
    {
269
        $this->expectExceptionMessage('Could not find type for parameter `$bar` for method `GraphQLTests\Doctrine\Blog\Model\Special\NoTypeArgument::getFoo()`. Either type hint the parameter, or specify the type with `@API\Argument` annotation.');
270
        $type = $this->types->get(Blog\Model\Special\NoTypeArgument::class);
271
        $type->getFields();
272
    }
273
274
    public function testInputWithoutTypeMustThrow(): void
275
    {
276
        $this->expectExceptionMessage('Could not find type for parameter `$bar` for method `GraphQLTests\Doctrine\Blog\Model\Special\NoTypeInput::setFoo()`. Either type hint the parameter, or specify the type with `@API\Input` annotation.');
277
        $type = $this->types->getInput(Blog\Model\Special\NoTypeInput::class);
278
        $type->getFields();
279
    }
280
281
    public function testFieldWithExtraArgumentMustThrow(): void
282
    {
283
        $this->expectExceptionMessage('The following arguments were declared via `@API\Argument` annotation but do not match actual parameter names on method `GraphQLTests\Doctrine\Blog\Model\Special\ExtraArgument::getWithParams()`. Either rename or remove the annotations: misspelled_name');
284
        $type = $this->types->get(Blog\Model\Special\ExtraArgument::class);
285
        $type->getFields();
286
    }
287
288
    public function testFieldWithArrayArgumentMustThrow(): void
289
    {
290
        $this->expectExceptionMessage('The parameter `$arg1` on method `GraphQLTests\Doctrine\Blog\Model\Special\ArrayArgument::getWithParams()` is type hinted as `array` and is not overridden via `@API\Argument` annotation. Either change the type hint or specify the type with `@API\Argument` annotation.');
291
        $type = $this->types->get(Blog\Model\Special\ArrayArgument::class);
292
        $type->getFields();
293
    }
294
295
    public function testFieldWithObjectTypeArgumentMustThrow(): void
296
    {
297
        $this->expectExceptionMessage('Type for parameter `$user` for method `GraphQLTests\Doctrine\Blog\Model\Special\ObjectTypeArgument::getWithParams()` must be an instance of `GraphQL\Type\Definition\InputType`, but was `GraphQL\Type\Definition\ObjectType`. Use `@API\Argument` annotation to specify a custom InputType.');
298
        $type = $this->types->get(Blog\Model\Special\ObjectTypeArgument::class);
299
        $type->getFields();
300
    }
301
302
    public function testCanGetMappedTypesEitherByMappedPhpClassOrDirectTypeClass(): void
303
    {
304
        $viaPhp = $this->types->get(DateTime::class);
305
        $viaType = $this->types->get(DateTimeType::class);
306
        self::assertSame($viaPhp, $viaType);
307
    }
308
309
    public function testFeieldWithObjectTypeArgumentMustThrow(): void
310
    {
311
        // Replace annotation driver with a driver chain
312
        $config = $this->entityManager->getConfiguration();
313
        $chain = new MappingDriverChain();
314
        $chain->setDefaultDriver($config->getMetadataDriverImpl());
315
        $config->setMetadataDriverImpl($chain);
316
317
        $type = $this->types->get(Post::class);
318
319
        $this->expectExceptionMessage('graphql-doctrine requires Doctrine to be configured with a Doctrine\Common\Persistence\Mapping\Driver\AnnotationDriver');
320
        $type->getFields();
321
    }
322
}
323