Skip to content

Commit d8893df

Browse files
KoditkarVedantjoemcbride
authored andcommitted
999 validation rule subscription operations must have exactly one root field (graphql-dotnet#1000)
* add validation rule: Single root field per subscription operation. * add test cases for Single root field per subscription operation rule * add skipping test case to check single root field with fragment spread * update SingleRootField per subsciption rule to validate fragment spread * add test cases to validate fragment spread in SingleRootFieldSubscriptions rule * update readme.md
1 parent 22eca05 commit d8893df

File tree

4 files changed

+268
-0
lines changed

4 files changed

+268
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ var json = schema.Execute(_ =>
193193
- [x] Unique variable names
194194
- [x] Variables are input types
195195
- [x] Variables in allowed position
196+
- [x] Single root field
196197

197198
### Schema Introspection
198199
- [x] __typename
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
namespace GraphQL.Tests.Validation
2+
{
3+
using GraphQL.Validation.Rules;
4+
using Xunit;
5+
6+
public class SingleRootFieldSubscriptionsTests
7+
: ValidationTestBase<SingleRootFieldSubscriptions, ValidationSchema>
8+
{
9+
[Fact]
10+
public void No_operations_should_pass()
11+
{
12+
ShouldPassRule("fragment fragA on Type { field }");
13+
}
14+
15+
[Fact]
16+
public void Anonymous_query_operation_should_pass()
17+
{
18+
ShouldPassRule("{ field1 }");
19+
}
20+
21+
[Fact]
22+
public void Anonymous_subscription_operation_with_single_root_field_should_pass()
23+
{
24+
ShouldPassRule("subscription { field }");
25+
}
26+
27+
[Fact]
28+
public void One_named_subscription_operation_with_single_root_field_should_pass()
29+
{
30+
ShouldPassRule("subscription { field }");
31+
}
32+
33+
[Fact]
34+
public void Fails_with_more_than_one_root_field_in_anonymous_subscription()
35+
{
36+
string query = @"
37+
subscription {
38+
field
39+
field2
40+
}
41+
";
42+
43+
ShouldFailRule(config =>
44+
{
45+
config.Query = query;
46+
config.Error(SingleRootFieldSubscriptions.InvalidNumberOfRootFieldMessaage(null), 4, 21);
47+
});
48+
}
49+
50+
[Fact]
51+
public void Fails_with_more_than_one_root_field_including_introspection_in_anonymous_subscription()
52+
{
53+
string query = @"
54+
subscription {
55+
field
56+
__typename
57+
}
58+
";
59+
60+
ShouldFailRule(config =>
61+
{
62+
config.Query = query;
63+
config.Error(SingleRootFieldSubscriptions.InvalidNumberOfRootFieldMessaage(null), 4, 21);
64+
});
65+
}
66+
67+
[Fact]
68+
public void Fails_with_more_than_one_root_field()
69+
{
70+
const string subscriptionName = "NamedSubscription";
71+
const string query = @"
72+
subscription NamedSubscription {
73+
field
74+
field2
75+
}
76+
";
77+
78+
ShouldFailRule(config =>
79+
{
80+
config.Query = query;
81+
config.Error(SingleRootFieldSubscriptions.InvalidNumberOfRootFieldMessaage(subscriptionName), 4, 21);
82+
});
83+
}
84+
85+
[Fact]
86+
public void Fails_with_more_than_one_root_field_including_introspection()
87+
{
88+
const string subscriptionName = "NamedSubscription";
89+
const string query = @"
90+
subscription NamedSubscription {
91+
field
92+
__typename
93+
}
94+
";
95+
96+
ShouldFailRule(config =>
97+
{
98+
config.Query = query;
99+
config.Error(SingleRootFieldSubscriptions.InvalidNumberOfRootFieldMessaage(subscriptionName), 4, 21);
100+
});
101+
}
102+
103+
[Fact]
104+
public void Fails_with_more_than_one_root_field_in_fragment_spead()
105+
{
106+
const string subscriptionName = "NamedSubscription";
107+
const string query = @"
108+
subscription NamedSubscription {
109+
...newMessageFields
110+
}
111+
112+
fragment newMessageFields on Subscription {
113+
newMessage {
114+
body
115+
sender
116+
}
117+
disallowedSecondRootField
118+
}
119+
";
120+
121+
ShouldFailRule(config =>
122+
{
123+
config.Query = query;
124+
config.Error(SingleRootFieldSubscriptions.InvalidNumberOfRootFieldMessaage(subscriptionName), 3, 21);
125+
});
126+
}
127+
128+
[Fact]
129+
public void Fails_with_more_than_one_root_field_in_inline_fragment()
130+
{
131+
const string subscriptionName = "NamedSubscription";
132+
const string query = @"
133+
subscription NamedSubscription {
134+
...on Subscription {
135+
newMessage {
136+
body
137+
sender
138+
}
139+
disallowedSecondRootField
140+
}
141+
}
142+
";
143+
144+
ShouldFailRule(config =>
145+
{
146+
config.Query = query;
147+
config.Error(SingleRootFieldSubscriptions.InvalidNumberOfRootFieldMessaage(subscriptionName), 3, 21);
148+
});
149+
}
150+
151+
[Fact]
152+
public void Pass_with_one_root_field_in_fragment_spead()
153+
{
154+
const string query = @"
155+
subscription NamedSubscription {
156+
...newMessageFields
157+
}
158+
159+
fragment newMessageFields on Subscription {
160+
newMessage {
161+
body
162+
sender
163+
}
164+
}
165+
";
166+
167+
ShouldPassRule(query);
168+
}
169+
170+
[Fact]
171+
public void Pass_with_one_root_field_in_inline_fragment()
172+
{
173+
const string query = @"
174+
subscription NamedSubscription {
175+
...on Subscription {
176+
newMessage {
177+
body
178+
sender
179+
}
180+
}
181+
}
182+
";
183+
184+
ShouldPassRule(query);
185+
}
186+
}
187+
}

src/GraphQL/Validation/DocumentValidator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public static List<IValidationRule> CoreRules()
6969
{
7070
new UniqueOperationNames(),
7171
new LoneAnonymousOperation(),
72+
new SingleRootFieldSubscriptions(),
7273
new KnownTypeNames(),
7374
new FragmentsOnCompositeTypes(),
7475
new VariablesAreInputTypes(),
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
namespace GraphQL.Validation.Rules
2+
{
3+
using GraphQL.Language.AST;
4+
using System.Linq;
5+
6+
/// <summary>
7+
/// Subscription operations must have exactly one root field.
8+
/// </summary>
9+
public class SingleRootFieldSubscriptions : IValidationRule
10+
{
11+
private static readonly string RuleCode = "5.2.3.1";
12+
13+
public INodeVisitor Validate(ValidationContext context)
14+
{
15+
return new EnterLeaveListener(config =>
16+
{
17+
config.Match<Operation>(operation =>
18+
{
19+
if (!IsSubscription(operation))
20+
{
21+
return;
22+
}
23+
24+
int rootFields = operation.SelectionSet.Selections.Count();
25+
26+
if (rootFields != 1)
27+
{
28+
context.ReportError(
29+
new ValidationError(
30+
context.OriginalQuery,
31+
RuleCode,
32+
InvalidNumberOfRootFieldMessaage(operation.Name),
33+
operation.SelectionSet.Selections.Skip(1).ToArray()));
34+
}
35+
36+
var fragment = operation.SelectionSet.Selections.FirstOrDefault(IsFragment);
37+
38+
if(fragment == null )
39+
{
40+
return;
41+
}
42+
43+
if(fragment is FragmentSpread fragmentSpread)
44+
{
45+
var fragmentDefinition = context.GetFragment(fragmentSpread.Name);
46+
rootFields = fragmentDefinition.SelectionSet.Selections.Count;
47+
}
48+
else if(fragment is InlineFragment fragmentSelectionSet)
49+
{
50+
rootFields = fragmentSelectionSet.SelectionSet.Selections.Count;
51+
}
52+
53+
if (rootFields != 1)
54+
{
55+
context.ReportError(
56+
new ValidationError(
57+
context.OriginalQuery,
58+
RuleCode,
59+
InvalidNumberOfRootFieldMessaage(operation.Name),
60+
fragment));
61+
}
62+
63+
});
64+
});
65+
}
66+
67+
public static string InvalidNumberOfRootFieldMessaage(string name)
68+
{
69+
string prefix = name != null ? $"Subscription '{name}'" : "Anonymous Subscription";
70+
return $"{prefix} must select only one top level field.";
71+
}
72+
73+
private static bool IsSubscription(Operation operation) =>
74+
operation.OperationType == OperationType.Subscription;
75+
76+
private static bool IsFragment(ISelection selection) =>
77+
(selection is FragmentSpread) || (selection is InlineFragment);
78+
}
79+
}

0 commit comments

Comments
 (0)