diff --git a/src/HotChocolate/Core/src/Validation/Rules/LeafFieldSelectionsRule.cs b/src/HotChocolate/Core/src/Validation/Rules/LeafFieldSelectionsRule.cs index 53eac70e857..0a36c3ff464 100644 --- a/src/HotChocolate/Core/src/Validation/Rules/LeafFieldSelectionsRule.cs +++ b/src/HotChocolate/Core/src/Validation/Rules/LeafFieldSelectionsRule.cs @@ -80,7 +80,9 @@ private void ValidateField( FieldNode field, IType parentType) { - if (parentType is not IComplexTypeDefinition complex + var namedParentType = parentType.NamedType(); + + if (namedParentType is not IComplexTypeDefinition complex || !complex.Fields.TryGetField(field.Name.Value, out var fieldDef)) { // handled by other rules like KnownFieldNames diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/FusionTestBase.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/FusionTestBase.cs index 69013eb7249..7022f7c27fb 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/FusionTestBase.cs +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/FusionTestBase.cs @@ -21,6 +21,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Xunit.Sdk; @@ -36,7 +37,8 @@ protected async Task CreateCompositeSchemaAsync( Action? configureServices = null, Action? configureApplication = null, Action? configureGatewayBuilder = null, - [StringSyntax("json")] string? gatewaySettings = null) + [StringSyntax("json")] string? gatewaySettings = null, + string? environmentName = "Development") { var sourceSchemas = new List(); var gatewayServices = new ServiceCollection(); @@ -181,7 +183,8 @@ protected async Task CreateCompositeSchemaAsync( configureServices?.Invoke(services); }, - configureApplication); + configureApplication, + environmentName); return new Gateway(gatewayTestServer, sourceSchemas, interactions); diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/IntrospectionTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/IntrospectionTests.cs index ce21f3dabc8..36dff816edb 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/IntrospectionTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/IntrospectionTests.cs @@ -358,6 +358,76 @@ public async Task Typename_On_Introspection_Types() await MatchSnapshotAsync(gateway, request, result); } + [Fact] + public async Task Introspection_OfType_Without_SelectionSet() + { + // arrange + using var server1 = CreateSourceSchema( + "A", + TestSchema); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", server1) + ]); + + // act + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + """ + { + __schema { + types { + ofType + } + } + } + """); + + using var result = await client.PostAsync( + request, + new Uri("http://localhost:5000/graphql")); + + // assert + await MatchSnapshotAsync(gateway, request, result); + } + + [Fact] + public async Task Introspection_Field_Type_Without_SelectionSet() + { + // arrange + using var server1 = CreateSourceSchema( + "A", + TestSchema); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", server1) + ]); + + // act + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + """ + { + __type(name: "Query") { + fields { + type + } + } + } + """); + + using var result = await client.PostAsync( + request, + new Uri("http://localhost:5000/graphql")); + + // assert + await MatchSnapshotAsync(gateway, request, result); + } + [Theory] [InlineData("SchemaCapabilitiesQuery")] [InlineData("InputValueCapabilitiesQuery")] diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/TestServerSession.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/TestServerSession.cs index 4c17a23c69f..89794bd9fed 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/TestServerSession.cs +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/TestServerSession.cs @@ -13,12 +13,18 @@ internal sealed class TestServerSession : IDisposable public TestServer CreateServer( Action configureServices, - Action configureApplication) + Action configureApplication, + string? environmentName = null) { var builder = new WebHostBuilder() .Configure(configureApplication) .ConfigureServices(configureServices); + if (environmentName is not null) + { + builder.UseEnvironment(environmentName); + } + var server = new TestServer(builder); if (!_cleanupPipeline.Writer.TryWrite(server)) diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Field_Type_Without_SelectionSet.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Field_Type_Without_SelectionSet.yaml new file mode 100644 index 00000000000..58715cd79f2 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_Field_Type_Without_SelectionSet.yaml @@ -0,0 +1,115 @@ +title: Introspection_Field_Type_Without_SelectionSet +request: + document: | + { + __type(name: "Query") { + fields { + type + } + } + } +response: + body: | + { + "errors": [ + { + "message": "Field \u0022type\u0022 of type \u0022__Type!\u0022 must have a selection of subfields. Did you mean \u0022type { ... }\u0022?", + "locations": [ + { + "line": 4, + "column": 7 + } + ], + "extensions": { + "declaringType": "__Field", + "field": "type", + "type": "__Type!", + "responseName": "type", + "specifiedBy": "https://spec.graphql.org/September2025/#sec-Field-Selections" + } + } + ] + } +sourceSchemas: + - name: A + schema: | + "Schema description" + schema @test(arg: "value") { + query: Query + mutation: Mutation + subscription: Subscription + } + + interface Node @test(arg: "value") { + id: ID! + } + + "Interface description" + interface Votable implements Node { + "Interface field description" + id: ID! + } + + type Mutation @test(arg: "value") { + postReview(input: PostReviewInput): Review @test(arg: "value") + } + + type Post implements Votable & Node @key(fields: "id") { + id: ID! + postKind: PostKind @shareable + location: String @inaccessible + } + + "Object type description" + type Query @test(arg: "value") { + "Object field description" + posts( + "Argument description" + filter: PostsFilter + first: Int! = 5 @test(arg: "value") + hidden: Boolean @deprecated(reason: "No longer supported") + ): [Post] + userCreation: UserCreation + votables: [Votable]! @deprecated(reason: "No longer supported") + postById(postId: ID! @is(field: "id")): Post @lookup + node(id: ID!): Node @lookup + } + + type Review implements Votable & Node @test(arg: "value") { + id: ID! + } + + type Subscription @test(arg: "value") { + onNewReview: Review + } + + "Union description" + union UserCreation @test(arg: "value") = Post | Review + + input PostReviewInput @oneOf { + scalar: String @deprecated(reason: "No longer supported") + pros: [PostReviewPro] + } + + input PostReviewPro { + scalar: Int! + } + + "Input object type description" + input PostsFilter @test(arg: "value") { + "Input field description" + scalar: String = "test" @test(arg: "value") + } + + "Enum description" + enum PostKind @test(arg: "value") { + "Enum value description" + STORY @test(arg: "value") + PHOTO @deprecated(reason: "No longer supported") + } + + "The `@oneOf` directive is used within the type system definition language to indicate that an Input Object is a OneOf Input Object." + directive @oneOf on INPUT_OBJECT + + "Directive description" + directive @test("Directive argument description" arg: String! = "default") repeatable on QUERY | MUTATION | SUBSCRIPTION | FIELD | FRAGMENT_DEFINITION | FRAGMENT_SPREAD | INLINE_FRAGMENT | VARIABLE_DEFINITION | SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_OfType_Without_SelectionSet.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_OfType_Without_SelectionSet.yaml new file mode 100644 index 00000000000..2f2b03110f9 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/IntrospectionTests.Introspection_OfType_Without_SelectionSet.yaml @@ -0,0 +1,115 @@ +title: Introspection_OfType_Without_SelectionSet +request: + document: | + { + __schema { + types { + ofType + } + } + } +response: + body: | + { + "errors": [ + { + "message": "Field \u0022ofType\u0022 of type \u0022__Type\u0022 must have a selection of subfields. Did you mean \u0022ofType { ... }\u0022?", + "locations": [ + { + "line": 4, + "column": 7 + } + ], + "extensions": { + "declaringType": "__Type", + "field": "ofType", + "type": "__Type", + "responseName": "ofType", + "specifiedBy": "https://spec.graphql.org/September2025/#sec-Field-Selections" + } + } + ] + } +sourceSchemas: + - name: A + schema: | + "Schema description" + schema @test(arg: "value") { + query: Query + mutation: Mutation + subscription: Subscription + } + + interface Node @test(arg: "value") { + id: ID! + } + + "Interface description" + interface Votable implements Node { + "Interface field description" + id: ID! + } + + type Mutation @test(arg: "value") { + postReview(input: PostReviewInput): Review @test(arg: "value") + } + + type Post implements Votable & Node @key(fields: "id") { + id: ID! + postKind: PostKind @shareable + location: String @inaccessible + } + + "Object type description" + type Query @test(arg: "value") { + "Object field description" + posts( + "Argument description" + filter: PostsFilter + first: Int! = 5 @test(arg: "value") + hidden: Boolean @deprecated(reason: "No longer supported") + ): [Post] + userCreation: UserCreation + votables: [Votable]! @deprecated(reason: "No longer supported") + postById(postId: ID! @is(field: "id")): Post @lookup + node(id: ID!): Node @lookup + } + + type Review implements Votable & Node @test(arg: "value") { + id: ID! + } + + type Subscription @test(arg: "value") { + onNewReview: Review + } + + "Union description" + union UserCreation @test(arg: "value") = Post | Review + + input PostReviewInput @oneOf { + scalar: String @deprecated(reason: "No longer supported") + pros: [PostReviewPro] + } + + input PostReviewPro { + scalar: Int! + } + + "Input object type description" + input PostsFilter @test(arg: "value") { + "Input field description" + scalar: String = "test" @test(arg: "value") + } + + "Enum description" + enum PostKind @test(arg: "value") { + "Enum value description" + STORY @test(arg: "value") + PHOTO @deprecated(reason: "No longer supported") + } + + "The `@oneOf` directive is used within the type system definition language to indicate that an Input Object is a OneOf Input Object." + directive @oneOf on INPUT_OBJECT + + "Directive description" + directive @test("Directive argument description" arg: String! = "default") repeatable on QUERY | MUTATION | SUBSCRIPTION | FIELD | FRAGMENT_DEFINITION | FRAGMENT_SPREAD | INLINE_FRAGMENT | VARIABLE_DEFINITION | SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION