From fd19d95a57a756a6811707b01bd29528fd870aaf Mon Sep 17 00:00:00 2001 From: Shane32 Date: Fri, 23 Aug 2024 11:20:28 -0400 Subject: [PATCH 1/6] Support persisted documents --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 1608648a..1800c57f 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -68,6 +68,7 @@ public class GraphQLHttpMiddleware : IUserContextBuilder private const string VARIABLES_KEY = "variables"; private const string EXTENSIONS_KEY = "extensions"; private const string OPERATION_NAME_KEY = "operationName"; + private const string DOCUMENT_ID_KEY = "documentId"; private const string OPERATIONS_KEY = "operations"; // used for multipart/form-data requests per https://github.com/jaydenseric/graphql-multipart-request-spec private const string MAP_KEY = "map"; // used for multipart/form-data requests per https://github.com/jaydenseric/graphql-multipart-request-spec private const string MEDIATYPE_GRAPHQLJSON = "application/graphql+json"; // deprecated @@ -343,8 +344,8 @@ void ApplyMapToRequests(Dictionary map, IFormCollection form, foreach (var entry in map) { // validate entry key - if (entry.Key == "" || entry.Key == "query" || entry.Key == "operationName" || entry.Key == "variables" || entry.Key == "extensions" || entry.Key == "operations" || entry.Key == "map") - throw new InvalidMapError("Map key cannot be query, operationName, variables, extensions, operations or map."); + if (entry.Key == "" || entry.Key == QUERY_KEY || entry.Key == OPERATION_NAME_KEY || entry.Key == VARIABLES_KEY || entry.Key == EXTENSIONS_KEY || entry.Key == DOCUMENT_ID_KEY || entry.Key == OPERATIONS_KEY || entry.Key == MAP_KEY) + throw new InvalidMapError("Map key cannot be query, operationName, variables, extensions, documentId, operations or map."); // locate file var file = form.Files[entry.Key] ?? throw new InvalidMapError("Map key does not refer to an uploaded file."); @@ -1220,6 +1221,7 @@ protected virtual Task WriteErrorResponseAsync(HttpContext context, HttpStatusCo Variables = formCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, Extensions = formCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize(extensionsValues[0]) : null, OperationName = formCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null, + DocumentId = formCollection.TryGetValue(DOCUMENT_ID_KEY, out var documentIdValues) ? documentIdValues[0] : null, }; /// From b3fcf19359a49f27a58803c85e53214919211cfb Mon Sep 17 00:00:00 2001 From: Shane32 Date: Fri, 23 Aug 2024 11:21:47 -0400 Subject: [PATCH 2/6] Update --- src/Transports.AspNetCore/GraphQLHttpMiddleware.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 1800c57f..f418e312 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -1213,6 +1213,7 @@ protected virtual Task WriteErrorResponseAsync(HttpContext context, HttpStatusCo Variables = _options.ReadVariablesFromQueryString && queryCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, Extensions = _options.ReadExtensionsFromQueryString && queryCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize(extensionsValues[0]) : null, OperationName = queryCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null, + DocumentId = queryCollection.TryGetValue(DOCUMENT_ID_KEY, out var documentIdValues) ? documentIdValues[0] : null, }; private GraphQLRequest DeserializeFromFormBody(IFormCollection formCollection) => new() From 6e9ef0e961367cb2d3921c2df18ce4e1b82ff937 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Fri, 23 Aug 2024 11:45:28 -0400 Subject: [PATCH 3/6] Add tests for get/post --- .../GraphQLHttpMiddleware.cs | 4 +- .../GraphQLWs/SubscriptionServer.cs | 1 + .../SubscriptionServer.cs | 1 + .../Middleware/GetTests.cs | 14 +++ .../Middleware/PostTests.cs | 92 +++++++++++++++---- 5 files changed, 91 insertions(+), 21 deletions(-) diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index f418e312..ebfa343e 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -200,7 +200,8 @@ protected virtual async Task InvokeAsync(HttpContext context, RequestDelegate ne Query = urlGQLRequest?.Query ?? bodyGQLRequest?.Query, Variables = urlGQLRequest?.Variables ?? bodyGQLRequest?.Variables, Extensions = urlGQLRequest?.Extensions ?? bodyGQLRequest?.Extensions, - OperationName = urlGQLRequest?.OperationName ?? bodyGQLRequest?.OperationName + OperationName = urlGQLRequest?.OperationName ?? bodyGQLRequest?.OperationName, + DocumentId = urlGQLRequest?.DocumentId ?? bodyGQLRequest?.DocumentId, }; await HandleRequestAsync(context, next, gqlRequest); @@ -684,6 +685,7 @@ protected virtual async Task ExecuteRequestAsync(HttpContext co Query = request?.Query, Variables = request?.Variables, Extensions = request?.Extensions, + DocumentId = request?.DocumentId, CancellationToken = context.RequestAborted, OperationName = request?.OperationName, RequestServices = serviceProvider, diff --git a/src/Transports.AspNetCore/WebSockets/GraphQLWs/SubscriptionServer.cs b/src/Transports.AspNetCore/WebSockets/GraphQLWs/SubscriptionServer.cs index 221f3345..3235d16c 100644 --- a/src/Transports.AspNetCore/WebSockets/GraphQLWs/SubscriptionServer.cs +++ b/src/Transports.AspNetCore/WebSockets/GraphQLWs/SubscriptionServer.cs @@ -208,6 +208,7 @@ protected override async Task ExecuteRequestAsync(OperationMess Query = request.Query, Variables = request.Variables, Extensions = request.Extensions, + DocumentId = request.DocumentId, OperationName = request.OperationName, RequestServices = scope.ServiceProvider, CancellationToken = CancellationToken, diff --git a/src/Transports.AspNetCore/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs b/src/Transports.AspNetCore/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs index 8f8fe366..b0cf7c53 100644 --- a/src/Transports.AspNetCore/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs +++ b/src/Transports.AspNetCore/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs @@ -186,6 +186,7 @@ protected override async Task ExecuteRequestAsync(OperationMess Query = request.Query, Variables = request.Variables, Extensions = request.Extensions, + DocumentId = request.DocumentId, OperationName = request.OperationName, RequestServices = scope.ServiceProvider, CancellationToken = CancellationToken, diff --git a/tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs b/tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs index 949750a4..ab0678ff 100644 --- a/tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs +++ b/tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs @@ -24,6 +24,12 @@ public GetTests() .WithSubscription()) .AddSchema() .AddSystemTextJson() + .UsePersistedDocuments(o => + { + o.AllowOnlyPersistedDocuments = false; + o.AllowedPrefixes.Add("test"); + o.GetQueryDelegate = (options, prefix, payload) => prefix == "test" && payload == "abc" ? new("{count}") : default; + }) .ConfigureExecutionOptions(o => _configureExecution(o))); #if NETCOREAPP2_1 || NET48 services.AddHostApplicationLifetime(); @@ -78,6 +84,14 @@ public async Task BasicTest() await response.ShouldBeAsync("""{"data":{"count":0}}"""); } + [Fact] + public async Task PersistedDocumentTest() + { + var client = _server.CreateClient(); + using var response = await client.GetAsync("/graphql?documentId=test:abc"); + await response.ShouldBeAsync("""{"data":{"count":0}}"""); + } + [Theory] [InlineData(true, true)] [InlineData(true, false)] diff --git a/tests/Transports.AspNetCore.Tests/Middleware/PostTests.cs b/tests/Transports.AspNetCore.Tests/Middleware/PostTests.cs index 5b394637..54f065ef 100644 --- a/tests/Transports.AspNetCore.Tests/Middleware/PostTests.cs +++ b/tests/Transports.AspNetCore.Tests/Middleware/PostTests.cs @@ -26,6 +26,15 @@ public PostTests() .AddAutoClrMappings() .AddFormFileGraphType() .AddSystemTextJson() + .UsePersistedDocuments(o => + { + o.AllowOnlyPersistedDocuments = false; + o.AllowedPrefixes.Add("test"); + o.GetQueryDelegate = (options, prefix, payload) => + prefix == "test" && payload == "abc" ? new("{count}") : + prefix == "test" && payload == "form" ? new("query op1{ext} query op2($test:String!){ext var(test:$test)}") : + default; + }) .ConfigureExecutionOptions(o => _configureExecution(o))); #if NETCOREAPP2_1 || NET48 services.AddHostApplicationLifetime(); @@ -125,6 +134,13 @@ public async Task BasicTest() await response.ShouldBeAsync("""{"data":{"count":0}}"""); } + [Fact] + public async Task PersistedDocument_Simple() + { + using var response = await PostRequestAsync(new() { DocumentId = "test:abc" }); + await response.ShouldBeAsync("""{"data":{"count":0}}"""); + } + #if NET5_0_OR_GREATER [Fact] public async Task AltCharset() @@ -159,26 +175,39 @@ public async Task AltCharset_Invalid() #endif [Theory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public async Task FormMultipart_Legacy(bool requireCsrf, bool supplyCsrf) + [InlineData(true, true, false)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + [InlineData(true, true, true)] + [InlineData(true, false, true)] + [InlineData(false, true, true)] + [InlineData(false, false, true)] + public async Task FormMultipart_Legacy(bool requireCsrf, bool supplyCsrf, bool useDocumentId) { _options2.ReadFormOnPost = true; if (!requireCsrf) _options2.CsrfProtectionEnabled = false; var client = _server.CreateClient(); var content = new MultipartFormDataContent(); - var queryContent = new StringContent("query op1{ext} query op2($test:String!){ext var(test:$test)}"); - queryContent.Headers.ContentType = new("application/graphql"); + if (!useDocumentId) + { + var queryContent = new StringContent("query op1{ext} query op2($test:String!){ext var(test:$test)}"); + queryContent.Headers.ContentType = new("application/graphql"); + content.Add(queryContent, "query"); + } + else + { + var documentIdContent = new StringContent("test:form"); + documentIdContent.Headers.ContentType = new("text/text"); + content.Add(documentIdContent, "documentId"); + } var variablesContent = new StringContent("""{"test":"1"}"""); variablesContent.Headers.ContentType = new("application/json"); var extensionsContent = new StringContent("""{"test":"2"}"""); extensionsContent.Headers.ContentType = new("application/json"); var operationNameContent = new StringContent("op2"); operationNameContent.Headers.ContentType = new("text/text"); - content.Add(queryContent, "query"); content.Add(variablesContent, "variables"); content.Add(extensionsContent, "extensions"); content.Add(operationNameContent, "operationName"); @@ -193,24 +222,35 @@ public async Task FormMultipart_Legacy(bool requireCsrf, bool supplyCsrf) } [Theory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public async Task FormMultipart_Upload(bool requireCsrf, bool supplyCsrf) + [InlineData(true, true, false)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + [InlineData(true, true, true)] + [InlineData(true, false, true)] + [InlineData(false, true, true)] + [InlineData(false, false, true)] + public async Task FormMultipart_Upload(bool requireCsrf, bool supplyCsrf, bool useDocumentId) { _options2.ReadFormOnPost = true; if (!requireCsrf) _options2.CsrfProtectionEnabled = false; var client = _server.CreateClient(); using var content = new MultipartFormDataContent(); - var jsonContent = new StringContent(""" + var jsonContent = new StringContent(!useDocumentId ? """ { "query": "query op1{ext} query op2($test:String!){ext var(test:$test)}", "operationName": "op2", "variables": { "test": "1" }, "extensions": { "test": "2"} } + """ : """ + { + "documentId": "test:form", + "operationName": "op2", + "variables": { "test": "1" }, + "extensions": { "test": "2"} + } """, Encoding.UTF8, "application/json"); content.Add(jsonContent, "operations"); using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql2") { Content = content }; @@ -414,18 +454,24 @@ public async Task FormMultipart_Upload_Validation(int? maxFileCount, int? maxFil } [Theory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public async Task FormUrlEncoded(bool requireCsrf, bool supplyCsrf) + [InlineData(true, true, false)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + [InlineData(true, true, true)] + [InlineData(true, false, true)] + [InlineData(false, true, true)] + [InlineData(false, false, true)] + public async Task FormUrlEncoded(bool requireCsrf, bool supplyCsrf, bool useDocumentId) { _options2.ReadFormOnPost = true; if (!requireCsrf) _options2.CsrfProtectionEnabled = false; var client = _server.CreateClient(); var content = new FormUrlEncodedContent(new[] { - new KeyValuePair("query", "query op1{ext} query op2($test:String!){ext var(test:$test)}"), + !useDocumentId + ? new KeyValuePair("query", "query op1{ext} query op2($test:String!){ext var(test:$test)}") + : new KeyValuePair("documentId", "test:form"), new KeyValuePair("variables", """{"test":"1"}"""), new KeyValuePair("extensions", """{"test":"2"}"""), new KeyValuePair("operationName", "op2"), @@ -739,4 +785,10 @@ public async Task ReadAlsoFromQueryString(bool readFromQueryString, bool readVar await response.ShouldBeAsync(expected); } + [Fact] + public async Task ReadAlsoFromQueryString_DocumentId() + { + using var response = await PostRequestAsync("/graphql?documentId=test:abc", new() { DocumentId = "test:def" }); + await response.ShouldBeAsync("""{"data":{"count":0}}"""); + } } From 196cb8c1e5d93915e27f80f5c7d60dadee89ad41 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Fri, 23 Aug 2024 11:59:50 -0400 Subject: [PATCH 4/6] Finish tests --- .../Middleware/GetTests.cs | 51 ++++++++++----- .../Middleware/PostTests.cs | 63 ++++++++++++------- .../WebSockets/NewSubscriptionServerTests.cs | 2 + .../WebSockets/OldSubscriptionServerTests.cs | 2 + 4 files changed, 83 insertions(+), 35 deletions(-) diff --git a/tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs b/tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs index ab0678ff..69149df0 100644 --- a/tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs +++ b/tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http.Headers; +using GraphQL.PersistedDocuments; using GraphQL.Server.Transports.AspNetCore.Errors; using GraphQL.Validation; @@ -10,6 +11,7 @@ public class GetTests : IDisposable private GraphQLHttpMiddlewareOptions _options = null!; private GraphQLHttpMiddlewareOptions _options2 = null!; private Action _configureExecution = _ => { }; + private bool _enablePersistedDocuments = true; private readonly TestServer _server; public GetTests() @@ -18,19 +20,35 @@ public GetTests() hostBuilder.ConfigureServices(services => { services.AddSingleton(); - services.AddGraphQL(b => b - .AddAutoSchema(s => s - .WithMutation() - .WithSubscription()) - .AddSchema() - .AddSystemTextJson() - .UsePersistedDocuments(o => + services.AddGraphQL(b => + { + b + .AddAutoSchema(s => s + .WithMutation() + .WithSubscription()) + .AddSchema() + .AddSystemTextJson() + .ConfigureExecution((options, next) => + { + if (_enablePersistedDocuments) + { + var handler = options.RequestServices!.GetRequiredService(); + return handler.ExecuteAsync(options, next); + } + return next(options); + }) + .ConfigureExecutionOptions(o => _configureExecution(o)); + b.Services.Configure(o => { o.AllowOnlyPersistedDocuments = false; o.AllowedPrefixes.Add("test"); - o.GetQueryDelegate = (options, prefix, payload) => prefix == "test" && payload == "abc" ? new("{count}") : default; - }) - .ConfigureExecutionOptions(o => _configureExecution(o))); + o.GetQueryDelegate = (options, prefix, payload) => + prefix == "test" && payload == "abc" ? new("{count}") : + prefix == "test" && payload == "form" ? new("query op1{ext} query op2($test:String!){ext var(test:$test)}") : + default; + }); + }); + services.AddSingleton(); #if NETCOREAPP2_1 || NET48 services.AddHostApplicationLifetime(); #endif @@ -360,14 +378,19 @@ public async Task QueryParseError(bool badRequest) } [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task NoQuery(bool badRequest) + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task NoQuery(bool badRequest, bool usePersistedDocumentHandler) { + _enablePersistedDocuments = usePersistedDocumentHandler; _options.ValidationErrorsReturnBadRequest = badRequest; var client = _server.CreateClient(); using var response = await client.GetAsync("/graphql"); - await response.ShouldBeAsync(badRequest, """{"errors":[{"message":"GraphQL query is missing.","extensions":{"code":"QUERY_MISSING","codes":["QUERY_MISSING"]}}]}"""); + await response.ShouldBeAsync(badRequest, usePersistedDocumentHandler + ? """{"errors":[{"message":"The request must have a documentId parameter.","extensions":{"code":"DOCUMENT_ID_MISSING","codes":["DOCUMENT_ID_MISSING"]}}]}""" + : """{"errors":[{"message":"GraphQL query is missing.","extensions":{"code":"QUERY_MISSING","codes":["QUERY_MISSING"]}}]}"""); } [Theory] diff --git a/tests/Transports.AspNetCore.Tests/Middleware/PostTests.cs b/tests/Transports.AspNetCore.Tests/Middleware/PostTests.cs index 54f065ef..4562993b 100644 --- a/tests/Transports.AspNetCore.Tests/Middleware/PostTests.cs +++ b/tests/Transports.AspNetCore.Tests/Middleware/PostTests.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http.Headers; +using GraphQL.PersistedDocuments; using GraphQL.Server.Transports.AspNetCore.Errors; using GraphQL.Validation; @@ -10,6 +11,7 @@ public class PostTests : IDisposable private GraphQLHttpMiddlewareOptions _options = null!; private GraphQLHttpMiddlewareOptions _options2 = null!; private Action _configureExecution = _ => { }; + private bool _enablePersistedDocuments = true; private readonly TestServer _server; public PostTests() @@ -18,15 +20,27 @@ public PostTests() hostBuilder.ConfigureServices(services => { services.AddSingleton(); - services.AddGraphQL(b => b - .AddAutoSchema(s => s - .WithMutation() - .WithSubscription()) - .AddSchema() - .AddAutoClrMappings() - .AddFormFileGraphType() - .AddSystemTextJson() - .UsePersistedDocuments(o => + services.AddGraphQL(b => + { + b + .AddAutoSchema(s => s + .WithMutation() + .WithSubscription()) + .AddSchema() + .AddAutoClrMappings() + .AddFormFileGraphType() + .AddSystemTextJson() + .ConfigureExecution((options, next) => + { + if (_enablePersistedDocuments) + { + var handler = options.RequestServices!.GetRequiredService(); + return handler.ExecuteAsync(options, next); + } + return next(options); + }) + .ConfigureExecutionOptions(o => _configureExecution(o)); + b.Services.Configure(o => { o.AllowOnlyPersistedDocuments = false; o.AllowedPrefixes.Add("test"); @@ -34,8 +48,9 @@ public PostTests() prefix == "test" && payload == "abc" ? new("{count}") : prefix == "test" && payload == "form" ? new("query op1{ext} query op2($test:String!){ext var(test:$test)}") : default; - }) - .ConfigureExecutionOptions(o => _configureExecution(o))); + }); + }); + services.AddSingleton(); #if NETCOREAPP2_1 || NET48 services.AddHostApplicationLifetime(); #endif @@ -335,17 +350,17 @@ public async Task FormMultipart_Upload(bool requireCsrf, bool supplyCsrf, bool u 400, "{\"errors\":[{\"message\":\"Invalid map path. Map target cannot be null.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] // invalid map keys [InlineData(40, null, "{\"\":[\"0.variables.arg\"]}", false, false, - 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key cannot be query, operationName, variables, extensions, operations or map.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] + 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key cannot be query, operationName, variables, extensions, documentId, operations or map.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] [InlineData(41, null, "{\"query\":[\"0.variables.arg\"]}", false, false, - 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key cannot be query, operationName, variables, extensions, operations or map.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] + 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key cannot be query, operationName, variables, extensions, documentId, operations or map.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] [InlineData(42, null, "{\"variables\":[\"0.variables.arg\"]}", false, false, - 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key cannot be query, operationName, variables, extensions, operations or map.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] + 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key cannot be query, operationName, variables, extensions, documentId, operations or map.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] [InlineData(43, null, "{\"extensions\":[\"0.variables.arg\"]}", false, false, - 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key cannot be query, operationName, variables, extensions, operations or map.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] + 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key cannot be query, operationName, variables, extensions, documentId, operations or map.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] [InlineData(44, null, "{\"operationName\":[\"0.variables.arg\"]}", false, false, - 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key cannot be query, operationName, variables, extensions, operations or map.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] + 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key cannot be query, operationName, variables, extensions, documentId, operations or map.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] [InlineData(45, null, "{\"map\":[\"0.variables.arg\"]}", false, false, - 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key cannot be query, operationName, variables, extensions, operations or map.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] + 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key cannot be query, operationName, variables, extensions, documentId, operations or map.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] // missing referenced file [InlineData(50, null, "{\"file0\":[\"0.variables.arg\"]}", false, false, 400, "{\"errors\":[{\"message\":\"Invalid map path. Map key does not refer to an uploaded file.\",\"extensions\":{\"code\":\"INVALID_MAP\",\"codes\":[\"INVALID_MAP\"]}}]}")] @@ -682,13 +697,18 @@ public async Task QueryParseError(bool badRequest) } [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task NoQuery(bool badRequest) + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task NoQuery(bool badRequest, bool usePersistedDocumentHandler) { + _enablePersistedDocuments = usePersistedDocumentHandler; _options.ValidationErrorsReturnBadRequest = badRequest; using var response = await PostJsonAsync("{}"); - await response.ShouldBeAsync(badRequest, """{"errors":[{"message":"GraphQL query is missing.","extensions":{"code":"QUERY_MISSING","codes":["QUERY_MISSING"]}}]}"""); + await response.ShouldBeAsync(badRequest, usePersistedDocumentHandler + ? """{"errors":[{"message":"The request must have a documentId parameter.","extensions":{"code":"DOCUMENT_ID_MISSING","codes":["DOCUMENT_ID_MISSING"]}}]}""" + : """{"errors":[{"message":"GraphQL query is missing.","extensions":{"code":"QUERY_MISSING","codes":["QUERY_MISSING"]}}]}"""); } [Theory] @@ -696,6 +716,7 @@ public async Task NoQuery(bool badRequest) [InlineData(true)] public async Task NullRequest(bool badRequest) { + _enablePersistedDocuments = false; _options.ValidationErrorsReturnBadRequest = badRequest; using var response = await PostJsonAsync("null"); await response.ShouldBeAsync(badRequest, """{"errors":[{"message":"GraphQL query is missing.","extensions":{"code":"QUERY_MISSING","codes":["QUERY_MISSING"]}}]}"""); diff --git a/tests/Transports.AspNetCore.Tests/WebSockets/NewSubscriptionServerTests.cs b/tests/Transports.AspNetCore.Tests/WebSockets/NewSubscriptionServerTests.cs index 29bdd333..41351ed6 100644 --- a/tests/Transports.AspNetCore.Tests/WebSockets/NewSubscriptionServerTests.cs +++ b/tests/Transports.AspNetCore.Tests/WebSockets/NewSubscriptionServerTests.cs @@ -356,6 +356,7 @@ public async Task ExecuteRequestAsync() Variables = new Inputs(new Dictionary()), Extensions = new Inputs(new Dictionary()), OperationName = "def", + DocumentId = "ghi", }; _mockSerializer.Setup(x => x.ReadNode(payload)) .Returns(request) @@ -381,6 +382,7 @@ public async Task ExecuteRequestAsync() options.Query.ShouldBe(request.Query); options.Variables.ShouldBe(request.Variables); options.Extensions.ShouldBe(request.Extensions); + options.DocumentId.ShouldBe(request.DocumentId); options.OperationName.ShouldBe(request.OperationName); options.UserContext.ShouldBe(mockUserContext.Object); options.RequestServices.ShouldBe(mockServiceProvider.Object); diff --git a/tests/Transports.AspNetCore.Tests/WebSockets/OldSubscriptionServerTests.cs b/tests/Transports.AspNetCore.Tests/WebSockets/OldSubscriptionServerTests.cs index 55828243..ec1ec3e2 100644 --- a/tests/Transports.AspNetCore.Tests/WebSockets/OldSubscriptionServerTests.cs +++ b/tests/Transports.AspNetCore.Tests/WebSockets/OldSubscriptionServerTests.cs @@ -310,6 +310,7 @@ public async Task ExecuteRequestAsync() Variables = new Inputs(new Dictionary()), Extensions = new Inputs(new Dictionary()), OperationName = "def", + DocumentId = "ghi", }; _mockSerializer.Setup(x => x.ReadNode(payload)) .Returns(request) @@ -335,6 +336,7 @@ public async Task ExecuteRequestAsync() options.Query.ShouldBe(request.Query); options.Variables.ShouldBe(request.Variables); options.Extensions.ShouldBe(request.Extensions); + options.DocumentId.ShouldBe(request.DocumentId); options.OperationName.ShouldBe(request.OperationName); options.UserContext.ShouldBe(mockUserContext.Object); options.RequestServices.ShouldBe(mockServiceProvider.Object); From cc1ab92e677437e902a63a5bc98e39c9f170b56b Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 23 Aug 2024 16:05:30 -0400 Subject: [PATCH 5/6] Update Directory.Build.props --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 5a9b792a..72ee82dd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -32,7 +32,7 @@ GraphQL.Server.$(MSBuildProjectName) GraphQL.Server.$(MSBuildProjectName) - [8.0.0,9.0.0) + [8.0.1,9.0.0) true <_FriendAssembliesPublicKey>PublicKey=0024000004800000940000000602000000240000525341310004000001000100352162dbf27be78fc45136884b8f324aa9f1dfc928c96c24704bf1df1a8779b2f26c760ed8321eca5b95ea6bd9bb60cd025b300f73bd1f4ae1ee6e281f85c527fa013ab5cb2c3fc7a1cbef7f9bf0c9014152e6a21f6e0ac6a371f8b45c6d7139c9119df9eeecf1cf59063545bb7c07437b1bc12be2c57d108d72d6c27176fbb8 From 0b5f778b2ffda4a6ef425b33fbb5351eb7e411bc Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Fri, 23 Aug 2024 16:05:54 -0400 Subject: [PATCH 6/6] Update Directory.Build.props --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 72ee82dd..ab0a97fe 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 8.0.0-preview + 8.0.1-preview 9.0.0 latest MIT