Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions ApiTestRunner.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiTestRunner.Core.Tests", "tests\ApiTestRunner.Core.Tests\ApiTestRunner.Core.Tests.csproj", "{EF423B95-619D-4789-81FF-36F4E58FE369}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiTestRunner.App.Tests", "tests\ApiTestRunner.App.Tests\ApiTestRunner.App.Tests.csproj", "{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -59,6 +61,18 @@ Global
{EF423B95-619D-4789-81FF-36F4E58FE369}.Release|x64.Build.0 = Release|Any CPU
{EF423B95-619D-4789-81FF-36F4E58FE369}.Release|x86.ActiveCfg = Release|Any CPU
{EF423B95-619D-4789-81FF-36F4E58FE369}.Release|x86.Build.0 = Release|Any CPU
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}.Debug|x64.ActiveCfg = Debug|Any CPU
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}.Debug|x64.Build.0 = Debug|Any CPU
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}.Debug|x86.ActiveCfg = Debug|Any CPU
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}.Debug|x86.Build.0 = Debug|Any CPU
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}.Release|Any CPU.Build.0 = Release|Any CPU
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}.Release|x64.ActiveCfg = Release|Any CPU
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}.Release|x64.Build.0 = Release|Any CPU
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}.Release|x86.ActiveCfg = Release|Any CPU
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -67,5 +81,6 @@ Global
{90A14FD8-067D-46BF-BF61-045708DF1A74} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{94252802-A533-483A-A08E-1F36CEABA7DB} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{EF423B95-619D-4789-81FF-36F4E58FE369} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{BB8BCC07-46B2-43AB-9812-41AD2F37E31C} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
- Path params, query params, headers, and JSON request bodies
- Dot-notation assertions with array index support
- Validation for strings, objects, and arrays
- Toggleable test selection in the dashboard, including select-all and individual test control
- Pass/fail reporting with response previews in the dashboard
- cURL analysis page that scans configured YAML definitions and generates suggested environment and endpoint YAML when missing
- Configurable dashboard host, port, browser auto-launch, suite files, and concurrency through `appsettings.json`

## Requirements summary
Expand All @@ -40,6 +42,38 @@ dotnet run --project src/ApiTestRunner.App -c Release

The app starts the dashboard at `http://localhost:5005` by default, auto-launches the browser if enabled, and executes the split sample suite after the web server is ready.

## Dashboard workflow

The main dashboard now exposes a test-selection panel before execution:

- `Select All` enables the full suite.
- `Clear All` disables every test.
- Environment, endpoint, and individual test checkboxes let you run only the subset you care about.
- Each page load starts with all tests selected by default.

Important note:

- The runner still executes one HTTP request per endpoint. Selecting one test under an endpoint will run that endpoint once and evaluate only the selected tests against that shared response.

## cURL import workflow

Open `http://localhost:5005/curl-import.html` to paste a cURL command and inspect it.

The tool will:

- parse the request URL, method, headers, query string, and JSON body
- 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
- generate suggested endpoint YAML when the endpoint is not already present
- accept a pasted JSON response body so you can pick response fields and build assertion YAML into the generated endpoint test

Current first-version scope:

- best support is for common `curl`, `-X`, `-H`, `--data`, `--data-raw`, `--data-binary`, and `--url` forms
- 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`, `minCount`, `maxCount`, and `count`

## CI/CD

The repository now includes a GitHub Actions based CI/CD setup under [`.github/workflows`](D:/Projects/Research/EndpointTestRunner/.github/workflows):
Expand Down
75 changes: 75 additions & 0 deletions src/ApiTestRunner.App/Models/CurlContracts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
namespace ApiTestRunner.App.Models;

public sealed class CurlAnalyzeRequest
{
public string Command { get; init; } = string.Empty;

public string? ResponseBody { get; init; }

public IReadOnlyList<CurlAssertionDraft> Assertions { get; init; } = [];
}

public sealed class CurlAnalyzeResponse
{
public CurlRequestSummary? Request { get; init; }

public CurlEnvironmentAnalysis Environment { get; init; } = new();

public CurlEndpointAnalysis Endpoint { get; init; } = new();
}

public sealed class CurlRequestSummary
{
public string Method { get; init; } = string.Empty;

public string Url { get; init; } = string.Empty;

public string BaseUrl { get; init; } = string.Empty;

public string Path { get; init; } = string.Empty;

public IReadOnlyDictionary<string, string> Query { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

public IReadOnlyDictionary<string, string> Headers { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

public object? Body { get; init; }

public string? RawBody { get; init; }

public string? RelativePath { get; init; }
}

public sealed class CurlAssertionDraft
{
public string Field { get; init; } = string.Empty;

public string Rule { get; init; } = string.Empty;

public object? Value { get; init; }
}

public sealed class CurlEnvironmentAnalysis
{
public bool Exists { get; init; }

public string SuggestedName { get; init; } = string.Empty;

public IReadOnlyList<string> MatchedEnvironmentNames { get; init; } = [];

public string? SuggestedFilePath { get; init; }

public string? SuggestedYaml { get; init; }
}

public sealed class CurlEndpointAnalysis
{
public bool Exists { get; init; }

public string SuggestedName { get; init; } = string.Empty;

public IReadOnlyList<string> MatchedEnvironmentNames { get; init; } = [];

public string? SuggestedFilePath { get; init; }

public string? SuggestedYaml { get; init; }
}
58 changes: 58 additions & 0 deletions src/ApiTestRunner.App/Models/DashboardContracts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using ApiTestRunner.Core.Models;

namespace ApiTestRunner.App.Models;

public sealed class TestSelectionRequest
{
public bool RunAll { get; init; } = true;

public IReadOnlyList<string> SelectedTestIds { get; init; } = [];
}

public sealed class DashboardSuiteManifest
{
public IReadOnlyList<DashboardEnvironmentManifest> Environments { get; init; } = [];

public int TotalEndpoints => Environments.Sum(environment => environment.Endpoints.Count);

public int TotalTests => Environments.Sum(environment => environment.TotalTests);
}

public sealed class DashboardEnvironmentManifest
{
public string Id { get; init; } = string.Empty;

public string Name { get; init; } = string.Empty;

public string BaseUrl { get; init; } = string.Empty;

public IReadOnlyList<DashboardEndpointManifest> Endpoints { get; init; } = [];

public int TotalTests => Endpoints.Sum(endpoint => endpoint.Tests.Count);
}

public sealed class DashboardEndpointManifest
{
public string Id { get; init; } = string.Empty;

public string Name { get; init; } = string.Empty;

public string Method { get; init; } = string.Empty;

public string Path { get; init; } = string.Empty;

public IReadOnlyList<DashboardTestManifest> Tests { get; init; } = [];
}

public sealed class DashboardTestManifest
{
public string Id { get; init; } = string.Empty;

public string Name { get; init; } = string.Empty;

public int ExpectedStatus { get; init; }
}

public sealed record LoadedTestSuite(
ApiTestSuiteDefinition Suite,
IReadOnlyList<string> FilePaths);
23 changes: 21 additions & 2 deletions src/ApiTestRunner.App/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Nodes;
using ApiTestRunner.App.Models;
using ApiTestRunner.App.Options;
using ApiTestRunner.App.Services;
using ApiTestRunner.Core.Extensions;
Expand All @@ -17,6 +18,8 @@
builder.Services.Configure<ExecutionOptions>(builder.Configuration.GetSection(ExecutionOptions.SectionName));

builder.Services.AddApiTestRunnerCore();
builder.Services.AddSingleton<IConfiguredTestSuiteProvider, ConfiguredTestSuiteProvider>();
builder.Services.AddSingleton<ICurlCommandAnalyzer, CurlCommandAnalyzer>();
builder.Services.AddSingleton<TestRunCoordinator>();
builder.Services.AddHostedService<StartupAutomationHostedService>();

Expand All @@ -36,9 +39,25 @@
return Results.Ok(coordinator.GetState());
});

app.MapPost("/api/dashboard/run", async (TestRunCoordinator coordinator, CancellationToken cancellationToken) =>
app.MapGet("/api/dashboard/manifest", async (TestRunCoordinator coordinator, CancellationToken cancellationToken) =>
{
var result = await coordinator.ExecuteAsync(cancellationToken);
var manifest = await coordinator.GetManifestAsync(cancellationToken);
return Results.Ok(manifest);
});

app.MapPost("/api/dashboard/run", async (HttpRequest request, TestRunCoordinator coordinator, CancellationToken cancellationToken) =>
{
var selection = request.ContentLength > 0
? await request.ReadFromJsonAsync<TestSelectionRequest>(cancellationToken)
: null;

var result = await coordinator.ExecuteAsync(selection, cancellationToken);
return Results.Ok(result);
});

app.MapPost("/api/tools/curl/analyze", async (CurlAnalyzeRequest request, ICurlCommandAnalyzer analyzer, CancellationToken cancellationToken) =>
{
var result = await analyzer.AnalyzeAsync(request, cancellationToken);
return Results.Ok(result);
});

Expand Down
Loading
Loading