mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-02-23 09:15:06 +01:00
Merge branch 'master' into version-2.x
This commit is contained in:
@@ -22,7 +22,7 @@ public class MatcherModel
|
||||
public object? Pattern { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the patterns. Can be array of strings (default) or an array of objects.
|
||||
/// Gets or sets the patterns. Can be an array of strings (default) or an array of objects.
|
||||
/// </summary>
|
||||
public object[]? Patterns { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using WireMock.Validators;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace WireMock.Admin.Mappings;
|
||||
@@ -94,9 +95,14 @@ public partial class RequestModelBuilder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Path.
|
||||
/// Set the Path. Must start with a forward slash (/).
|
||||
/// </summary>
|
||||
public RequestModelBuilder WithPath(string value) => WithPath(() => value);
|
||||
public RequestModelBuilder WithPath(string value)
|
||||
{
|
||||
PathValidator.ValidateAndThrow(value);
|
||||
|
||||
return WithPath(() => value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Path.
|
||||
|
||||
19
src/WireMock.Net.Abstractions/Validators/PathValidator.cs
Normal file
19
src/WireMock.Net.Abstractions/Validators/PathValidator.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
|
||||
namespace WireMock.Validators;
|
||||
|
||||
public static class PathValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// A valid path must start with a '/' and cannot be null, empty or whitespace.
|
||||
/// </summary>
|
||||
public static void ValidateAndThrow(string? path, string? paramName = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || path?.StartsWith("/") == false)
|
||||
{
|
||||
throw new ArgumentException("Path must start with a '/' and cannot be null, empty or whitespace.", paramName ?? nameof(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/WireMock.Net.Aspire/WireMockHealthCheck.cs
Normal file
44
src/WireMock.Net.Aspire/WireMockHealthCheck.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Aspire.Hosting.ApplicationModel;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using WireMock.Client;
|
||||
|
||||
namespace WireMock.Net.Aspire;
|
||||
|
||||
/// <summary>
|
||||
/// WireMockHealthCheck
|
||||
/// </summary>
|
||||
public class WireMockHealthCheck(WireMockServerResource resource) : IHealthCheck
|
||||
{
|
||||
private const string HealthStatusHealthy = "Healthy";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!await IsHealthyAsync(resource.AdminApi.Value, cancellationToken))
|
||||
{
|
||||
return HealthCheckResult.Unhealthy("WireMock.Net is not healthy");
|
||||
}
|
||||
|
||||
if (resource.ApiMappingState == WireMockMappingState.NotSubmitted)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy("WireMock.Net has not received mappings");
|
||||
}
|
||||
|
||||
return HealthCheckResult.Healthy();
|
||||
}
|
||||
|
||||
private static async Task<bool> IsHealthyAsync(IWireMockAdminApi adminApi, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = await adminApi.GetHealthAsync(cancellationToken);
|
||||
return string.Equals(status, HealthStatusHealthy, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/WireMock.Net.Aspire/WireMockMappingState.cs
Normal file
10
src/WireMock.Net.Aspire/WireMockMappingState.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
namespace WireMock.Net.Aspire;
|
||||
|
||||
internal enum WireMockMappingState
|
||||
{
|
||||
NoMappings,
|
||||
NotSubmitted,
|
||||
Submitted,
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
using Aspire.Hosting.ApplicationModel;
|
||||
using Aspire.Hosting.Lifecycle;
|
||||
using Aspire.Hosting.WireMock;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Stef.Validation;
|
||||
using WireMock.Client.Builders;
|
||||
@@ -53,11 +54,21 @@ public static class WireMockServerBuilderExtensions
|
||||
Guard.NotNull(arguments);
|
||||
|
||||
var wireMockContainerResource = new WireMockServerResource(name, arguments);
|
||||
|
||||
var healthCheckKey = $"{name}_check";
|
||||
var healthCheckRegistration = new HealthCheckRegistration(
|
||||
healthCheckKey,
|
||||
_ => new WireMockHealthCheck(wireMockContainerResource),
|
||||
failureStatus: null,
|
||||
tags: null);
|
||||
builder.Services.AddHealthChecks().Add(healthCheckRegistration);
|
||||
|
||||
var resourceBuilder = builder
|
||||
.AddResource(wireMockContainerResource)
|
||||
.WithImage(DefaultLinuxImage)
|
||||
.WithEnvironment(ctx => ctx.EnvironmentVariables.Add("DOTNET_USE_POLLING_FILE_WATCHER", "1")) // https://khalidabuhakmeh.com/aspnet-docker-gotchas-and-workarounds#configuration-reloads-and-filesystemwatcher
|
||||
.WithHttpEndpoint(port: arguments.HttpPort, targetPort: WireMockServerArguments.HttpContainerPort)
|
||||
.WithHealthCheck(healthCheckKey)
|
||||
.WithWireMockInspectorCommand();
|
||||
|
||||
if (!string.IsNullOrEmpty(arguments.MappingsPath))
|
||||
@@ -172,6 +183,7 @@ public static class WireMockServerBuilderExtensions
|
||||
|
||||
wiremock.ApplicationBuilder.Services.TryAddLifecycleHook<WireMockServerLifecycleHook>();
|
||||
wiremock.Resource.Arguments.ApiMappingBuilder = configure;
|
||||
wiremock.Resource.ApiMappingState = WireMockMappingState.NotSubmitted;
|
||||
|
||||
return wiremock;
|
||||
}
|
||||
|
||||
@@ -10,32 +10,47 @@ internal class WireMockServerLifecycleHook(ILoggerFactory loggerFactory) : IDist
|
||||
{
|
||||
private readonly CancellationTokenSource _shutdownCts = new();
|
||||
|
||||
public async Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
|
||||
private CancellationTokenSource? _linkedCts;
|
||||
private Task? _mappingTask;
|
||||
|
||||
public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(_shutdownCts.Token, cancellationToken);
|
||||
_linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_shutdownCts.Token, cancellationToken);
|
||||
|
||||
var wireMockServerResources = appModel.Resources
|
||||
.OfType<WireMockServerResource>()
|
||||
.ToArray();
|
||||
|
||||
foreach (var wireMockServerResource in wireMockServerResources)
|
||||
_mappingTask = Task.Run(async () =>
|
||||
{
|
||||
wireMockServerResource.SetLogger(loggerFactory.CreateLogger<WireMockServerResource>());
|
||||
var wireMockServerResources = appModel.Resources
|
||||
.OfType<WireMockServerResource>()
|
||||
.ToArray();
|
||||
|
||||
var endpoint = wireMockServerResource.GetEndpoint();
|
||||
if (endpoint.IsAllocated)
|
||||
foreach (var wireMockServerResource in wireMockServerResources)
|
||||
{
|
||||
await wireMockServerResource.WaitForHealthAsync(cts.Token);
|
||||
wireMockServerResource.SetLogger(loggerFactory.CreateLogger<WireMockServerResource>());
|
||||
|
||||
await wireMockServerResource.CallApiMappingBuilderActionAsync(cts.Token);
|
||||
var endpoint = wireMockServerResource.GetEndpoint();
|
||||
System.Diagnostics.Debug.Assert(endpoint.IsAllocated);
|
||||
|
||||
wireMockServerResource.StartWatchingStaticMappings(cts.Token);
|
||||
await wireMockServerResource.WaitForHealthAsync(_linkedCts.Token);
|
||||
|
||||
await wireMockServerResource.CallApiMappingBuilderActionAsync(_linkedCts.Token);
|
||||
|
||||
wireMockServerResource.StartWatchingStaticMappings(_linkedCts.Token);
|
||||
}
|
||||
}
|
||||
}, _linkedCts.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _shutdownCts.CancelAsync();
|
||||
|
||||
_linkedCts?.Dispose();
|
||||
_shutdownCts.Dispose();
|
||||
|
||||
if (_mappingTask is not null)
|
||||
{
|
||||
await _mappingTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using RestEase;
|
||||
using Stef.Validation;
|
||||
using WireMock.Client;
|
||||
using WireMock.Client.Extensions;
|
||||
using WireMock.Net.Aspire;
|
||||
using WireMock.Util;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
@@ -19,6 +20,7 @@ public class WireMockServerResource : ContainerResource, IResourceWithServiceDis
|
||||
|
||||
internal WireMockServerArguments Arguments { get; }
|
||||
internal Lazy<IWireMockAdminApi> AdminApi => new(CreateWireMockAdminApi);
|
||||
internal WireMockMappingState ApiMappingState { get; set; } = WireMockMappingState.NoMappings;
|
||||
|
||||
private ILogger? _logger;
|
||||
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
|
||||
@@ -64,6 +66,8 @@ public class WireMockServerResource : ContainerResource, IResourceWithServiceDis
|
||||
|
||||
var mappingBuilder = AdminApi.Value.GetMappingBuilder();
|
||||
await Arguments.ApiMappingBuilder.Invoke(mappingBuilder, cancellationToken);
|
||||
|
||||
ApiMappingState = WireMockMappingState.Submitted;
|
||||
}
|
||||
|
||||
internal void StartWatchingStaticMappings(CancellationToken cancellationToken)
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using Stef.Validation;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.Matchers.Request;
|
||||
using WireMock.Validators;
|
||||
|
||||
namespace WireMock.RequestBuilders;
|
||||
|
||||
@@ -34,6 +35,10 @@ public partial class Request
|
||||
public IRequestBuilder WithPath(MatchOperator matchOperator, params string[] paths)
|
||||
{
|
||||
Guard.NotNullOrEmpty(paths);
|
||||
foreach (var path in paths)
|
||||
{
|
||||
PathValidator.ValidateAndThrow(path, nameof(paths));
|
||||
}
|
||||
|
||||
_requestMatchers.Add(new RequestMessagePathMatcher(MatchBehaviour.AcceptOnMatch, matchOperator, paths));
|
||||
return this;
|
||||
|
||||
@@ -36,7 +36,7 @@ public static class WireMockServerSettingsParser
|
||||
|
||||
if (parser.GetBoolSwitchValue("help"))
|
||||
{
|
||||
(logger ?? new WireMockConsoleLogger()).Info("See https://github.com/wiremock/WireMock.Net/wiki/WireMock-commandline-parameters for details on all commandline options.");
|
||||
(logger ?? new WireMockConsoleLogger()).Info("See https://wiremock.org/dotnet/wiremock-commandline-parameters/ for details on all commandline options.");
|
||||
settings = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -18,18 +18,12 @@ using SystemTextJsonSerializer = System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace WireMock.Net.OpenApiParser.Mappers;
|
||||
|
||||
internal class OpenApiPathsMapper
|
||||
internal class OpenApiPathsMapper(WireMockOpenApiParserSettings settings)
|
||||
{
|
||||
private const string HeaderContentType = "Content-Type";
|
||||
|
||||
private readonly WireMockOpenApiParserSettings _settings;
|
||||
private readonly ExampleValueGenerator _exampleValueGenerator;
|
||||
|
||||
public OpenApiPathsMapper(WireMockOpenApiParserSettings settings)
|
||||
{
|
||||
_settings = Guard.NotNull(settings);
|
||||
_exampleValueGenerator = new ExampleValueGenerator(settings);
|
||||
}
|
||||
private readonly WireMockOpenApiParserSettings _settings = Guard.NotNull(settings);
|
||||
private readonly ExampleValueGenerator _exampleValueGenerator = new(settings);
|
||||
|
||||
public IReadOnlyList<MappingModel> ToMappingModels(OpenApiPaths? paths, IList<OpenApiServer> servers)
|
||||
{
|
||||
@@ -40,7 +34,7 @@ internal class OpenApiPathsMapper
|
||||
.ToArray() ?? [];
|
||||
}
|
||||
|
||||
private IReadOnlyList<MappingModel> MapPath(string path, IOpenApiPathItem pathItem, IList<OpenApiServer> servers)
|
||||
private MappingModel[] MapPath(string path, IOpenApiPathItem pathItem, IList<OpenApiServer> servers)
|
||||
{
|
||||
return pathItem.Operations?.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers)).ToArray() ?? [];
|
||||
}
|
||||
@@ -49,35 +43,7 @@ internal class OpenApiPathsMapper
|
||||
{
|
||||
var queryParameters = operation.Parameters?.Where(p => p.In == ParameterLocation.Query) ?? [];
|
||||
var pathParameters = operation.Parameters?.Where(p => p.In == ParameterLocation.Path) ?? [];
|
||||
var headers = operation.Parameters?.Where(p => p.In == ParameterLocation.Header) ?? [];
|
||||
|
||||
var response = operation.Responses?.FirstOrDefault() ?? new KeyValuePair<string, IOpenApiResponse>();
|
||||
|
||||
TryGetContent(response.Value?.Content, out OpenApiMediaType? responseContent, out var responseContentType);
|
||||
var responseSchema = response.Value?.Content?.FirstOrDefault().Value?.Schema;
|
||||
var responseExample = responseContent?.Example;
|
||||
var responseSchemaExample = responseContent?.Schema?.Example;
|
||||
|
||||
var responseBody = responseExample ?? responseSchemaExample ?? MapSchemaToObject(responseSchema);
|
||||
|
||||
var requestBodyModel = new BodyModel();
|
||||
if (operation.RequestBody != null && operation.RequestBody.Content != null && operation.RequestBody.Required)
|
||||
{
|
||||
var request = operation.RequestBody.Content;
|
||||
TryGetContent(request, out var requestContent, out _);
|
||||
|
||||
var requestBodySchema = operation.RequestBody.Content.First().Value?.Schema;
|
||||
var requestBodyExample = requestContent!.Example;
|
||||
var requestBodySchemaExample = requestContent.Schema?.Example;
|
||||
|
||||
var requestBodyMapped = requestBodyExample ?? requestBodySchemaExample ?? MapSchemaToObject(requestBodySchema);
|
||||
requestBodyModel = MapRequestBody(requestBodyMapped);
|
||||
}
|
||||
|
||||
if (!int.TryParse(response.Key, out var httpStatusCode))
|
||||
{
|
||||
httpStatusCode = 200;
|
||||
}
|
||||
var requestHeaders = operation.Parameters?.Where(p => p.In == ParameterLocation.Header) ?? [];
|
||||
|
||||
return new MappingModel
|
||||
{
|
||||
@@ -87,15 +53,94 @@ internal class OpenApiPathsMapper
|
||||
Methods = [httpMethod],
|
||||
Path = PathUtils.Combine(MapBasePath(servers), MapPathWithParameters(path, pathParameters)),
|
||||
Params = MapQueryParameters(queryParameters),
|
||||
Headers = MapRequestHeaders(headers),
|
||||
Body = requestBodyModel
|
||||
Headers = MapRequestHeaders(requestHeaders),
|
||||
Body = GetRequestBodyModel(operation.RequestBody)
|
||||
},
|
||||
Response = new ResponseModel
|
||||
{
|
||||
StatusCode = httpStatusCode,
|
||||
Headers = MapHeaders(responseContentType, response.Value?.Headers),
|
||||
BodyAsJson = responseBody != null ? JsonConvert.DeserializeObject(SystemTextJsonSerializer.Serialize(responseBody)) : null
|
||||
}
|
||||
Response = GetResponseModel(operation.Responses?.FirstOrDefault())
|
||||
};
|
||||
}
|
||||
|
||||
private BodyModel GetRequestBodyModel(IOpenApiRequestBody? openApiRequestBody)
|
||||
{
|
||||
if (openApiRequestBody is not { Content: not null, Required: true })
|
||||
{
|
||||
return new BodyModel();
|
||||
}
|
||||
|
||||
var content = openApiRequestBody.Content;
|
||||
|
||||
TryGetContent(content, out var requestContent, out _);
|
||||
|
||||
var requestExample = requestContent?.Example;
|
||||
var requestExamples = requestContent?.Examples;
|
||||
var requestSchemaExample = requestContent?.Schema?.Example;
|
||||
var requestSchemaExamples = requestContent?.Schema?.Examples;
|
||||
|
||||
JsonNode? request;
|
||||
if (requestExample != null)
|
||||
{
|
||||
request = requestExample;
|
||||
}
|
||||
else if (requestSchemaExample != null)
|
||||
{
|
||||
request = requestSchemaExample;
|
||||
}
|
||||
else if (requestExamples != null)
|
||||
{
|
||||
request = requestExamples.FirstOrDefault().Value.Value;
|
||||
}
|
||||
else if (requestSchemaExamples != null)
|
||||
{
|
||||
request = requestSchemaExamples.FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
var requestSchema = content?.FirstOrDefault().Value.Schema;
|
||||
request = MapSchemaToObject(requestSchema);
|
||||
}
|
||||
|
||||
return MapRequestBody(request) ?? new BodyModel();
|
||||
}
|
||||
|
||||
private ResponseModel GetResponseModel(KeyValuePair<string, IOpenApiResponse>? openApiResponse)
|
||||
{
|
||||
var content = openApiResponse?.Value.Content;
|
||||
|
||||
TryGetContent(content, out var responseContent, out var contentType);
|
||||
|
||||
var responseExample = responseContent?.Example;
|
||||
var responseExamples = responseContent?.Examples;
|
||||
var responseSchemaExample = responseContent?.Schema?.Example;
|
||||
var responseSchemaExamples = responseContent?.Schema?.Examples;
|
||||
|
||||
JsonNode? response;
|
||||
if (responseExample != null)
|
||||
{
|
||||
response = responseExample;
|
||||
}
|
||||
else if (responseSchemaExample != null)
|
||||
{
|
||||
response = responseSchemaExample;
|
||||
}
|
||||
else if (responseExamples != null)
|
||||
{
|
||||
response = responseExamples.FirstOrDefault().Value.Value;
|
||||
}
|
||||
else if (responseSchemaExamples != null)
|
||||
{
|
||||
response = responseSchemaExamples.FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
var responseSchema = content?.FirstOrDefault().Value?.Schema;
|
||||
response = MapSchemaToObject(responseSchema);
|
||||
}
|
||||
|
||||
return new ResponseModel
|
||||
{
|
||||
StatusCode = int.TryParse(openApiResponse?.Key, out var httpStatusCode) ? httpStatusCode : 200,
|
||||
Headers = MapHeaders(contentType, openApiResponse?.Value.Headers),
|
||||
BodyAsJson = response != null ? JsonConvert.DeserializeObject(SystemTextJsonSerializer.Serialize(response)) : null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user