From 0a09fd858dde71f580e4de2935010855e78a8a46 Mon Sep 17 00:00:00 2001 From: gali Date: Sat, 21 Mar 2026 23:51:18 +0800 Subject: [PATCH 1/8] feat: Implement multi-test drafting with separate assertions and enhance UI for test management --- README.md | 3 +- src/ApiTestRunner.App/Models/CurlContracts.cs | 11 + .../Services/CurlCommandAnalyzer.cs | 93 ++++++-- .../wwwroot/curl-import.html | 21 +- src/ApiTestRunner.App/wwwroot/curl-import.js | 209 ++++++++++++++++-- src/ApiTestRunner.App/wwwroot/styles.css | 43 ++++ .../CurlCommandAnalyzerTests.cs | 51 +++++ .../net8.0/ApiTestRunner.Core.Tests.dll | Bin 31232 -> 31744 bytes .../net8.0/ApiTestRunner.Core.Tests.pdb | Bin 25692 -> 25692 bytes .../bin/Release/net8.0/ApiTestRunner.Core.dll | Bin 75264 -> 75264 bytes .../bin/Release/net8.0/ApiTestRunner.Core.pdb | Bin 38984 -> 38984 bytes .../ApiTestRunner.Core.Tests.AssemblyInfo.cs | 2 +- ...Runner.Core.Tests.AssemblyInfoInputs.cache | 2 +- ....Core.Tests.csproj.AssemblyReference.cache | Bin 10666 -> 10666 bytes .../net8.0/ApiTestRunner.Core.Tests.dll | Bin 31232 -> 31744 bytes .../net8.0/ApiTestRunner.Core.Tests.pdb | Bin 25692 -> 25692 bytes .../ApiTestRunner.Core.Tests.sourcelink.json | 2 +- .../net8.0/ref/ApiTestRunner.Core.Tests.dll | Bin 14848 -> 14848 bytes .../refint/ApiTestRunner.Core.Tests.dll | Bin 14848 -> 14848 bytes 19 files changed, 397 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 451e822..029e90a 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ The tool will: - parse the request URL, method, headers, query string, and JSON body - optionally parse a pasted JSON response body in the same flow so the assertion builder stays on the same page -- let you add assertion drafts first, then use the main `Analyze and Generate` action to include them in the endpoint YAML preview +- let you create multiple drafted tests, each with its own expected status and assertion set, then use `Analyze and Generate` to include all of them in the endpoint YAML preview - scan the configured YAML suite for an existing environment whose `baseUrl` already covers the pasted request URL - scan for an existing endpoint with the same method and either the same relative path or a matching path template such as `/customers/{customerId}` - generate suggested environment YAML when the base URL is not already present @@ -125,6 +125,7 @@ Current first-version scope: - best support is for common `curl`, `-X`, `-H`, `--data`, `--data-raw`, `--data-binary`, and `--url` forms - the page now uses one main `Analyze and Generate` action instead of separate request-analysis and response-parse steps +- each drafted test in the cURL page keeps its own assertions, so you can generate multiple YAML tests for one endpoint in a single pass - generated YAML is shown as preview text, not written directly to disk - the assertion builder currently targets the most common field assertions: `equals`, `notEquals`, `type`, `containsText`, `startsWith`, `endsWith`, `notEmpty`, `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual`, `minCount`, `maxCount`, and `count` - missing YAML warnings are surfaced in the cURL import UI, but malformed YAML content still needs to be fixed before the dashboard runner can execute the suite diff --git a/src/ApiTestRunner.App/Models/CurlContracts.cs b/src/ApiTestRunner.App/Models/CurlContracts.cs index 7af2909..567b193 100644 --- a/src/ApiTestRunner.App/Models/CurlContracts.cs +++ b/src/ApiTestRunner.App/Models/CurlContracts.cs @@ -6,6 +6,8 @@ public sealed class CurlAnalyzeRequest public string? ResponseBody { get; init; } + public IReadOnlyList Tests { get; init; } = []; + public IReadOnlyList Assertions { get; init; } = []; } @@ -52,6 +54,15 @@ public sealed class CurlAssertionDraft public object? Value { get; init; } } +public sealed class CurlTestDraft +{ + public string Name { get; init; } = string.Empty; + + public int ExpectedStatus { get; init; } = 200; + + public IReadOnlyList Assertions { get; init; } = []; +} + public sealed class CurlEnvironmentAnalysis { public bool Exists { get; init; } diff --git a/src/ApiTestRunner.App/Services/CurlCommandAnalyzer.cs b/src/ApiTestRunner.App/Services/CurlCommandAnalyzer.cs index 9deb157..9f23727 100644 --- a/src/ApiTestRunner.App/Services/CurlCommandAnalyzer.cs +++ b/src/ApiTestRunner.App/Services/CurlCommandAnalyzer.cs @@ -97,6 +97,8 @@ public async Task AnalyzeAsync(CurlAnalyzeRequest request, .ToArray() : [suggestedEnvironmentName]; + var testDrafts = NormalizeTestDrafts(request, parsedRequest.Method, effectivePath); + return new CurlAnalyzeResponse { Request = requestSummary, @@ -126,7 +128,7 @@ public async Task AnalyzeAsync(CurlAnalyzeRequest request, variableSuggestions.TransformedRequest, effectivePath, targetEnvironmentNames, - request.Assertions) + testDrafts) }, Variables = new CurlVariableAnalysis { @@ -732,14 +734,14 @@ private string GenerateEndpointYaml( CurlRequestSummary request, string endpointPath, IReadOnlyList targetEnvironmentNames, - IReadOnlyList assertions) + IReadOnlyList tests) { var endpointDocument = new Dictionary { ["targetEnvironments"] = targetEnvironmentNames, ["endpoints"] = new[] { - BuildEndpointDocument(request, endpointPath, assertions) + BuildEndpointDocument(request, endpointPath, tests) } }; @@ -749,7 +751,7 @@ private string GenerateEndpointYaml( private Dictionary BuildEndpointDocument( CurlRequestSummary request, string endpointPath, - IReadOnlyList assertions) + IReadOnlyList tests) { var endpoint = new Dictionary { @@ -776,20 +778,52 @@ private string GenerateEndpointYaml( endpoint["body"] = request.Body; } - var testDefinition = new Dictionary + endpoint["tests"] = BuildTestDocuments(request.Method, endpointPath, tests); + return endpoint; + } + + private static List> BuildTestDocuments( + string method, + string endpointPath, + IReadOnlyList tests) + { + var normalizedTests = tests + .Where(test => !string.IsNullOrWhiteSpace(test.Name)) + .ToArray(); + + if (normalizedTests.Length == 0) { - ["name"] = $"{SuggestEndpointName(request.Method, endpointPath)} should return success", - ["expectedStatus"] = 200 - }; + normalizedTests = + [ + new CurlTestDraft + { + Name = $"{SuggestEndpointName(method, endpointPath)} should return success", + ExpectedStatus = 200, + Assertions = [] + } + ]; + } - var assertionDocuments = BuildAssertionDocuments(assertions); - if (assertionDocuments.Count > 0) + var documents = new List>(normalizedTests.Length); + + foreach (var test in normalizedTests) { - testDefinition["assertions"] = assertionDocuments; + var testDefinition = new Dictionary + { + ["name"] = test.Name, + ["expectedStatus"] = test.ExpectedStatus + }; + + var assertionDocuments = BuildAssertionDocuments(test.Assertions); + if (assertionDocuments.Count > 0) + { + testDefinition["assertions"] = assertionDocuments; + } + + documents.Add(testDefinition); } - endpoint["tests"] = new[] { testDefinition }; - return endpoint; + return documents; } private static List> BuildAssertionDocuments(IReadOnlyList assertions) @@ -817,6 +851,39 @@ private string GenerateEndpointYaml( return documents; } + private static IReadOnlyList NormalizeTestDrafts( + CurlAnalyzeRequest request, + string method, + string endpointPath) + { + if (request.Tests.Count > 0) + { + return request.Tests + .Where(test => !string.IsNullOrWhiteSpace(test.Name)) + .Select(test => new CurlTestDraft + { + Name = test.Name, + ExpectedStatus = test.ExpectedStatus <= 0 ? 200 : test.ExpectedStatus, + Assertions = test.Assertions + .Where(assertion => !string.IsNullOrWhiteSpace(assertion.Field) && !string.IsNullOrWhiteSpace(assertion.Rule)) + .ToArray() + }) + .ToArray(); + } + + return + [ + new CurlTestDraft + { + Name = $"{SuggestEndpointName(method, endpointPath)} should return success", + ExpectedStatus = 200, + Assertions = request.Assertions + .Where(assertion => !string.IsNullOrWhiteSpace(assertion.Field) && !string.IsNullOrWhiteSpace(assertion.Rule)) + .ToArray() + } + ]; + } + private static object? ConvertAssertionValue(object? value) { return value switch diff --git a/src/ApiTestRunner.App/wwwroot/curl-import.html b/src/ApiTestRunner.App/wwwroot/curl-import.html index b41374e..a25dfaf 100644 --- a/src/ApiTestRunner.App/wwwroot/curl-import.html +++ b/src/ApiTestRunner.App/wwwroot/curl-import.html @@ -115,12 +115,29 @@

Paste r
-

Assertion builder

-

Choose a response field, define the rule, and then generate the endpoint YAML preview with those assertions included.

+

Test builder

+

Create one or more tests, each with its own expected status and assertions, then generate endpoint YAML with all drafted tests included.

+
+ + +
+ +
+ +
+ +
+
-
+