diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs index 0d39bf04578..9c02a7321b9 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs @@ -81,6 +81,51 @@ void Action() => helper.CoerceVariableValues( Assert.Throws(Action); } + [Fact] + public void Coerce_String_WithEscapedQuotes_IsUnescaped() + { + // arrange + var schema = SchemaBuilder.New().AddStarWarsTypes().Create(); + + var variableDefinitions = new List + { + new VariableDefinitionNode( + null, + new VariableNode("abc"), + description: null, + new NamedTypeNode("String"), + null, + Array.Empty()) + }; + + using var variableValues = JsonDocument.Parse( + """ + { + "abc": "tag:\"type_portable-lamp\"" + } + """); + + var coercedValues = new Dictionary(); + var featureProvider = new MockFeatureProvider(); + var helper = new VariableCoercionHelper(new()); + + // act + helper.CoerceVariableValues( + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); + + // assert + Assert.Collection(coercedValues, + t => + { + Assert.Equal("abc", t.Key); + Assert.Equal("String", Assert.IsType(t.Value.Type).Name); + Assert.Equal("tag:\"type_portable-lamp\"", t.Value.RuntimeValue); + Assert.Equal( + "tag:\"type_portable-lamp\"", + Assert.IsType(t.Value.ValueLiteral).Value); + }); + } + [Fact] public void Coerce_Nullable_String_Variable_With_Default_Where_Value_Is_Not_Provided() { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs index 35ef6c4312b..ffcc9145394 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs @@ -435,17 +435,10 @@ private readonly IValueNode ParseLiteral(JsonElement element, int depth) return BooleanValueNode.False; case JsonValueKind.String: - { - var rawValue = element.GetRawText(); - var utf8Value = System.Text.Encoding.UTF8.GetBytes(rawValue); - var span = utf8Value.AsSpan(); - span = span[1..^1]; // Remove quotes - var segment = WriteValue(span); - return new StringValueNode(null, segment, false); - } + var stringValue = element.GetString()!; + return new StringValueNode(null, stringValue, false); case JsonValueKind.Number: - { var rawValue = element.GetRawText(); var utf8Value = System.Text.Encoding.UTF8.GetBytes(rawValue); var span = utf8Value.AsSpan(); @@ -462,7 +455,6 @@ private readonly IValueNode ParseLiteral(JsonElement element, int depth) } return new IntValueNode(segment); - } case JsonValueKind.Array: { diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/VariableCoercionTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/VariableCoercionTests.cs index c36d5ac2852..3e6397888df 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/VariableCoercionTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/VariableCoercionTests.cs @@ -1,10 +1,46 @@ using HotChocolate.Transport; using HotChocolate.Transport.Http; +using Microsoft.Extensions.DependencyInjection; namespace HotChocolate.Fusion; public class VariableCoercionTests : FusionTestBase { + [Fact] + public async Task String_With_Quotes() + { + // arrange + using var serverA = CreateSourceSchema( + "A", + r => r.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", serverA) + ]); + + // act + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + """ + query testQuery($input: String!) { + field(input: $input) + } + """, + variables: new Dictionary + { + ["input"] = "tag:\"type_portable-lamp\"" + }); + + using var result = await client.PostAsync( + request, + new Uri("http://localhost:5000/graphql")); + + // assert + await MatchSnapshotAsync(gateway, request, result); + } + [Fact] public async Task InputObject_Invalid_Field() { @@ -418,4 +454,12 @@ query testQuery($pet: Pet!) { // assert await MatchSnapshotAsync(gateway, request, result); } + + public static class SourceSchema1 + { + public class Query + { + public string GetField(string input) => input; + } + } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/VariableCoercionTests.String_With_Quotes.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/VariableCoercionTests.String_With_Quotes.yaml new file mode 100644 index 00000000000..c3829b752bb --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/VariableCoercionTests.String_With_Quotes.yaml @@ -0,0 +1,73 @@ +title: String_With_Quotes +request: + document: | + query testQuery( + $input: String! + ) { + field(input: $input) + } + variables: | + { + "input": "tag:\u0022type_portable-lamp\u0022" + } +response: + body: | + { + "data": { + "field": "tag:\u0022type_portable-lamp\u0022" + } + } +sourceSchemas: + - name: A + schema: | + schema { + query: Query + } + + type Query { + field(input: String!): String! + } + interactions: + - request: + document: | + query testQuery_9dd2f586_1( + $input: String! + ) { + field(input: $input) + } + variables: | + { + "input": "tag:\u0022type_portable-lamp\u0022" + } + response: + results: + - | + { + "data": { + "field": "tag:\u0022type_portable-lamp\u0022" + } + } +operationPlan: + operation: + - document: | + query testQuery( + $input: String! + ) { + field(input: $input) + } + name: testQuery + hash: 9dd2f5869cf5d627883a05994b98b50a + searchSpace: 1 + expandedNodes: 1 + nodes: + - id: 1 + type: Operation + schema: A + operation: | + query testQuery_9dd2f586_1( + $input: String! + ) { + field(input: $input) + } + forwardedVariables: + - input diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/VariableCoercionHelperTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/VariableCoercionHelperTests.cs new file mode 100644 index 00000000000..c58240f3e38 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/VariableCoercionHelperTests.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using HotChocolate.Features; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Execution; + +public class VariableCoercionHelperTests : FusionTestBase +{ + [Fact] + public void Coerce_String_WithEscapedQuotes_IsUnescaped() + { + // arrange + var schema = CreateCompositeSchema(); + + var variableDefinitions = new List + { + new VariableDefinitionNode( + null, + new VariableNode("abc"), + description: null, + new NamedTypeNode("String"), + null, + Array.Empty()) + }; + + using var variableValues = JsonDocument.Parse( + """ + { + "abc": "tag:\"type_portable-lamp\"" + } + """); + + // act + var success = VariableCoercionHelper.TryCoerceVariableValues( + new MockFeatureProvider(), + schema, + variableDefinitions, + variableValues.RootElement, + out var coercedVariableValues, + out var error); + + // assert + Assert.True(success, error?.Message); + Assert.Null(error); + Assert.NotNull(coercedVariableValues); + + var stringValue = Assert.IsType(coercedVariableValues["abc"].Value); + Assert.Equal("tag:\"type_portable-lamp\"", stringValue.Value); + } + + private sealed class MockFeatureProvider : IFeatureProvider + { + public IFeatureCollection Features { get; } = new FeatureCollection(); + } +} diff --git a/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs b/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs index d5a20da7fe4..cdbefcd2335 100644 --- a/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs +++ b/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs @@ -90,15 +90,10 @@ internal IValueNode Parse(JsonElement element, int depth) return BooleanValueNode.False; case JsonValueKind.String: - { - var value = JsonMarshal.GetRawUtf8Value(element); - value = value.Slice(1, value.Length - 2); // Remove quotes. - var segment = WriteValue(value); - return new StringValueNode(null, segment, false); - } + var stringValue = element.GetString()!; + return new StringValueNode(null, stringValue, false); case JsonValueKind.Number: - { var value = JsonMarshal.GetRawUtf8Value(element); var segment = WriteValue(value); @@ -113,7 +108,6 @@ internal IValueNode Parse(JsonElement element, int depth) } return new IntValueNode(segment); - } case JsonValueKind.Array: { diff --git a/src/HotChocolate/Language/test/Language.Web.Tests/JsonValueParserTests.cs b/src/HotChocolate/Language/test/Language.Web.Tests/JsonValueParserTests.cs new file mode 100644 index 00000000000..63beb4c680b --- /dev/null +++ b/src/HotChocolate/Language/test/Language.Web.Tests/JsonValueParserTests.cs @@ -0,0 +1,24 @@ +using System.Text.Json; + +namespace HotChocolate.Language; + +public class JsonValueParserTests +{ + [Fact] + public void Parse_JsonElement_StringWithEscapedQuotes_IsUnescaped() + { + // arrange + using var document = JsonDocument.Parse( + """ + "tag:\"type_portable-lamp\"" + """); + var parser = new JsonValueParser(); + + // act + var value = parser.Parse(document.RootElement); + + // assert + var stringValue = Assert.IsType(value); + Assert.Equal("tag:\"type_portable-lamp\"", stringValue.Value); + } +}