Skip to content

Commit 1235a0d

Browse files
committed
- Implement the oneOf directive tests, following the graphql-js implementation
1 parent 66b0dda commit 1235a0d

File tree

5 files changed

+230
-5
lines changed

5 files changed

+230
-5
lines changed

tests/Type/IntrospectionTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,17 @@ public function testExecutesAnIntrospectionQuery(): void
365365
'isDeprecated' => false,
366366
'deprecationReason' => null,
367367
],
368+
9 => [
369+
'name' => 'isOneOf',
370+
'args' => [],
371+
'type' => [
372+
'kind' => 'SCALAR',
373+
'name' => 'Boolean',
374+
'ofType' => null,
375+
],
376+
'isDeprecated' => false,
377+
'deprecationReason' => null,
378+
],
368379
],
369380
'inputFields' => null,
370381
'interfaces' => [],
@@ -962,6 +973,14 @@ public function testExecutesAnIntrospectionQuery(): void
962973
3 => 'INPUT_FIELD_DEFINITION',
963974
],
964975
],
976+
[
977+
'name' => 'oneOf',
978+
'args' => [],
979+
'isRepeatable' => false,
980+
'locations' => [
981+
0 => 'INPUT_OBJECT',
982+
],
983+
],
965984
],
966985
],
967986
],
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace GraphQL\Tests\Type;
4+
5+
use GraphQL\Error\InvariantViolation;
6+
use GraphQL\GraphQL;
7+
use GraphQL\Type\Definition\InputObjectType;
8+
use GraphQL\Type\Definition\ObjectType;
9+
use GraphQL\Type\Definition\Type;
10+
use GraphQL\Type\Schema;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class OneOfInputObjectTest extends TestCase
14+
{
15+
public function testOneOfInputObjectBasicDefinition(): void
16+
{
17+
$oneOfInput = new InputObjectType([
18+
'name' => 'OneOfInput',
19+
'isOneOf' => true,
20+
'fields' => [
21+
'stringField' => Type::string(),
22+
'intField' => Type::int(),
23+
],
24+
]);
25+
26+
self::assertTrue($oneOfInput->isOneOf());
27+
self::assertCount(2, $oneOfInput->getFields());
28+
}
29+
30+
public function testOneOfInputObjectValidation(): void
31+
{
32+
$oneOfInput = new InputObjectType([
33+
'name' => 'OneOfInput',
34+
'isOneOf' => true,
35+
'fields' => [
36+
'stringField' => Type::string(),
37+
'intField' => Type::int(),
38+
],
39+
]);
40+
41+
// Should not throw for valid oneOf input
42+
$oneOfInput->assertValid();
43+
self::addToAssertionCount(1); // Test passes if no exception thrown
44+
}
45+
46+
public function testOneOfInputObjectRejectsNonNullFields(): void
47+
{
48+
$this->expectException(InvariantViolation::class);
49+
$this->expectExceptionMessage('OneOf input object type OneOfInput field stringField must be nullable');
50+
51+
$oneOfInput = new InputObjectType([
52+
'name' => 'OneOfInput',
53+
'isOneOf' => true,
54+
'fields' => [
55+
'stringField' => Type::nonNull(Type::string()), // This should fail
56+
'intField' => Type::int(),
57+
],
58+
]);
59+
60+
$oneOfInput->assertValid();
61+
}
62+
63+
public function testOneOfInputObjectRejectsDefaultValues(): void
64+
{
65+
$this->expectException(InvariantViolation::class);
66+
$this->expectExceptionMessage('OneOf input object type OneOfInput field stringField cannot have a default value');
67+
68+
$oneOfInput = new InputObjectType([
69+
'name' => 'OneOfInput',
70+
'isOneOf' => true,
71+
'fields' => [
72+
'stringField' => [
73+
'type' => Type::string(),
74+
'defaultValue' => 'default', // This should fail
75+
],
76+
'intField' => Type::int(),
77+
],
78+
]);
79+
80+
$oneOfInput->assertValid();
81+
}
82+
83+
public function testOneOfInputObjectSchemaValidation(): void
84+
{
85+
$oneOfInput = new InputObjectType([
86+
'name' => 'OneOfInput',
87+
'isOneOf' => true,
88+
'fields' => [
89+
'stringField' => Type::string(),
90+
'intField' => Type::int(),
91+
],
92+
]);
93+
94+
$query = new ObjectType([
95+
'name' => 'Query',
96+
'fields' => [
97+
'test' => [
98+
'type' => Type::string(),
99+
'args' => [
100+
'input' => $oneOfInput,
101+
],
102+
'resolve' => static fn ($source, $args) => 'test',
103+
],
104+
],
105+
]);
106+
107+
$schema = new Schema(['query' => $query]);
108+
109+
// Valid query with exactly one field
110+
$validQuery = '{ test(input: { stringField: "hello" }) }';
111+
$result = GraphQL::executeQuery($schema, $validQuery);
112+
self::assertEmpty($result->errors ?? []);
113+
114+
// Invalid query with multiple fields
115+
$invalidQuery = '{ test(input: { stringField: "hello", intField: 42 }) }';
116+
$result = GraphQL::executeQuery($schema, $invalidQuery);
117+
self::assertNotEmpty($result->errors ?? []);
118+
self::assertCount(1, $result->errors);
119+
self::assertStringContainsString('must specify exactly one field', $result->errors[0]->getMessage());
120+
121+
// Invalid query with no fields
122+
$emptyQuery = '{ test(input: {}) }';
123+
$result = GraphQL::executeQuery($schema, $emptyQuery);
124+
self::assertNotEmpty($result->errors ?? []);
125+
self::assertCount(1, $result->errors);
126+
self::assertStringContainsString('must specify exactly one field', $result->errors[0]->getMessage());
127+
}
128+
129+
public function testOneOfIntrospection(): void
130+
{
131+
$oneOfInput = new InputObjectType([
132+
'name' => 'OneOfInput',
133+
'isOneOf' => true,
134+
'fields' => [
135+
'stringField' => Type::string(),
136+
'intField' => Type::int(),
137+
],
138+
]);
139+
140+
$regularInput = new InputObjectType([
141+
'name' => 'RegularInput',
142+
'fields' => [
143+
'stringField' => Type::string(),
144+
'intField' => Type::int(),
145+
],
146+
]);
147+
148+
$query = new ObjectType([
149+
'name' => 'Query',
150+
'fields' => [
151+
'test' => [
152+
'type' => Type::string(),
153+
'resolve' => static fn () => 'test',
154+
],
155+
],
156+
]);
157+
158+
$schema = new Schema([
159+
'query' => $query,
160+
'types' => [$oneOfInput, $regularInput],
161+
]);
162+
163+
$introspectionQuery = '
164+
{
165+
__schema {
166+
types {
167+
name
168+
isOneOf
169+
}
170+
}
171+
}
172+
';
173+
174+
$result = GraphQL::executeQuery($schema, $introspectionQuery);
175+
self::assertEmpty($result->errors ?? []);
176+
177+
$types = $result->data['__schema']['types'] ?? [];
178+
$oneOfType = null;
179+
$regularType = null;
180+
181+
foreach ($types as $type) {
182+
if ($type['name'] === 'OneOfInput') {
183+
$oneOfType = $type;
184+
} elseif ($type['name'] === 'RegularInput') {
185+
$regularType = $type;
186+
}
187+
}
188+
189+
self::assertNotNull($oneOfType);
190+
self::assertNotNull($regularType);
191+
self::assertTrue($oneOfType['isOneOf']);
192+
self::assertFalse($regularType['isOneOf']); // Should be false for regular input objects
193+
}
194+
}

tests/Utils/BreakingChangesFinderTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,13 +1333,18 @@ public function testShouldDetectIfADirectiveWasImplicitlyRemoved(): void
13331333
]);
13341334

13351335
$deprecatedDirective = Directive::deprecatedDirective();
1336+
$oneOfDirective = Directive::oneOfDirective();
13361337

13371338
self::assertEquals(
13381339
[
13391340
[
13401341
'type' => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_REMOVED,
13411342
'description' => "{$deprecatedDirective->name} was removed",
13421343
],
1344+
[
1345+
'type' => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_REMOVED,
1346+
'description' => "{$oneOfDirective->name} was removed",
1347+
],
13431348
],
13441349
BreakingChangesFinder::findRemovedDirectives($oldSchema, $newSchema)
13451350
);

tests/Utils/BuildSchemaTest.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -272,11 +272,12 @@ public function testMaintainsIncludeSkipAndSpecifiedBy(): void
272272
{
273273
$schema = BuildSchema::buildAST(Parser::parse('type Query'));
274274

275-
// TODO switch to 4 when adding @specifiedBy - see https://github.com/webonyx/graphql-php/issues/1140
276-
self::assertCount(3, $schema->getDirectives());
275+
// TODO switch to 5 when adding @specifiedBy - see https://github.com/webonyx/graphql-php/issues/1140
276+
self::assertCount(4, $schema->getDirectives());
277277
self::assertSame(Directive::skipDirective(), $schema->getDirective('skip'));
278278
self::assertSame(Directive::includeDirective(), $schema->getDirective('include'));
279279
self::assertSame(Directive::deprecatedDirective(), $schema->getDirective('deprecated'));
280+
self::assertSame(Directive::oneOfDirective(), $schema->getDirective('oneOf'));
280281

281282
self::markTestIncomplete('See https://github.com/webonyx/graphql-php/issues/1140');
282283
self::assertSame(Directive::specifiedByDirective(), $schema->getDirective('specifiedBy'));
@@ -292,10 +293,11 @@ public function testOverridingDirectivesExcludesSpecified(): void
292293
directive @specifiedBy on FIELD_DEFINITION
293294
'));
294295

295-
self::assertCount(4, $schema->getDirectives());
296+
self::assertCount(5, $schema->getDirectives());
296297
self::assertNotEquals(Directive::skipDirective(), $schema->getDirective('skip'));
297298
self::assertNotEquals(Directive::includeDirective(), $schema->getDirective('include'));
298299
self::assertNotEquals(Directive::deprecatedDirective(), $schema->getDirective('deprecated'));
300+
self::assertSame(Directive::oneOfDirective(), $schema->getDirective('oneOf'));
299301

300302
self::markTestIncomplete('See https://github.com/webonyx/graphql-php/issues/1140');
301303
self::assertNotEquals(Directive::specifiedByDirective(), $schema->getDirective('specifiedBy'));
@@ -310,12 +312,13 @@ public function testAddingDirectivesMaintainsIncludeSkipAndSpecifiedBy(): void
310312
GRAPHQL;
311313
$schema = BuildSchema::buildAST(Parser::parse($sdl));
312314

313-
// TODO switch to 5 when adding @specifiedBy - see https://github.com/webonyx/graphql-php/issues/1140
314-
self::assertCount(4, $schema->getDirectives());
315+
// TODO switch to 6 when adding @specifiedBy - see https://github.com/webonyx/graphql-php/issues/1140
316+
self::assertCount(5, $schema->getDirectives());
315317
self::assertNotNull($schema->getDirective('foo'));
316318
self::assertNotNull($schema->getDirective('skip'));
317319
self::assertNotNull($schema->getDirective('include'));
318320
self::assertNotNull($schema->getDirective('deprecated'));
321+
self::assertNotNull($schema->getDirective('oneOf'));
319322

320323
self::markTestIncomplete('See https://github.com/webonyx/graphql-php/issues/1140');
321324
self::assertNotNull($schema->getDirective('specifiedBy'));

tests/Utils/SchemaPrinterTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,9 @@ public function testPrintIntrospectionSchema(): void
10111011
reason: String = "No longer supported"
10121012
) on FIELD_DEFINITION | ENUM_VALUE | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
10131013
1014+
"Indicates that an input object is a oneof input object and exactly one of the input fields must be specified."
1015+
directive @oneOf on INPUT_OBJECT
1016+
10141017
"A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations."
10151018
type __Schema {
10161019
"A list of all types supported by this server."
@@ -1044,6 +1047,7 @@ interfaces: [__Type!]
10441047
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
10451048
inputFields(includeDeprecated: Boolean = false): [__InputValue!]
10461049
ofType: __Type
1050+
isOneOf: Boolean
10471051
}
10481052
10491053
"An enum describing what kind of type a given `__Type` is."

0 commit comments

Comments
 (0)