From 777907839970432cbbce8f29d297bee8c5a42299 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Sat, 24 Aug 2024 00:15:17 -0400 Subject: [PATCH] Add persisted document support --- Directory.Build.props | 4 +- .../GraphQLHttpMiddleware.cs | 11 +- .../GraphQLWs/SubscriptionServer.cs | 1 + .../SubscriptionServer.cs | 1 + src/Tests/Middleware/GetTests.cs | 55 +++++-- src/Tests/Middleware/PostTests.cs | 144 +++++++++++++----- .../WebSockets/NewSubscriptionServerTests.cs | 2 + .../WebSockets/OldSubscriptionServerTests.cs | 2 + 8 files changed, 165 insertions(+), 55 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 041c88a..e44b060 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 6.0.0-preview + 6.0.1-preview 12.0 Shane Krueger Shane Krueger @@ -20,7 +20,7 @@ true enable false - 8.0.0 + 8.0.1 $(NoWarn);IDE0056;IDE0057;NU1902;NU1903 diff --git a/src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs b/src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs index 7695cc8..69512e0 100644 --- a/src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs +++ b/src/GraphQL.AspNetCore3/GraphQLHttpMiddleware.cs @@ -67,6 +67,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"; @@ -174,7 +175,8 @@ public virtual async Task InvokeAsync(HttpContext context) 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); @@ -299,8 +301,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."); @@ -603,6 +605,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, @@ -884,6 +887,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() { @@ -891,6 +895,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, }; /// diff --git a/src/GraphQL.AspNetCore3/WebSockets/GraphQLWs/SubscriptionServer.cs b/src/GraphQL.AspNetCore3/WebSockets/GraphQLWs/SubscriptionServer.cs index d858588..9b57782 100644 --- a/src/GraphQL.AspNetCore3/WebSockets/GraphQLWs/SubscriptionServer.cs +++ b/src/GraphQL.AspNetCore3/WebSockets/GraphQLWs/SubscriptionServer.cs @@ -192,6 +192,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/GraphQL.AspNetCore3/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs b/src/GraphQL.AspNetCore3/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs index 2e72846..c52f53b 100644 --- a/src/GraphQL.AspNetCore3/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs +++ b/src/GraphQL.AspNetCore3/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs @@ -172,6 +172,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/Tests/Middleware/GetTests.cs b/src/Tests/Middleware/GetTests.cs index 37ccbb8..d5aa2fe 100644 --- a/src/Tests/Middleware/GetTests.cs +++ b/src/Tests/Middleware/GetTests.cs @@ -1,4 +1,5 @@ using System.Net; +using GraphQL.PersistedDocuments; namespace Tests.Middleware; @@ -7,6 +8,7 @@ public class GetTests : IDisposable private GraphQLHttpMiddlewareOptions _options = null!; private GraphQLHttpMiddlewareOptions _options2 = null!; private readonly Action _configureExecution = _ => { }; + private bool _enablePersistedDocuments = true; private readonly TestServer _server; public GetTests() @@ -14,13 +16,31 @@ public GetTests() var hostBuilder = new WebHostBuilder(); hostBuilder.ConfigureServices(services => { services.AddSingleton(); - services.AddGraphQL(b => b - .AddAutoSchema(s => s - .WithMutation() - .WithSubscription()) - .AddSchema() - .AddSystemTextJson() - .ConfigureExecutionOptions(o => _configureExecution(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}") : + 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 @@ -63,6 +83,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)] @@ -160,14 +188,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/src/Tests/Middleware/PostTests.cs b/src/Tests/Middleware/PostTests.cs index d279a82..8d78cc0 100644 --- a/src/Tests/Middleware/PostTests.cs +++ b/src/Tests/Middleware/PostTests.cs @@ -1,4 +1,5 @@ using System.Net; +using GraphQL.PersistedDocuments; namespace Tests.Middleware; @@ -7,6 +8,7 @@ public class PostTests : IDisposable private GraphQLHttpMiddlewareOptions _options = null!; private GraphQLHttpMiddlewareOptions _options2 = null!; private readonly Action _configureExecution = _ => { }; + private bool _enablePersistedDocuments = true; private readonly TestServer _server; public PostTests() @@ -14,15 +16,33 @@ public PostTests() var hostBuilder = new WebHostBuilder(); hostBuilder.ConfigureServices(services => { services.AddSingleton(); - services.AddGraphQL(b => b - .AddAutoSchema(s => s - .WithMutation() - .WithSubscription()) - .AddSchema() - .AddAutoClrMappings() - .AddFormFileGraphType() - .AddSystemTextJson() - .ConfigureExecutionOptions(o => _configureExecution(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"); + 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 @@ -110,6 +130,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() @@ -144,26 +171,36 @@ 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"); @@ -178,24 +215,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 }; @@ -280,17 +328,17 @@ public async Task FormMultipart_Upload(bool requireCsrf, bool supplyCsrf) 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\"]}}]}")] @@ -399,18 +447,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"), @@ -545,13 +599,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] @@ -559,6 +618,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""]}}]}"); @@ -646,4 +706,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}}"""); + } } diff --git a/src/Tests/WebSockets/NewSubscriptionServerTests.cs b/src/Tests/WebSockets/NewSubscriptionServerTests.cs index d26394b..d703f31 100644 --- a/src/Tests/WebSockets/NewSubscriptionServerTests.cs +++ b/src/Tests/WebSockets/NewSubscriptionServerTests.cs @@ -340,6 +340,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) @@ -364,6 +365,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/src/Tests/WebSockets/OldSubscriptionServerTests.cs b/src/Tests/WebSockets/OldSubscriptionServerTests.cs index 3a3907b..83e91a1 100644 --- a/src/Tests/WebSockets/OldSubscriptionServerTests.cs +++ b/src/Tests/WebSockets/OldSubscriptionServerTests.cs @@ -295,6 +295,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) @@ -319,6 +320,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);