diff --git a/execution/engine/federation_integration_test.go b/execution/engine/federation_integration_test.go index baaa60ff50..1b867bb370 100644 --- a/execution/engine/federation_integration_test.go +++ b/execution/engine/federation_integration_test.go @@ -364,6 +364,26 @@ func TestFederationIntegrationTest(t *testing.T) { assert.Len(t, resp, 0) }) + t.Run("empty fragment", func(t *testing.T) { + setup := federationtesting.NewFederationSetup(addGateway(false)) + t.Cleanup(setup.Close) + gqlClient := NewGraphqlClient(http.DefaultClient) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + resp := gqlClient.QueryStatusCode(ctx, setup.GatewayServer.URL, testQueryPath("queries/empty_fragment.graphql"), nil, http.StatusInternalServerError, t) + assert.Len(t, resp, 0) + }) + + t.Run("empty fragment variant", func(t *testing.T) { + setup := federationtesting.NewFederationSetup(addGateway(false)) + t.Cleanup(setup.Close) + gqlClient := NewGraphqlClient(http.DefaultClient) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + resp := gqlClient.QueryStatusCode(ctx, setup.GatewayServer.URL, testQueryPath("queries/empty_fragment_variant.graphql"), nil, http.StatusInternalServerError, t) + assert.Len(t, resp, 0) + }) + t.Run("Union response type with interface fragments", func(t *testing.T) { setup := federationtesting.NewFederationSetup(addGateway(false)) t.Cleanup(setup.Close) diff --git a/execution/federationtesting/testdata/queries/empty_fragment.graphql b/execution/federationtesting/testdata/queries/empty_fragment.graphql new file mode 100644 index 0000000000..4a2677b2f2 --- /dev/null +++ b/execution/federationtesting/testdata/queries/empty_fragment.graphql @@ -0,0 +1,5 @@ +fragment A on Query {} + +{ + ...A +} \ No newline at end of file diff --git a/execution/federationtesting/testdata/queries/empty_fragment_variant.graphql b/execution/federationtesting/testdata/queries/empty_fragment_variant.graphql new file mode 100644 index 0000000000..4b173ab583 --- /dev/null +++ b/execution/federationtesting/testdata/queries/empty_fragment_variant.graphql @@ -0,0 +1,9 @@ +fragment A on Query { + ... { + + } +} + +{ + ...A +} \ No newline at end of file diff --git a/v2/pkg/ast/ast_operation_definition_test.go b/v2/pkg/ast/ast_operation_definition_test.go index 2deaec8221..1ba98b0cad 100644 --- a/v2/pkg/ast/ast_operation_definition_test.go +++ b/v2/pkg/ast/ast_operation_definition_test.go @@ -30,19 +30,19 @@ func TestDocument_OperationNameExists(t *testing.T) { )) t.Run("found on document with a single operations", run( - "query MyOperation {}", + "query MyOperation {other}", "MyOperation", true, )) t.Run("found on document with multiple operations", run( - "query OtherOperation {other} query AnotherOperation {another} query MyOperation {}", + "query OtherOperation {other} query AnotherOperation {another} query MyOperation {other}", "MyOperation", true, )) t.Run("found on a document with preceding root nodes of not operation type", run( - "fragment F on T {field} query MyOperation {}", + "fragment F on T {field} query MyOperation {another}", "MyOperation", true, )) diff --git a/v2/pkg/astparser/parser.go b/v2/pkg/astparser/parser.go index 34403da121..b950b4aa2f 100644 --- a/v2/pkg/astparser/parser.go +++ b/v2/pkg/astparser/parser.go @@ -1311,7 +1311,8 @@ func (p *Parser) parseSelectionSet() (int, bool) { set.RBrace = rbraceToken.TextPosition if len(set.SelectionRefs) == 0 { - return 0, false + p.errUnexpectedToken(rbraceToken, keyword.IDENT, keyword.SPREAD) + return ast.InvalidRef, false } p.document.SelectionSets = append(p.document.SelectionSets, set) diff --git a/v2/pkg/astparser/parser_test.go b/v2/pkg/astparser/parser_test.go index 2c6d87235e..d651adbd7f 100644 --- a/v2/pkg/astparser/parser_test.go +++ b/v2/pkg/astparser/parser_test.go @@ -778,27 +778,25 @@ func TestParser_Parse(t *testing.T) { """" Object Type BlockString to ignore """ - extend type Person { + extend type Person { a: Int } """" Interface Type BlockString to ignore """ - extend interface NamedEntity { + extend interface NamedEntity { a: Int } """" Scalar Type BlockString to ignore """ - extend scalar JSON { - } + extend scalar JSON """" Union Type BlockString to ignore """ - extend union SearchResult = Photo | Person { - } + extend union SearchResult = Photo | Person """" Input Type BlockString to ignore """ - extend input NamedEntity { + extend input NamedEntity { a: Int }`, parse, false) }) t.Run("scalar type definition", func(t *testing.T) { @@ -2233,18 +2231,18 @@ func TestParser_Parse(t *testing.T) { mutation: Mutation subscription: Subscription } - scalar Scalar1 {} - scalar Scalar2 {} - type Type1 {} - type Type2 {} - interface Iface1 {} - interface Iface2 {} - input Input1 {} - input Input2 {} + scalar Scalar1 + scalar Scalar2 + type Type1 { a: Int } + type Type2 { a: Int } + interface Iface1 { a: Int } + interface Iface2 { a: Int } + input Input1 { a: Int } + input Input2 { a: Int } union Union1 union Union2 - enum Enum1 {} - enum Enum2 {} + enum Enum1 { A } + enum Enum2 { B } directive @directive1 on FIELD directive @directive2 on FIELD `, parse, false, @@ -2586,6 +2584,21 @@ func TestErrorReport(t *testing.T) { t.Fatalf("want:\n%s\ngot:\n%s\n", want, report.Error()) } }) + t.Run("empty fragment", func(t *testing.T) { + _, report := ParseGraphqlDocumentString(` + fragment A on Query {} + query { ...A } + `) + + if !report.HasErrors() { + t.Fatalf("want err, got nil") + } + + want := "external: unexpected token - got: RBRACE want one of: [IDENT SPREAD], locations: [{Line:2 Column:25}], path: []" + if report.Error() != want { + t.Fatalf("want:\n%s\ngot:\n%s\n", want, report.Error()) + } + }) } func TestParseStarwars(t *testing.T) { diff --git a/v2/pkg/astvalidation/operation_validation_test.go b/v2/pkg/astvalidation/operation_validation_test.go index 76f12a1957..5b9ee14cf8 100644 --- a/v2/pkg/astvalidation/operation_validation_test.go +++ b/v2/pkg/astvalidation/operation_validation_test.go @@ -3385,14 +3385,6 @@ type Query { }`, DirectivesAreInValidLocations(), Invalid) }) - t.Run("150 variant", func(t *testing.T) { - run(t, ` - { - ...frag @spread - } - fragment frag on Query {}`, - DirectivesAreInValidLocations(), Valid, withDisableNormalization()) - }) t.Run("150 variant", func(t *testing.T) { run(t, ` { ... { @@ -3409,13 +3401,6 @@ type Query { }`, DirectivesAreInValidLocations(), Invalid) }) - t.Run("150 variant", func(t *testing.T) { - run(t, ` { - ...frag - } - fragment frag on Query @fragmentDefinition {}`, - DirectivesAreInValidLocations(), Valid, withDisableNormalization()) - }) t.Run("150 variant", func(t *testing.T) { run(t, ` query @onQuery { dog @@ -3548,15 +3533,15 @@ type Query { VariablesAreInputTypes(), Valid) }) t.Run("157", func(t *testing.T) { - run(t, `query takesCat($cat: Cat) {}`, + run(t, `query takesCat($cat: Cat) { a }`, VariablesAreInputTypes(), Invalid) - run(t, `query takesDogBang($dog: Dog!) {}`, + run(t, `query takesDogBang($dog: Dog!) { a }`, VariablesAreInputTypes(), Invalid) - run(t, `query takesListOfPet($pets: [Pet]) {}`, + run(t, `query takesListOfPet($pets: [Pet]) { a }`, VariablesAreInputTypes(), Invalid) - run(t, `query takesCatOrDog($catOrDog: CatOrDog) {}`, + run(t, `query takesCatOrDog($catOrDog: CatOrDog) { a }`, VariablesAreInputTypes(), Invalid) - run(t, `query takesCatOrDog($catCommand: CatCommand) {}`, + run(t, `query takesCatOrDog($catCommand: CatCommand) { a }`, VariablesAreInputTypes(), Valid) }) }) diff --git a/v2/pkg/graphqljsonschema/jsonschema_test.go b/v2/pkg/graphqljsonschema/jsonschema_test.go index 39a37293a8..bb8f7b244a 100644 --- a/v2/pkg/graphqljsonschema/jsonschema_test.go +++ b/v2/pkg/graphqljsonschema/jsonschema_test.go @@ -50,7 +50,7 @@ func runTest(schema, operation, expectedJsonSchema string, valid []string, inval func TestJsonSchema(t *testing.T) { t.Run("object", runTest( `scalar String input Test { str: String }`, - `query ($input: Test){}`, + `query ($input: Test){ a }`, `{"type":["object","null"],"properties":{"str":{"type":["string","null"]}},"additionalProperties":false}`, []string{ `{"str":"validString"}`, @@ -62,7 +62,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("string", runTest( `scalar String input Test { str: String }`, - `query ($input: String){}`, + `query ($input: String){ a }`, `{"type":["string","null"]}`, []string{ `"validString"`, @@ -90,7 +90,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("id", runTest( `scalar ID input Test { str: String }`, - `query ($input: ID){}`, + `query ($input: ID){ a }`, `{"type":["string","integer","null"]}`, []string{ `"validString"`, @@ -104,7 +104,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("array", runTest( `scalar String`, - `query ($input: [String]){}`, + `query ($input: [String]){ a }`, `{"type":["array","null"],"items":{"type":["string","null"]}}`, []string{ `null`, @@ -120,7 +120,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("input object array", runTest( `scalar String input StringInput { str: String }`, - `query ($input: [StringInput]){}`, + `query ($input: [StringInput]){ a }`, `{"type":["array","null"],"items":{"anyOf": [{"type":["null"]},{"$ref":"#/$defs/StringInput"}]},"$defs":{"StringInput":{"type":["object","null"],"properties":{"str":{"type":["string","null"]}},"additionalProperties":false}}}`, []string{ `null`, @@ -136,7 +136,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("nested input object both as required and not required", runTest( `scalar String input StringInput { str: String } input Input { requireInput: StringInput! optionalInput: StringInput }`, - `query ($input: Input){}`, + `query ($input: Input){ a }`, `{"type":["object","null"],"properties":{"optionalInput":{"anyOf":[{"type":["null"]},{"$ref":"#/$defs/StringInput"}]},"requireInput":{"$ref":"#/$defs/StringInputNotNull"}},"required":["requireInput"],"additionalProperties":false,"$defs":{"StringInput":{"type":["object","null"],"properties":{"str":{"type":["string","null"]}},"additionalProperties":false},"StringInputNotNull":{"type":["object"],"properties":{"str":{"type":["string","null"]}},"additionalProperties":false}}}`, []string{ `{"optionalInput": {}, "requireInput": {"str":"validString"}}`, @@ -149,7 +149,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("nested input object both as required and not required - reverse order", runTest( `scalar String input StringInput { str: String } input Input { optionalInput: StringInput requireInput: StringInput! }`, - `query ($input: Input){}`, + `query ($input: Input){ a }`, `{"type":["object","null"],"properties":{"optionalInput":{"anyOf":[{"type":["null"]},{"$ref":"#/$defs/StringInput"}]},"requireInput":{"$ref":"#/$defs/StringInputNotNull"}},"required":["requireInput"],"additionalProperties":false,"$defs":{"StringInput":{"type":["object","null"],"properties":{"str":{"type":["string","null"]}},"additionalProperties":false},"StringInputNotNull":{"type":["object"],"properties":{"str":{"type":["string","null"]}},"additionalProperties":false}}}`, []string{ `{"optionalInput": {}, "requireInput": {"str":"validString"}}`, @@ -162,7 +162,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("optional nested input object used twice", runTest( `scalar String input StringInput { str: String } input Input { optionalInput1: StringInput optionalInput2: StringInput }`, - `query ($input: Input){}`, + `query ($input: Input){ a }`, `{"type":["object","null"],"properties":{"optionalInput1":{"anyOf":[{"type":["null"]},{"$ref":"#/$defs/StringInput"}]},"optionalInput2":{"anyOf":[{"type":["null"]},{"$ref":"#/$defs/StringInput"}]}},"additionalProperties":false,"$defs":{"StringInput":{"type":["object","null"],"properties":{"str":{"type":["string","null"]}},"additionalProperties":false}}}`, []string{ `{"optionalInput1": {}, "optionalInput2": {"str":"validString"}}`, @@ -173,7 +173,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("required nested input object used twice", runTest( `scalar String input StringInput { str: String } input Input { requireInput1: StringInput! requireInput2: StringInput! }`, - `query ($input: Input){}`, + `query ($input: Input){ a }`, `{"type":["object","null"],"properties":{"requireInput1":{"$ref":"#/$defs/StringInputNotNull"},"requireInput2":{"$ref":"#/$defs/StringInputNotNull"}},"required":["requireInput1","requireInput2"],"additionalProperties":false,"$defs":{"StringInputNotNull":{"type":["object"],"properties":{"str":{"type":["string","null"]}},"additionalProperties":false}}}`, []string{ `{"requireInput1": {"str":"validString"}, "requireInput2": {"str":"validString"}}`, @@ -185,7 +185,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("required array", runTest( `scalar String`, - `query ($input: [String]!){}`, + `query ($input: [String]!){ a }`, `{"type":["array"],"items":{"type":["string","null"]}}`, []string{ `[]`, @@ -201,7 +201,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("required array element", runTest( `scalar String`, - `query ($input: [String!]){}`, + `query ($input: [String!]){ a }`, `{"type":["array","null"],"items":{"type":["string"]}}`, []string{ `null`, @@ -218,7 +218,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("nested object", runTest( `scalar String scalar Boolean input Test { str: String! nested: Nested } input Nested { boo: Boolean }`, - `query ($input: Test){}`, + `query ($input: Test){ a }`, `{"type":["object","null"],"properties":{"nested":{"anyOf":[{"type":["null"]},{"$ref":"#/$defs/Nested"}]},"str":{"type":["string"]}},"required":["str"],"additionalProperties":false,"$defs":{"Nested":{"type":["object","null"],"properties":{"boo":{"type":["boolean","null"]}},"additionalProperties":false}}}`, []string{ `null`, @@ -237,7 +237,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("nested object with override", runTest( `scalar String scalar Boolean input Test { str: String! override: Override } input Override { boo: Boolean }`, - `query ($input: Test){}`, + `query ($input: Test){ a }`, `{"type":["object","null"],"properties":{"override":{"type":["string","null"]},"str":{"type":["string"]}},"required":["str"],"additionalProperties":false}`, []string{ `null`, @@ -257,7 +257,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("recursive object", runTest( `scalar String scalar Boolean input Test { str: String! nested: Nested } input Nested { boo: Boolean recursive: Test }`, - `query ($input: Test){}`, + `query ($input: Test){ a }`, `{"type":["object","null"],"properties":{"nested":{"anyOf":[{"type":["null"]},{"$ref":"#/$defs/Nested"}]},"str":{"type":["string"]}},"required":["str"],"additionalProperties":false,"$defs":{"Nested":{"type":["object","null"],"properties":{"boo":{"type":["boolean","null"]},"recursive":{"anyOf": [{"type":["null"]},{"$ref":"#/$defs/Test"}]}},"additionalProperties":false},"Test":{"type":["object","null"],"properties":{"nested":{"anyOf":[{"type":["null"]},{"$ref":"#/$defs/Nested"}]},"str":{"type":["string"]}},"required":["str"],"additionalProperties":false}}}`, []string{ `{"str":"validString"}`, @@ -273,7 +273,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("recursive object with multiple branches", runTest( `scalar String scalar Boolean input Root { test: Test another: Another } input Test { str: String! nested: Nested } input Nested { boo: Boolean recursive: Test another: Another } input Another { boo: Boolean }`, - `query ($input: Root){}`, + `query ($input: Root){ a }`, `{"type":["object","null"],"properties":{"another":{"anyOf":[{"type":["null"]},{"$ref":"#/$defs/Another"}]},"test":{"anyOf":[{"type":["null"]},{"$ref":"#/$defs/Test"}]}},"additionalProperties":false,"$defs":{"Another":{"type":["object","null"],"properties":{"boo":{"type":["boolean","null"]}},"additionalProperties":false},"Nested":{"type":["object","null"],"properties":{"another":{"anyOf":[{"type":["null"]},{"$ref":"#/$defs/Another"}]},"boo":{"type":["boolean","null"]},"recursive":{"anyOf":[{"type":["null"]},{"$ref":"#/$defs/Test"}]}},"additionalProperties":false},"Test":{"type":["object","null"],"properties":{"nested":{"anyOf":[{"type":["null"]},{"$ref":"#/$defs/Nested"}]},"str":{"type":["string"]}},"required":["str"],"additionalProperties":false}}}`, []string{ `{"test":{"str":"validString"}}`, @@ -287,14 +287,14 @@ func TestJsonSchema(t *testing.T) { )) t.Run("complex recursive schema", runTest( complexRecursiveSchema, - `query ($input: db_messagesWhereInput){}`, + `query ($input: db_messagesWhereInput){ a }`, complexRecursiveSchemaResult, []string{}, []string{}, )) t.Run("one level deep sub path", runTest( "input Human { name: String! } scalar String", - "query ($human: Human!) { }", + "query ($human: Human!) { a }", `{"type":["string"]}`, []string{ `"John Doe"`, @@ -306,7 +306,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("multi level deep sub path", runTest( "input Human { name: String! pet: Animal } scalar String type Animal { name: String! }", - "query ($human: Human!) { }", + "query ($human: Human!) { a }", `{"type":["string"]}`, []string{ `"Doggie"`, @@ -319,7 +319,7 @@ func TestJsonSchema(t *testing.T) { )) t.Run("not defined scalar", runTest( `input Container { name: MyScalar }`, - `query ($input: Container){}`, + `query ($input: Container){ a }`, `{"type":["object", "null"], "properties": {"name": {}}, "additionalProperties": false}`, []string{}, []string{},