diff --git a/src/CHttpExecutor/CHttpExecutor.csproj b/src/CHttpExecutor/CHttpExecutor.csproj index f6b6cb6..77c667a 100644 --- a/src/CHttpExecutor/CHttpExecutor.csproj +++ b/src/CHttpExecutor/CHttpExecutor.csproj @@ -22,7 +22,11 @@ - + + + + + diff --git a/src/CHttpExecutor/VariablePostProcessingWriterStrategy.cs b/src/CHttpExecutor/VariablePostProcessingWriterStrategy.cs index 53672f6..8c095a5 100644 --- a/src/CHttpExecutor/VariablePostProcessingWriterStrategy.cs +++ b/src/CHttpExecutor/VariablePostProcessingWriterStrategy.cs @@ -33,14 +33,14 @@ public VariablePostProcessingWriterStrategy(bool enabled) public async Task CompleteAsync(CancellationToken token) { - await _pipe.Reader.CopyToAsync(Content, token); + if (Enabled) + await _pipe.Reader.CopyToAsync(Content, token); IsCompleted = true; Content.Seek(0, SeekOrigin.Begin); } public ValueTask DisposeAsync() { - Enabled = true; return ValueTask.CompletedTask; } diff --git a/src/CHttpExecutor/VariablePreprocessor.cs b/src/CHttpExecutor/VariablePreprocessor.cs index 6e86334..0e90990 100644 --- a/src/CHttpExecutor/VariablePreprocessor.cs +++ b/src/CHttpExecutor/VariablePreprocessor.cs @@ -1,7 +1,9 @@ using System.Buffers; -using System.Runtime.CompilerServices; using System.Text.Json; +using System.Text.Json.Nodes; using CHttp.Writers; +using Json.More; +using Json.Path; namespace CHttpExecutor; @@ -118,20 +120,24 @@ private static bool TryGetPathValue(ReadOnlySpan jsonPath, VariablePostPro return ParseHeaderValue(jsonPath.Slice(headersPart.Length), responseCtx, ref result); if (jsonPath.StartsWith(bodyPart, StringComparison.OrdinalIgnoreCase)) - return ParseBody(jsonPath.Slice(bodyPart.Length), responseCtx, ref result); + return ParseBody(jsonPath.Slice(bodyPart.Length - 1), responseCtx, ref result); return false; } - private static bool ParseBody(ReadOnlySpan jsonPath, VariablePostProcessingWriterStrategy responseCtx, ref string result) + private static bool ParseBody_Custom(ReadOnlySpan jsonPath, VariablePostProcessingWriterStrategy responseCtx, ref string result) { + // VSCE does not require $. + if (jsonPath.StartsWith(".$")) + jsonPath = jsonPath.Slice(2); + responseCtx.Content.Seek(0, SeekOrigin.Begin); var jsonDoc = JsonDocument.Parse(responseCtx.Content); var currentElement = jsonDoc.RootElement; while (jsonPath.Length > 0) { var segment = jsonPath; - var separatorIndex = jsonPath.IndexOf('.'); + var separatorIndex = jsonPath.Slice(1).IndexOfAny(".["); if (separatorIndex == -1) { segment = jsonPath; @@ -139,16 +145,17 @@ private static bool ParseBody(ReadOnlySpan jsonPath, VariablePostProcessin } else { - segment = jsonPath[..separatorIndex]; + segment = jsonPath[..(separatorIndex + 1)]; jsonPath = jsonPath.Slice(separatorIndex + 1); } - if (currentElement.ValueKind == JsonValueKind.Object && currentElement.TryGetProperty(segment, out var element)) + if (currentElement.ValueKind == JsonValueKind.Object && currentElement.TryGetProperty(segment[1..], out var element)) { currentElement = element; } else if (currentElement.ValueKind == JsonValueKind.Array - && int.TryParse(segment, out var arrayIndex) + && segment.Length > 2 && segment.StartsWith("[") && segment.EndsWith("]") + && int.TryParse(segment[1..^1].Trim(), out var arrayIndex) && currentElement.GetArrayLength() > arrayIndex) { currentElement = currentElement.EnumerateArray().ElementAt(arrayIndex); @@ -162,6 +169,36 @@ private static bool ParseBody(ReadOnlySpan jsonPath, VariablePostProcessin return true; } + private static bool ParseBody(ReadOnlySpan jsonPath, VariablePostProcessingWriterStrategy responseCtx, ref string result) + { + // VSCE does not require $. + if (jsonPath.StartsWith(".$")) + jsonPath = jsonPath.Slice(1); + if (jsonPath.StartsWith(".")) + jsonPath = $"${jsonPath}"; + + var path = JsonPath.Parse(jsonPath.ToString(), new PathParsingOptions() { AllowMathOperations = false, AllowRelativePathStart = true, AllowJsonConstructs = false, AllowInOperator = false, TolerateExtraWhitespace = true }); + responseCtx.Content.Seek(0, SeekOrigin.Begin); + var instance = JsonNode.Parse(responseCtx.Content); + var matches = path.Evaluate(instance); + if (matches?.Matches == null || matches.Matches.Count == 0) + return false; + + if (matches.Matches.Count != 1) + { + return false; + } + var matchedValue = matches.Matches.First().Value; + if (matchedValue == null) + return false; + + if (matchedValue.GetValueKind() == JsonValueKind.String) + result = matchedValue.ToString(); + else + result = matches.Matches.First().Value?.ToJsonString(new JsonSerializerOptions() { WriteIndented = false }) ?? string.Empty; + return true; + } + private static bool ParseHeaderValue(ReadOnlySpan jsonPath, VariablePostProcessingWriterStrategy responseCtx, ref string result) { var headerName = jsonPath.ToString(); diff --git a/tests/CHttpExecutor.Tests/IntegrationTests.cs b/tests/CHttpExecutor.Tests/IntegrationTests.cs index 02a7fdf..bf06a35 100644 --- a/tests/CHttpExecutor.Tests/IntegrationTests.cs +++ b/tests/CHttpExecutor.Tests/IntegrationTests.cs @@ -1,8 +1,6 @@ -using System.Net.Http.Json; +using System.Text.Json; using CHttp.Tests; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Server.Kestrel.Core; namespace CHttpExecutor.Tests; @@ -46,34 +44,6 @@ public class IntegrationTests GET https://localhost:5020/ HTTP/2 my: {{myheader}}"u8.ToArray(); - private byte[] _postProcessingContentHeaderRequest = @"### -# @no-cert-validation -# @name firstRequest -GET https://localhost:5020/ HTTP/2 -### -@myheader = {{firstRequest.response.headers.content-type}} -### - -# @no-cert-validation -GET https://localhost:5020/ HTTP/2 -my: {{myheader}}"u8.ToArray(); - - private byte[] _postProcessingBodyRequest = @"### -# @no-cert-validation -# @name firstRequest -GET https://localhost:5020/ HTTP/2 -### -@myvalue = {{firstRequest.response.body.data.1.stringValue}} -@mydate = {{firstRequest.response.body.data.1.dateValue}} -@mynumber = {{firstRequest.response.body.data.1.numberValue}} -### - -# @no-cert-validation -GET https://localhost:5020/ HTTP/2 -myvalue: {{myvalue}} -mydate: {{mydate}} -mynumber: {{mynumber}}"u8.ToArray(); - [Fact] public async Task SingleRequestInvokesEndpoint() { @@ -167,6 +137,18 @@ public async Task PostProcessingTrailersVariables() await requestReceived.Task.WaitAsync(TimeSpan.FromSeconds(5)); } + private byte[] _postProcessingContentHeaderRequest = @"### +# @no-cert-validation +# @name firstRequest +GET https://localhost:5020/ HTTP/2 +### +@myheader = {{firstRequest.response.headers.content-type}} +### + +# @no-cert-validation +GET https://localhost:5020/ HTTP/2 +my: {{myheader}}"u8.ToArray(); + [Fact] public async Task PostProcessingContentHeadersVariables() { @@ -191,6 +173,22 @@ public async Task PostProcessingContentHeadersVariables() await requestReceived.Task.WaitAsync(TimeSpan.FromSeconds(5)); } + private byte[] _postProcessingBodyRequest = @"### +# @no-cert-validation +# @name firstRequest +GET https://localhost:5020/ HTTP/2 +### +@myvalue = {{firstRequest.response.body.data[1].stringValue}} +@mydate = {{firstRequest.response.body.$.data[1].dateValue}} +@mynumber = {{firstRequest.response.body.data[1].numberValue}} +### + +# @no-cert-validation +GET https://localhost:5020/ HTTP/2 +myvalue: {{myvalue}} +mydate: {{mydate}} +mynumber: {{mynumber}}"u8.ToArray(); + [Fact] public async Task PostProcessingBodyVariables() { @@ -215,7 +213,79 @@ public async Task PostProcessingBodyVariables() await requestReceived.Task.WaitAsync(TimeSpan.FromSeconds(5)); } + private byte[] _postProcessingBodyArrayRequest = @"### +# @no-cert-validation +# @name firstRequest +GET https://localhost:5020/ HTTP/2 +### +@myvalue = {{firstRequest.response.body.data[1]}} +### + +# @no-cert-validation +GET https://localhost:5020/ HTTP/2 +myvalue: {{myvalue}}"u8.ToArray(); + + [Fact] + public async Task PostProcessingBodyArrayVariables() + { + string testValue = "roundtripped header value"; + var testDate = new DateTime(2024, 06, 02); + TaskCompletionSource requestReceived = new(); + using var host = HttpServer.CreateHostBuilder(async context => + { + if (context.Request.Headers["myvalue"] == """{"stringValue":"roundtripped header value","dateValue":"2024-06-02T00:00:00","numberValue":2}""") + requestReceived.TrySetResult(); + await context.Response.WriteAsJsonAsync(new Root([new(testValue, testDate, 1), new(testValue, testDate, 2)]), new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + }, HttpProtocols.Http2, port: Port); + await host.StartAsync(); + + var stream = new MemoryStream(_postProcessingBodyArrayRequest); + + var reader = new InputReader(new ExecutionPlanBuilder()); + var plan = await reader.ReadStreamAsync(stream); + var executor = new Executor(plan); + await executor.ExecuteAsync(); + + await requestReceived.Task.WaitAsync(TimeSpan.FromSeconds(5)); + } + + private byte[] _postProcessingBodyArrayIntegersRequest = @"### +# @no-cert-validation +# @name firstRequest +GET https://localhost:5020/ HTTP/2 +### +@myvalue = {{firstRequest.response.body.data}} +### + +# @no-cert-validation +GET https://localhost:5020/ HTTP/2 +myvalue: {{myvalue}}"u8.ToArray(); + + [Fact] + public async Task PostProcessingBodyArrayIntegersVariables() + { + TaskCompletionSource requestReceived = new(); + using var host = HttpServer.CreateHostBuilder(async context => + { + if (context.Request.Headers["myvalue"] == """[1,2,3]""") + requestReceived.TrySetResult(); + await context.Response.WriteAsJsonAsync(new RootInts([1, 2, 3])); + }, HttpProtocols.Http2, port: Port); + await host.StartAsync(); + + var stream = new MemoryStream(_postProcessingBodyArrayIntegersRequest); + + var reader = new InputReader(new ExecutionPlanBuilder()); + var plan = await reader.ReadStreamAsync(stream); + var executor = new Executor(plan); + await executor.ExecuteAsync(); + + await requestReceived.Task.WaitAsync(TimeSpan.FromSeconds(5)); + } + private record class Root(IEnumerable Data); private record class Data(string StringValue, DateTime DateValue, int NumberValue); + + private record class RootInts(IEnumerable Data); } \ No newline at end of file diff --git a/tests/CHttpExecutor.Tests/VariablePreprocessorTests.cs b/tests/CHttpExecutor.Tests/VariablePreprocessorTests.cs index ccf554e..dcf5d32 100644 --- a/tests/CHttpExecutor.Tests/VariablePreprocessorTests.cs +++ b/tests/CHttpExecutor.Tests/VariablePreprocessorTests.cs @@ -1,4 +1,6 @@ -namespace CHttpExecutor.Tests; +using System.Buffers; + +namespace CHttpExecutor.Tests; public class VariablePreprocessorTests { @@ -90,4 +92,19 @@ public void InlinedReplacementFromValues() var result = VariablePreprocessor.Evaluate("https://{{host}}/", variables, new Dictionary()); Assert.Equal("https://localhost/", result); } + + [Fact] + public async Task BodyParse() + { + var responseWriter = new VariablePostProcessingWriterStrategy(true); + responseWriter.Buffer.Write("""{"Data":"hello"}"""u8); + await responseWriter.Buffer.CompleteAsync(); + await responseWriter.CompleteAsync(CancellationToken.None); + var responses = new Dictionary() + { + { "first", responseWriter } + }; + var result = VariablePreprocessor.Evaluate("https://{{first.response.body.$.Data}}/", new Dictionary(), responses); + Assert.Equal("https://hello/", result); + } }