From 304cb8da06094bcb319d85774f36ddca230ee571 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 30 Aug 2019 14:48:10 +0200 Subject: [PATCH] Fixed Apollo Active Query Persistence Flow (#1049) --- .../Execution/PersistedQueriesTests.cs | 330 +++++++++++++++++- ...s.ActivePersistedQueries_Invalid_Hash.snap | 14 + ...ivePersistedQueries_Invalid_Hash_Type.snap | 14 + ..._No_PersistedQuery_Section_In_Request.snap | 5 + ...sh_MD5_Query_Loaded_From_Cache_Base64.snap | 8 + ...dHash_MD5_Query_Loaded_From_Cache_Hex.snap | 8 + ...nvalidHash_MD5_Query_Not_Found_Base64.snap | 17 + ...y_InvalidHash_MD5_Query_Not_Found_Hex.snap | 17 + ...y_InvalidHash_MD5_Query_Stored_Base64.snap | 13 + ...uery_InvalidHash_MD5_Query_Stored_Hex.snap | 13 + ...h_Sha1_Query_Loaded_From_Cache_Base64.snap | 8 + ...Hash_Sha1_Query_Loaded_From_Cache_Hex.snap | 8 + ...validHash_Sha1_Query_Not_Found_Base64.snap | 17 + ..._InvalidHash_Sha1_Query_Not_Found_Hex.snap | 17 + ..._InvalidHash_Sha1_Query_Stored_Base64.snap | 13 + ...ery_InvalidHash_Sha1_Query_Stored_Hex.snap | 13 + ...Sha256_Query_Loaded_From_Cache_Base64.snap | 8 + ...sh_Sha256_Query_Loaded_From_Cache_Hex.snap | 8 + ...lidHash_Sha256_Query_Not_Found_Base64.snap | 17 + ...nvalidHash_Sha256_Query_Not_Found_Hex.snap | 17 + ...nvalidHash_Sha256_Query_Stored_Base64.snap | 13 + ...y_InvalidHash_Sha256_Query_Stored_Hex.snap | 13 + .../QueryExecutionBuilderExtensions.cs | 36 +- .../WritePersistedQueryMiddleware.cs | 67 ++-- .../InternalsVisibleTo.cs} | 0 .../Parser/Utf8GraphQLRequestParserTests.cs | 102 ++++-- .../__resources__/Apollo_AQP_FullRequest.json | 11 + .../Parser/DocumentHashProviderBase.cs | 49 +++ .../Language/Parser/HashRepresentation.cs | 8 + .../Language/Parser/IDocumentHashProvider.cs | 2 + .../Parser/MD5DocumentHashProvider.cs | 31 +- .../Parser/Sha1DocumentHashProvider.cs | 31 +- .../Parser/Sha256DocumentHashProvider.cs | 31 +- .../Utf8GraphQLRequestParser.Request.cs | 2 + .../Parser/Utf8GraphQLRequestParser.cs | 66 ++-- 35 files changed, 881 insertions(+), 146 deletions(-) create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_Invalid_Hash.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_Invalid_Hash_Type.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_No_PersistedQuery_Section_In_Request.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Loaded_From_Cache_Base64.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Loaded_From_Cache_Hex.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Not_Found_Base64.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Not_Found_Hex.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Stored_Base64.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Stored_Hex.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Loaded_From_Cache_Base64.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Loaded_From_Cache_Hex.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Not_Found_Base64.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Not_Found_Hex.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Stored_Base64.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Stored_Hex.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Loaded_From_Cache_Base64.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Loaded_From_Cache_Hex.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Not_Found_Base64.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Not_Found_Hex.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Stored_Base64.snap create mode 100644 src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Stored_Hex.snap rename src/Core/Core/{TestSettings.cs => Properties/InternalsVisibleTo.cs} (100%) create mode 100644 src/Core/Language.Tests/__resources__/Apollo_AQP_FullRequest.json create mode 100644 src/Core/Language/Parser/DocumentHashProviderBase.cs create mode 100644 src/Core/Language/Parser/HashRepresentation.cs diff --git a/src/Core/Core.Tests/Execution/PersistedQueriesTests.cs b/src/Core/Core.Tests/Execution/PersistedQueriesTests.cs index d4ce68c2e95..63baf5e0569 100644 --- a/src/Core/Core.Tests/Execution/PersistedQueriesTests.cs +++ b/src/Core/Core.Tests/Execution/PersistedQueriesTests.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using HotChocolate.Language; using Microsoft.Extensions.DependencyInjection; +using Snapshooter; using Snapshooter.Xunit; using Xunit; @@ -305,8 +306,236 @@ public async Task ActivePersistedQueries_SaveQuery_And_Execute() result.ToJson().MatchSnapshot(); } + [InlineData(HashFormat.Base64)] + [InlineData(HashFormat.Hex)] + [Theory] + public async Task ActivePersistedQueries_SaveQuery_InvalidHash_Sha1( + HashFormat format) + { + // arrange + var serviceCollection = new ServiceCollection(); + + // configure presistence + serviceCollection.AddGraphQLSchema(b => b + .AddDocumentFromString("type Query { foo: String }") + .AddResolver("Query", "foo", "bar")); + serviceCollection.AddQueryExecutor(b => b + .AddSha1DocumentHashProvider(format) + .UseActivePersistedQueryPipeline()); + + // add in-memory query storage + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(sp => + sp.GetRequiredService()); + serviceCollection.AddSingleton(sp => + sp.GetRequiredService()); + + IServiceProvider services = + serviceCollection.BuildServiceProvider(); + + IQueryExecutor executor = + services.GetRequiredService(); + + var hashProvider = + services.GetRequiredService(); + var storage = services.GetRequiredService(); + + var query = new QuerySourceText("{ foo }"); + string hash = hashProvider.ComputeHash(query.ToSpan()); + + // act and assert + IExecutionResult result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQueryName(hash) + .AddExtension("persistedQuery", + new Dictionary + { + { hashProvider.Name, hash } + }) + .Create()); + result.MatchSnapshot(new SnapshotNameExtension( + "Query_Not_Found_" + format)); + + result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQueryName(hash) + .SetQuery("{ foo }") + .AddExtension("persistedQuery", + new Dictionary + { + { hashProvider.Name, hash } + }) + .Create()); + result.MatchSnapshot(new SnapshotNameExtension( + "Query_Stored_" + format)); + + result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQueryName(hash) + .AddExtension("persistedQuery", + new Dictionary + { + { hashProvider.Name, hash } + }) + .Create()); + result.MatchSnapshot(new SnapshotNameExtension( + "Query_Loaded_From_Cache_" + format)); + } + + [InlineData(HashFormat.Base64)] + [InlineData(HashFormat.Hex)] + [Theory] + public async Task ActivePersistedQueries_SaveQuery_InvalidHash_Sha256( + HashFormat format) + { + // arrange + var serviceCollection = new ServiceCollection(); + + // configure presistence + serviceCollection.AddGraphQLSchema(b => b + .AddDocumentFromString("type Query { foo: String }") + .AddResolver("Query", "foo", "bar")); + serviceCollection.AddQueryExecutor(b => b + .AddSha256DocumentHashProvider(format) + .UseActivePersistedQueryPipeline()); + + // add in-memory query storage + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(sp => + sp.GetRequiredService()); + serviceCollection.AddSingleton(sp => + sp.GetRequiredService()); + + IServiceProvider services = + serviceCollection.BuildServiceProvider(); + + IQueryExecutor executor = + services.GetRequiredService(); + + var hashProvider = + services.GetRequiredService(); + var storage = services.GetRequiredService(); + + var query = new QuerySourceText("{ foo }"); + string hash = hashProvider.ComputeHash(query.ToSpan()); + + // act and assert + IExecutionResult result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQueryName(hash) + .AddExtension("persistedQuery", + new Dictionary + { + { hashProvider.Name, hash } + }) + .Create()); + result.MatchSnapshot(new SnapshotNameExtension( + "Query_Not_Found_" + format)); + + result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQueryName(hash) + .SetQuery("{ foo }") + .AddExtension("persistedQuery", + new Dictionary + { + { hashProvider.Name, hash } + }) + .Create()); + result.MatchSnapshot(new SnapshotNameExtension( + "Query_Stored_" + format)); + + result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQueryName(hash) + .AddExtension("persistedQuery", + new Dictionary + { + { hashProvider.Name, hash } + }) + .Create()); + result.MatchSnapshot(new SnapshotNameExtension( + "Query_Loaded_From_Cache_" + format)); + } + + [InlineData(HashFormat.Base64)] + [InlineData(HashFormat.Hex)] + [Theory] + public async Task ActivePersistedQueries_SaveQuery_InvalidHash_MD5( + HashFormat format) + { + // arrange + var serviceCollection = new ServiceCollection(); + + // configure presistence + serviceCollection.AddGraphQLSchema(b => b + .AddDocumentFromString("type Query { foo: String }") + .AddResolver("Query", "foo", "bar")); + serviceCollection.AddQueryExecutor(b => b + .AddMD5DocumentHashProvider(format) + .UseActivePersistedQueryPipeline()); + + // add in-memory query storage + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(sp => + sp.GetRequiredService()); + serviceCollection.AddSingleton(sp => + sp.GetRequiredService()); + + IServiceProvider services = + serviceCollection.BuildServiceProvider(); + + IQueryExecutor executor = + services.GetRequiredService(); + + var hashProvider = + services.GetRequiredService(); + var storage = services.GetRequiredService(); + + var query = new QuerySourceText("{ foo }"); + string hash = hashProvider.ComputeHash(query.ToSpan()); + + // act and assert + IExecutionResult result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQueryName(hash) + .AddExtension("persistedQuery", + new Dictionary + { + { hashProvider.Name, hash } + }) + .Create()); + result.MatchSnapshot(new SnapshotNameExtension( + "Query_Not_Found_" + format)); + + result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQueryName(hash) + .SetQuery("{ foo }") + .AddExtension("persistedQuery", + new Dictionary + { + { hashProvider.Name, hash } + }) + .Create()); + result.MatchSnapshot(new SnapshotNameExtension( + "Query_Stored_" + format)); + + result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQueryName(hash) + .AddExtension("persistedQuery", + new Dictionary + { + { hashProvider.Name, hash } + }) + .Create()); + result.MatchSnapshot(new SnapshotNameExtension( + "Query_Loaded_From_Cache_" + format)); + } + [Fact] - public async Task ActivePersistedQueries_SaveQuery_InvalidHash() + public async Task ActivePersistedQueries_Invalid_Hash() { // arrange var serviceCollection = new ServiceCollection(); @@ -316,7 +545,7 @@ public async Task ActivePersistedQueries_SaveQuery_InvalidHash() .AddDocumentFromString("type Query { foo: String }") .AddResolver("Query", "foo", "bar")); serviceCollection.AddQueryExecutor(b => b - .AddSha256DocumentHashProvider() + .AddSha256DocumentHashProvider(HashFormat.Hex) .UseActivePersistedQueryPipeline()); // add in-memory query storage @@ -356,6 +585,103 @@ public async Task ActivePersistedQueries_SaveQuery_InvalidHash() result.ToJson().MatchSnapshot(); } + [Fact] + public async Task ActivePersistedQueries_Invalid_Hash_Type() + { + // arrange + var serviceCollection = new ServiceCollection(); + + // configure presistence + serviceCollection.AddGraphQLSchema(b => b + .AddDocumentFromString("type Query { foo: String }") + .AddResolver("Query", "foo", "bar")); + serviceCollection.AddQueryExecutor(b => b + .AddSha256DocumentHashProvider(HashFormat.Hex) + .UseActivePersistedQueryPipeline()); + + // add in-memory query storage + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(sp => + sp.GetRequiredService()); + serviceCollection.AddSingleton(sp => + sp.GetRequiredService()); + + IServiceProvider services = + serviceCollection.BuildServiceProvider(); + + IQueryExecutor executor = + services.GetRequiredService(); + + var hashProvider = + services.GetRequiredService(); + var storage = services.GetRequiredService(); + + var query = new QuerySourceText("{ foo }"); + string hash = hashProvider.ComputeHash(query.ToSpan()); + + // act + IExecutionResult result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery(query.Text) + .SetQueryName(hash) + .AddExtension("persistedQuery", + new Dictionary + { + { "sha1Hash", "123" } + }) + .Create()); + + // assert + Assert.False(storage.WrittenQuery.HasValue); + result.ToJson().MatchSnapshot(); + } + + [Fact] + public async Task ActivePersistedQueries_No_PersistedQuery_Section_In_Request() + { + // arrange + var serviceCollection = new ServiceCollection(); + + // configure presistence + serviceCollection.AddGraphQLSchema(b => b + .AddDocumentFromString("type Query { foo: String }") + .AddResolver("Query", "foo", "bar")); + serviceCollection.AddQueryExecutor(b => b + .AddSha256DocumentHashProvider(HashFormat.Hex) + .UseActivePersistedQueryPipeline()); + + // add in-memory query storage + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(sp => + sp.GetRequiredService()); + serviceCollection.AddSingleton(sp => + sp.GetRequiredService()); + + IServiceProvider services = + serviceCollection.BuildServiceProvider(); + + IQueryExecutor executor = + services.GetRequiredService(); + + var hashProvider = + services.GetRequiredService(); + var storage = services.GetRequiredService(); + + var query = new QuerySourceText("{ foo }"); + string hash = hashProvider.ComputeHash(query.ToSpan()); + + // act + IExecutionResult result = await executor.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery(query.Text) + .SetQueryName(hash) + .Create()); + + // assert + Assert.False(storage.WrittenQuery.HasValue); + result.ToJson().MatchSnapshot(); + } + public class InMemoryQueryStorage : IReadStoredQueries , IWriteStoredQueries diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_Invalid_Hash.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_Invalid_Hash.snap new file mode 100644 index 00000000000..af28268c7b1 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_Invalid_Hash.snap @@ -0,0 +1,14 @@ +{ + "data": { + "foo": "bar" + }, + "extensions": { + "persistedQuery": { + "sha256Hash": "123", + "expectedHashValue": "1a4eb6a25bda520ded59f5d74567463b72620c586ac0b4656bdbd4875f5ec5e5", + "expectedHashType": "sha256Hash", + "expectedHashFormat": "Hex", + "persisted": false + } + } +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_Invalid_Hash_Type.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_Invalid_Hash_Type.snap new file mode 100644 index 00000000000..a93f565381c --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_Invalid_Hash_Type.snap @@ -0,0 +1,14 @@ +{ + "data": { + "foo": "bar" + }, + "extensions": { + "persistedQuery": { + "sha256Hash": null, + "expectedHashValue": "1a4eb6a25bda520ded59f5d74567463b72620c586ac0b4656bdbd4875f5ec5e5", + "expectedHashType": "sha256Hash", + "expectedHashFormat": "Hex", + "persisted": false + } + } +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_No_PersistedQuery_Section_In_Request.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_No_PersistedQuery_Section_In_Request.snap new file mode 100644 index 00000000000..0ca61aab0dc --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_No_PersistedQuery_Section_In_Request.snap @@ -0,0 +1,5 @@ +{ + "data": { + "foo": "bar" + } +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Loaded_From_Cache_Base64.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Loaded_From_Cache_Base64.snap new file mode 100644 index 00000000000..b3d8c268e1d --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Loaded_From_Cache_Base64.snap @@ -0,0 +1,8 @@ +{ + "Data": { + "foo": "bar" + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Loaded_From_Cache_Hex.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Loaded_From_Cache_Hex.snap new file mode 100644 index 00000000000..b3d8c268e1d --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Loaded_From_Cache_Hex.snap @@ -0,0 +1,8 @@ +{ + "Data": { + "foo": "bar" + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Not_Found_Base64.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Not_Found_Base64.snap new file mode 100644 index 00000000000..63c97ebf115 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Not_Found_Base64.snap @@ -0,0 +1,17 @@ +{ + "Data": {}, + "Extensions": {}, + "Errors": [ + { + "Message": "PersistedQueryNotFound", + "Code": "PERSISTED_QUERY_NOT_FOUND", + "Path": null, + "Locations": [], + "Exception": null, + "Extensions": { + "code": "PERSISTED_QUERY_NOT_FOUND" + } + } + ], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Not_Found_Hex.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Not_Found_Hex.snap new file mode 100644 index 00000000000..63c97ebf115 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Not_Found_Hex.snap @@ -0,0 +1,17 @@ +{ + "Data": {}, + "Extensions": {}, + "Errors": [ + { + "Message": "PersistedQueryNotFound", + "Code": "PERSISTED_QUERY_NOT_FOUND", + "Path": null, + "Locations": [], + "Exception": null, + "Extensions": { + "code": "PERSISTED_QUERY_NOT_FOUND" + } + } + ], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Stored_Base64.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Stored_Base64.snap new file mode 100644 index 00000000000..1c97bf1513a --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Stored_Base64.snap @@ -0,0 +1,13 @@ +{ + "Data": { + "foo": "bar" + }, + "Extensions": { + "persistedQuery": { + "md5Hash": "uABiY2sImPW0idy8HUdmBg==", + "persisted": true + } + }, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Stored_Hex.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Stored_Hex.snap new file mode 100644 index 00000000000..618fc1df616 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_MD5_Query_Stored_Hex.snap @@ -0,0 +1,13 @@ +{ + "Data": { + "foo": "bar" + }, + "Extensions": { + "persistedQuery": { + "md5Hash": "b80062636b0898f5b489dcbc1d476606", + "persisted": true + } + }, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Loaded_From_Cache_Base64.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Loaded_From_Cache_Base64.snap new file mode 100644 index 00000000000..b3d8c268e1d --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Loaded_From_Cache_Base64.snap @@ -0,0 +1,8 @@ +{ + "Data": { + "foo": "bar" + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Loaded_From_Cache_Hex.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Loaded_From_Cache_Hex.snap new file mode 100644 index 00000000000..b3d8c268e1d --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Loaded_From_Cache_Hex.snap @@ -0,0 +1,8 @@ +{ + "Data": { + "foo": "bar" + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Not_Found_Base64.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Not_Found_Base64.snap new file mode 100644 index 00000000000..63c97ebf115 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Not_Found_Base64.snap @@ -0,0 +1,17 @@ +{ + "Data": {}, + "Extensions": {}, + "Errors": [ + { + "Message": "PersistedQueryNotFound", + "Code": "PERSISTED_QUERY_NOT_FOUND", + "Path": null, + "Locations": [], + "Exception": null, + "Extensions": { + "code": "PERSISTED_QUERY_NOT_FOUND" + } + } + ], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Not_Found_Hex.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Not_Found_Hex.snap new file mode 100644 index 00000000000..63c97ebf115 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Not_Found_Hex.snap @@ -0,0 +1,17 @@ +{ + "Data": {}, + "Extensions": {}, + "Errors": [ + { + "Message": "PersistedQueryNotFound", + "Code": "PERSISTED_QUERY_NOT_FOUND", + "Path": null, + "Locations": [], + "Exception": null, + "Extensions": { + "code": "PERSISTED_QUERY_NOT_FOUND" + } + } + ], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Stored_Base64.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Stored_Base64.snap new file mode 100644 index 00000000000..fd7aeed00a3 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Stored_Base64.snap @@ -0,0 +1,13 @@ +{ + "Data": { + "foo": "bar" + }, + "Extensions": { + "persistedQuery": { + "sha1Hash": "jD7lS7Fj3hzlVV7YGvJSPrSr3sk=", + "persisted": true + } + }, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Stored_Hex.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Stored_Hex.snap new file mode 100644 index 00000000000..596714066c0 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha1_Query_Stored_Hex.snap @@ -0,0 +1,13 @@ +{ + "Data": { + "foo": "bar" + }, + "Extensions": { + "persistedQuery": { + "sha1Hash": "8c3ee54bb163de1ce5555ed81af2523eb4abdec9", + "persisted": true + } + }, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Loaded_From_Cache_Base64.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Loaded_From_Cache_Base64.snap new file mode 100644 index 00000000000..b3d8c268e1d --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Loaded_From_Cache_Base64.snap @@ -0,0 +1,8 @@ +{ + "Data": { + "foo": "bar" + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Loaded_From_Cache_Hex.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Loaded_From_Cache_Hex.snap new file mode 100644 index 00000000000..b3d8c268e1d --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Loaded_From_Cache_Hex.snap @@ -0,0 +1,8 @@ +{ + "Data": { + "foo": "bar" + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Not_Found_Base64.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Not_Found_Base64.snap new file mode 100644 index 00000000000..63c97ebf115 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Not_Found_Base64.snap @@ -0,0 +1,17 @@ +{ + "Data": {}, + "Extensions": {}, + "Errors": [ + { + "Message": "PersistedQueryNotFound", + "Code": "PERSISTED_QUERY_NOT_FOUND", + "Path": null, + "Locations": [], + "Exception": null, + "Extensions": { + "code": "PERSISTED_QUERY_NOT_FOUND" + } + } + ], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Not_Found_Hex.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Not_Found_Hex.snap new file mode 100644 index 00000000000..63c97ebf115 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Not_Found_Hex.snap @@ -0,0 +1,17 @@ +{ + "Data": {}, + "Extensions": {}, + "Errors": [ + { + "Message": "PersistedQueryNotFound", + "Code": "PERSISTED_QUERY_NOT_FOUND", + "Path": null, + "Locations": [], + "Exception": null, + "Extensions": { + "code": "PERSISTED_QUERY_NOT_FOUND" + } + } + ], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Stored_Base64.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Stored_Base64.snap new file mode 100644 index 00000000000..a54467b2f96 --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Stored_Base64.snap @@ -0,0 +1,13 @@ +{ + "Data": { + "foo": "bar" + }, + "Extensions": { + "persistedQuery": { + "sha256Hash": "Gk62olvaUg3tWfXXRWdGO3JiDFhqwLRla9vUh19exeU=", + "persisted": true + } + }, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Stored_Hex.snap b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Stored_Hex.snap new file mode 100644 index 00000000000..2273859893c --- /dev/null +++ b/src/Core/Core.Tests/Execution/__snapshots__/PersistedQueriesTests.ActivePersistedQueries_SaveQuery_InvalidHash_Sha256_Query_Stored_Hex.snap @@ -0,0 +1,13 @@ +{ + "Data": { + "foo": "bar" + }, + "Extensions": { + "persistedQuery": { + "sha256Hash": "1a4eb6a25bda520ded59f5d74567463b72620c586ac0b4656bdbd4875f5ec5e5", + "persisted": true + } + }, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Core/Execution/Extensions/QueryExecutionBuilderExtensions.cs b/src/Core/Core/Execution/Extensions/QueryExecutionBuilderExtensions.cs index b3ea13848fa..602d4230b1b 100644 --- a/src/Core/Core/Execution/Extensions/QueryExecutionBuilderExtensions.cs +++ b/src/Core/Core/Execution/Extensions/QueryExecutionBuilderExtensions.cs @@ -709,32 +709,44 @@ public static IQueryExecutionBuilder AddDefaultDocumentHashProvider( } public static IQueryExecutionBuilder AddSha1DocumentHashProvider( - this IQueryExecutionBuilder builder) + this IQueryExecutionBuilder builder) => + builder.AddSha1DocumentHashProvider(HashFormat.Base64); + + public static IQueryExecutionBuilder AddSha256DocumentHashProvider( + this IQueryExecutionBuilder builder) => + builder.AddSha256DocumentHashProvider(HashFormat.Base64); + + public static IQueryExecutionBuilder AddMD5DocumentHashProvider( + this IQueryExecutionBuilder builder) => + builder.AddMD5DocumentHashProvider(HashFormat.Base64); + + public static IQueryExecutionBuilder AddSha1DocumentHashProvider( + this IQueryExecutionBuilder builder, + HashFormat format) { builder.RemoveService(); - builder.Services.AddSingleton< - IDocumentHashProvider, - Sha1DocumentHashProvider>(); + builder.Services.AddSingleton( + new Sha1DocumentHashProvider(format)); return builder; } public static IQueryExecutionBuilder AddSha256DocumentHashProvider( - this IQueryExecutionBuilder builder) + this IQueryExecutionBuilder builder, + HashFormat format) { builder.RemoveService(); - builder.Services.AddSingleton< - IDocumentHashProvider, - Sha256DocumentHashProvider>(); + builder.Services.AddSingleton( + new Sha256DocumentHashProvider(format)); return builder; } public static IQueryExecutionBuilder AddMD5DocumentHashProvider( - this IQueryExecutionBuilder builder) + this IQueryExecutionBuilder builder, + HashFormat format) { builder.RemoveService(); - builder.Services.AddSingleton< - IDocumentHashProvider, - MD5DocumentHashProvider>(); + builder.Services.AddSingleton( + new MD5DocumentHashProvider(format)); return builder; } diff --git a/src/Core/Core/Execution/Middleware/WritePersistedQueryMiddleware.cs b/src/Core/Core/Execution/Middleware/WritePersistedQueryMiddleware.cs index 8cb8d3fb468..6c85d137d79 100644 --- a/src/Core/Core/Execution/Middleware/WritePersistedQueryMiddleware.cs +++ b/src/Core/Core/Execution/Middleware/WritePersistedQueryMiddleware.cs @@ -10,9 +10,14 @@ internal sealed class WritePersistedQueryMiddleware { private const string _persistedQuery = "persistedQuery"; private const string _persisted = "persisted"; + private const string _expectedValue = "expectedHashValue"; + private const string _expectedType = "expectedHashType"; + private const string _expectedFormat = "expectedHashFormat"; + private const string _hash = "hash"; private readonly QueryDelegate _next; private readonly IWriteStoredQueries _writeStoredQueries; private readonly string _hashName; + private readonly HashFormat _hashFormat; public WritePersistedQueryMiddleware( QueryDelegate next, @@ -29,6 +34,7 @@ public WritePersistedQueryMiddleware( _writeStoredQueries = writeStoredQueries ?? throw new ArgumentNullException(nameof(writeStoredQueries)); _hashName = documentHashProvider.Name; + _hashFormat = documentHashProvider.Format; } public async Task InvokeAsync(IQueryContext context) @@ -45,31 +51,42 @@ public async Task InvokeAsync(IQueryContext context) if (_writeStoredQueries != null && context.Request.Query != null && context.QueryKey != null - && DoHashesMatch(context, _hashName)) + && context.Result is QueryResult result + && context.Request.Extensions != null + && context.Request.Extensions.TryGetValue(_persistedQuery, out var s) + && s is IReadOnlyDictionary settings) { - // save the query - await _writeStoredQueries.WriteQueryAsync( - context.QueryKey, - context.Request.Query) - .ConfigureAwait(false); + // hash is found and matches the query key -> store the query + if (DoHashesMatch(settings, context.QueryKey, _hashName, out string userHash)) + { + // save the query + await _writeStoredQueries.WriteQueryAsync( + context.QueryKey, + context.Request.Query) + .ConfigureAwait(false); - // add persistence receipt to the result - if (context.Result is QueryResult result - && context.Request.Extensions.TryGetValue( - _persistedQuery, out var s) - && s is IReadOnlyDictionary settings - && settings.TryGetValue(_hashName, out object h) - && h is string hash) + // add persistence receipt to the result + result.Extensions[_persistedQuery] = + new Dictionary + { + { _hashName, userHash }, + { _persisted, true } + }; + + context.ContextData[ContextDataKeys.DocumentSaved] = true; + } + else { result.Extensions[_persistedQuery] = new Dictionary { - { _hashName, hash }, - { _persisted, true } + { _hashName, userHash }, + { _expectedValue, context.QueryKey }, + { _expectedType, _hashName }, + { _expectedFormat, _hashFormat.ToString() }, + { _persisted, false } }; } - - context.ContextData[ContextDataKeys.DocumentSaved] = true; } await _next(context).ConfigureAwait(false); @@ -81,17 +98,19 @@ private static bool IsContextIncomplete(IQueryContext context) } private static bool DoHashesMatch( - IQueryContext context, - string hashName) + IReadOnlyDictionary settings, + string expectedHash, + string hashName, + out string userHash) { - if (context.Request.Extensions.TryGetValue( - _persistedQuery, out var s) - && s is IReadOnlyDictionary settings - && settings.TryGetValue(hashName, out object h) + if (settings.TryGetValue(hashName, out object h) && h is string hash) { - return hash.Equals(context.QueryKey, StringComparison.Ordinal); + userHash = hash; + return hash.Equals(expectedHash, StringComparison.Ordinal); } + + userHash = null; return false; } } diff --git a/src/Core/Core/TestSettings.cs b/src/Core/Core/Properties/InternalsVisibleTo.cs similarity index 100% rename from src/Core/Core/TestSettings.cs rename to src/Core/Core/Properties/InternalsVisibleTo.cs diff --git a/src/Core/Language.Tests/Parser/Utf8GraphQLRequestParserTests.cs b/src/Core/Language.Tests/Parser/Utf8GraphQLRequestParserTests.cs index 167a7707563..34c46ec39c1 100644 --- a/src/Core/Language.Tests/Parser/Utf8GraphQLRequestParserTests.cs +++ b/src/Core/Language.Tests/Parser/Utf8GraphQLRequestParserTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Security.Cryptography; using System.Collections.Generic; using System.IO; using System.Text; @@ -107,13 +109,19 @@ public void Parse_Kitchen_Sink_Query_With_Russion_Escaped_Characters() public void Parse_Kitchen_Sink_Query_With_Cache() { // arrange + var request = new GraphQLRequestDto + { + Query = FileResource.Open("kitchen-sink.graphql") + .NormalizeLineBreaks() + }; + + byte[] buffer = Encoding.UTF8.GetBytes(request.Query); + string expectedHash = Convert.ToBase64String( + SHA1.Create().ComputeHash(buffer)); + byte[] source = Encoding.UTF8.GetBytes( - JsonConvert.SerializeObject( - new GraphQLRequestDto - { - Query = FileResource.Open("kitchen-sink.graphql") - .NormalizeLineBreaks() - }).NormalizeLineBreaks()); + JsonConvert.SerializeObject(request + ).NormalizeLineBreaks()); var cache = new DocumentCache(); @@ -145,7 +153,7 @@ public void Parse_Kitchen_Sink_Query_With_Cache() Assert.Null(r.Variables); Assert.Null(r.Extensions); - Assert.Equal("KwPz8bJWrVDRrtFPjW2sh5CUQwE=", r.QueryName); + Assert.Equal(expectedHash, r.QueryName); QuerySyntaxSerializer.Serialize(r.Query, true) .MatchSnapshot(); }); @@ -155,14 +163,20 @@ public void Parse_Kitchen_Sink_Query_With_Cache() public void Parse_Skip_Custom_Property() { // arrange + var request = new CustomGraphQLRequestDto + { + CustomProperty = "FooBar", + Query = FileResource.Open("kitchen-sink.graphql") + .NormalizeLineBreaks() + }; + byte[] source = Encoding.UTF8.GetBytes( - JsonConvert.SerializeObject( - new CustomGraphQLRequestDto - { - CustomProperty = "FooBar", - Query = FileResource.Open("kitchen-sink.graphql") - .NormalizeLineBreaks() - }).NormalizeLineBreaks()); + JsonConvert.SerializeObject(request + ).NormalizeLineBreaks()); + + byte[] buffer = Encoding.UTF8.GetBytes(request.Query); + string expectedHash = Convert.ToBase64String( + SHA1.Create().ComputeHash(buffer)); var cache = new DocumentCache(); @@ -183,7 +197,7 @@ public void Parse_Skip_Custom_Property() Assert.Null(r.Variables); Assert.Null(r.Extensions); - Assert.Equal("KwPz8bJWrVDRrtFPjW2sh5CUQwE=", r.QueryName); + Assert.Equal(expectedHash, r.QueryName); QuerySyntaxSerializer.Serialize(r.Query, true) .MatchSnapshot(); }); @@ -193,14 +207,20 @@ public void Parse_Skip_Custom_Property() public void Parse_Id_As_Name() { // arrange + var request = new RelayGraphQLRequestDto + { + Id = "FooBar", + Query = FileResource.Open("kitchen-sink.graphql") + .NormalizeLineBreaks() + }; + byte[] source = Encoding.UTF8.GetBytes( - JsonConvert.SerializeObject( - new RelayGraphQLRequestDto - { - Id = "FooBar", - Query = FileResource.Open("kitchen-sink.graphql") - .NormalizeLineBreaks() - }).NormalizeLineBreaks()); + JsonConvert.SerializeObject(request + ).NormalizeLineBreaks()); + + byte[] buffer = Encoding.UTF8.GetBytes(request.Query); + string expectedHash = Convert.ToBase64String( + SHA1.Create().ComputeHash(buffer)); var cache = new DocumentCache(); @@ -222,7 +242,7 @@ public void Parse_Id_As_Name() Assert.Null(r.Extensions); Assert.Equal("FooBar", r.QueryName); - Assert.Equal("KwPz8bJWrVDRrtFPjW2sh5CUQwE=", r.QueryHash); + Assert.Equal(expectedHash, r.QueryHash); QuerySyntaxSerializer.Serialize(r.Query, true) .MatchSnapshot(); }); @@ -473,6 +493,42 @@ public void Parse_Apollo_AQP_SignatureQuery_Variables_Without_Values() }); } + [Fact] + public void Parse_Apollo_AQP_FullRequest_And_Verify_Hash() + { + // arrange + byte[] source = Encoding.UTF8.GetBytes( + FileResource.Open("Apollo_AQP_FullRequest.json") + .NormalizeLineBreaks()); + + // act + var parserOptions = new ParserOptions(); + var requestParser = new Utf8GraphQLRequestParser( + source, + parserOptions, + new DocumentCache(), + new Sha256DocumentHashProvider(HashFormat.Hex)); + IReadOnlyList batch = requestParser.Parse(); + + // assert + Assert.Collection(batch, + r => + { + Assert.Null(r.OperationName); + Assert.Empty(r.Variables); + Assert.True(r.Extensions.ContainsKey("persistedQuery")); + Assert.NotNull(r.Query); + + if (r.Extensions.TryGetValue("persistedQuery", out object o) + && o is IReadOnlyDictionary persistedQuery + && persistedQuery.TryGetValue("sha256Hash", out o) + && o is string hash) + { + Assert.Equal(hash, r.QueryHash); + } + }); + } + private class GraphQLRequestDto { [JsonProperty("operationName")] diff --git a/src/Core/Language.Tests/__resources__/Apollo_AQP_FullRequest.json b/src/Core/Language.Tests/__resources__/Apollo_AQP_FullRequest.json new file mode 100644 index 00000000000..003455bbd56 --- /dev/null +++ b/src/Core/Language.Tests/__resources__/Apollo_AQP_FullRequest.json @@ -0,0 +1,11 @@ +{ + "operationName": null, + "variables": {}, + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "0f1dbd90e1cf63cc96c4ebb3d0a9813ee387be56bc291704d8b46d68283d0847" + } + }, + "query": "{\n Services {\n id\n name\n domain\n poc\n owner\n description\n tags\n versions {\n notes\n version\n state\n url\n dependencies {\n id\n name\n domain\n poc\n owner\n description\n __typename\n }\n __typename\n }\n __typename\n }\n}\n" +} diff --git a/src/Core/Language/Parser/DocumentHashProviderBase.cs b/src/Core/Language/Parser/DocumentHashProviderBase.cs new file mode 100644 index 00000000000..df7edfacd3d --- /dev/null +++ b/src/Core/Language/Parser/DocumentHashProviderBase.cs @@ -0,0 +1,49 @@ +using System; +using System.Buffers; + +namespace HotChocolate.Language +{ + public abstract class DocumentHashProviderBase + : IDocumentHashProvider + { + internal DocumentHashProviderBase(HashFormat format) + { + Format = format; + } + + public abstract string Name { get; } + + public HashFormat Format { get; } + + public string ComputeHash(ReadOnlySpan document) + { + // TODO : with netcoreapp 3.0 we do not need that anymore. + byte[] rented = ArrayPool.Shared.Rent(document.Length); + document.CopyTo(rented); + + try + { + byte[] hash = ComputeHash(rented, document.Length); + + switch (Format) + { + case HashFormat.Base64: + return Convert.ToBase64String(hash); + case HashFormat.Hex: + return BitConverter.ToString(hash) + .ToLowerInvariant() + .Replace("-", string.Empty); + default: + throw new NotSupportedException( + "The specified has format is not supported."); + } + } + finally + { + ArrayPool.Shared.Return(rented); + } + } + + protected abstract byte[] ComputeHash(byte[] document, int length); + } +} diff --git a/src/Core/Language/Parser/HashRepresentation.cs b/src/Core/Language/Parser/HashRepresentation.cs new file mode 100644 index 00000000000..93aa0670725 --- /dev/null +++ b/src/Core/Language/Parser/HashRepresentation.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Language +{ + public enum HashFormat + { + Base64, + Hex + } +} diff --git a/src/Core/Language/Parser/IDocumentHashProvider.cs b/src/Core/Language/Parser/IDocumentHashProvider.cs index dd05ff52ebf..de8d4fa4342 100644 --- a/src/Core/Language/Parser/IDocumentHashProvider.cs +++ b/src/Core/Language/Parser/IDocumentHashProvider.cs @@ -6,6 +6,8 @@ public interface IDocumentHashProvider { string Name { get; } + HashFormat Format { get; } + string ComputeHash(ReadOnlySpan document); } } diff --git a/src/Core/Language/Parser/MD5DocumentHashProvider.cs b/src/Core/Language/Parser/MD5DocumentHashProvider.cs index e6b4a280d7d..d1d8c7b28c8 100644 --- a/src/Core/Language/Parser/MD5DocumentHashProvider.cs +++ b/src/Core/Language/Parser/MD5DocumentHashProvider.cs @@ -1,34 +1,29 @@ -using System; -using System.Buffers; using System.Threading; using System.Security.Cryptography; namespace HotChocolate.Language { public class MD5DocumentHashProvider - : IDocumentHashProvider + : DocumentHashProviderBase { private readonly ThreadLocal _md5 = new ThreadLocal(() => MD5.Create()); - public string Name => "md5Hash"; + public MD5DocumentHashProvider() + : this(HashFormat.Base64) + { + } - public string ComputeHash(ReadOnlySpan document) + public MD5DocumentHashProvider(HashFormat format) + : base(format) { - // TODO : with netcoreapp 3.0 we do not need that anymore. - byte[] rented = ArrayPool.Shared.Rent(document.Length); - document.CopyTo(rented); + } - try - { - byte[] hash = _md5.Value.ComputeHash( - rented, 0, document.Length); - return Convert.ToBase64String(hash); - } - finally - { - ArrayPool.Shared.Return(rented); - } + public override string Name => "md5Hash"; + + protected override byte[] ComputeHash(byte[] document, int length) + { + return _md5.Value.ComputeHash(document, 0, length); } } } diff --git a/src/Core/Language/Parser/Sha1DocumentHashProvider.cs b/src/Core/Language/Parser/Sha1DocumentHashProvider.cs index 22c9d242239..4ba11da9a0c 100644 --- a/src/Core/Language/Parser/Sha1DocumentHashProvider.cs +++ b/src/Core/Language/Parser/Sha1DocumentHashProvider.cs @@ -1,34 +1,29 @@ -using System; -using System.Buffers; using System.Threading; using System.Security.Cryptography; namespace HotChocolate.Language { public class Sha1DocumentHashProvider - : IDocumentHashProvider + : DocumentHashProviderBase { private readonly ThreadLocal _sha = new ThreadLocal(() => SHA1.Create()); - public string Name => "sha1Hash"; + public Sha1DocumentHashProvider() + : this(HashFormat.Base64) + { + } - public string ComputeHash(ReadOnlySpan document) + public Sha1DocumentHashProvider(HashFormat format) + : base(format) { - // TODO : with netcoreapp 3.0 we do not need that anymore. - byte[] rented = ArrayPool.Shared.Rent(document.Length); - document.CopyTo(rented); + } - try - { - byte[] hash = _sha.Value.ComputeHash( - rented, 0, document.Length); - return Convert.ToBase64String(hash); - } - finally - { - ArrayPool.Shared.Return(rented); - } + public override string Name => "sha1Hash"; + + protected override byte[] ComputeHash(byte[] document, int length) + { + return _sha.Value.ComputeHash(document, 0, length); } } } diff --git a/src/Core/Language/Parser/Sha256DocumentHashProvider.cs b/src/Core/Language/Parser/Sha256DocumentHashProvider.cs index f304944118c..962c1f69f99 100644 --- a/src/Core/Language/Parser/Sha256DocumentHashProvider.cs +++ b/src/Core/Language/Parser/Sha256DocumentHashProvider.cs @@ -1,34 +1,29 @@ -using System; -using System.Buffers; using System.Threading; using System.Security.Cryptography; namespace HotChocolate.Language { public class Sha256DocumentHashProvider - : IDocumentHashProvider + : DocumentHashProviderBase { private readonly ThreadLocal _sha = new ThreadLocal(() => SHA256.Create()); - public string Name => "sha256Hash"; + public Sha256DocumentHashProvider() + : this(HashFormat.Base64) + { + } - public string ComputeHash(ReadOnlySpan document) + public Sha256DocumentHashProvider(HashFormat format) + : base(format) { - // TODO : with netcoreapp 3.0 we do not need that anymore. - byte[] rented = ArrayPool.Shared.Rent(document.Length); - document.CopyTo(rented); + } - try - { - byte[] hash = _sha.Value.ComputeHash( - rented, 0, document.Length); - return Convert.ToBase64String(hash); - } - finally - { - ArrayPool.Shared.Return(rented); - } + public override string Name => "sha256Hash"; + + protected override byte[] ComputeHash(byte[] document, int length) + { + return _sha.Value.ComputeHash(document, 0, length); } } } diff --git a/src/Core/Language/Parser/Utf8GraphQLRequestParser.Request.cs b/src/Core/Language/Parser/Utf8GraphQLRequestParser.Request.cs index f0da2355f40..c0939023b31 100644 --- a/src/Core/Language/Parser/Utf8GraphQLRequestParser.Request.cs +++ b/src/Core/Language/Parser/Utf8GraphQLRequestParser.Request.cs @@ -20,6 +20,8 @@ private ref struct Request public IReadOnlyDictionary Variables { get; set; } public IReadOnlyDictionary Extensions { get; set; } + + public DocumentNode Document { get; set; } } } } diff --git a/src/Core/Language/Parser/Utf8GraphQLRequestParser.cs b/src/Core/Language/Parser/Utf8GraphQLRequestParser.cs index de2d10c080b..3d993b85cc2 100644 --- a/src/Core/Language/Parser/Utf8GraphQLRequestParser.cs +++ b/src/Core/Language/Parser/Utf8GraphQLRequestParser.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Buffers; using HotChocolate.Language.Properties; +using System.Text; namespace HotChocolate.Language { @@ -149,41 +150,14 @@ private GraphQLRequest ParseRequest() } } - DocumentNode document = null; - if (request.HasQuery) { - if (_useCache) - { - if (request.QueryName is null) - { - request.QueryName = - request.QueryHash = - _hashProvider.ComputeHash(request.Query); - } - - if (!_cache.TryGetDocument( - request.QueryName, - out document)) - { - document = ParseQuery(in request); - - if (request.QueryHash is null) - { - request.QueryHash = - _hashProvider.ComputeHash(request.Query); - } - } - } - else - { - document = ParseQuery(in request); - } + ParseQuery(ref request); } return new GraphQLRequest ( - document, + request.Document, request.QueryName, request.QueryHash, request.OperationName, @@ -324,7 +298,7 @@ private void ParseMessageProperty(ref Message message) } } - private DocumentNode ParseQuery(in Request request) + private void ParseQuery(ref Request request) { int length = checked(request.Query.Length); bool useStackalloc = @@ -336,10 +310,40 @@ private DocumentNode ParseQuery(in Request request) ? stackalloc byte[length] : (unescapedArray = ArrayPool.Shared.Rent(length)); + DocumentNode document = null; + try { Utf8Helper.Unescape(request.Query, ref unescapedSpan, false); - return Utf8GraphQLParser.Parse(unescapedSpan, _options); + + if (_useCache) + { + if (request.QueryName is null) + { + request.QueryName = + request.QueryHash = + _hashProvider.ComputeHash(unescapedSpan); + } + + if (!_cache.TryGetDocument( + request.QueryName, + out document)) + { + document = Utf8GraphQLParser.Parse(unescapedSpan, _options); + + if (request.QueryHash is null) + { + request.QueryHash = + _hashProvider.ComputeHash(unescapedSpan); + } + } + } + else + { + document = Utf8GraphQLParser.Parse(unescapedSpan, _options); + } + + request.Document = document; } finally {