diff --git a/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj b/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj
index f05544701..cf43ca011 100644
--- a/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj
+++ b/examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj
@@ -21,4 +21,13 @@
-
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
\ No newline at end of file
diff --git a/examples-Aspire/AspireApp1.AppHost/Program.cs b/examples-Aspire/AspireApp1.AppHost/Program.cs
index 451cf92ef..3ed786573 100644
--- a/examples-Aspire/AspireApp1.AppHost/Program.cs
+++ b/examples-Aspire/AspireApp1.AppHost/Program.cs
@@ -4,12 +4,25 @@
// IResourceBuilder apiService = builder.AddProject("apiservice");
-var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "WireMockMappings");
+var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings");
-IResourceBuilder apiService = builder
- .AddWireMock("apiservice", WireMockServerArguments.DefaultPort)
+//IResourceBuilder apiService1 = builder
+// //.AddWireMock("apiservice", WireMockServerArguments.DefaultPort)
+// .AddWireMock("apiservice1", "http://*:8081", "grpc://*:9091")
+// .AsHttp2Service()
+// .WithMappingsPath(mappingsPath)
+// .WithReadStaticMappings()
+// .WithWatchStaticMappings()
+// .WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync);
+
+IResourceBuilder apiService2 = builder
+ .AddWireMock("apiservice", async args =>
+ {
+ args.WithAdditionalUrls("http://*:8081", "grpc://*:9093");
+ args.WithProtoDefinition("my-greeter", await File.ReadAllTextAsync(Path.Combine(mappingsPath, "greet.proto")));
+ })
+ .AsHttp2Service()
.WithMappingsPath(mappingsPath)
- .WithReadStaticMappings()
.WithWatchStaticMappings()
.WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync);
@@ -45,7 +58,7 @@
builder.AddProject("webfrontend")
.WithExternalHttpEndpoints()
- .WithReference(apiService)
- .WaitFor(apiService);
+ .WithReference(apiService2)
+ .WaitFor(apiService2);
-builder.Build().Run();
\ No newline at end of file
+await builder.Build().RunAsync();
\ No newline at end of file
diff --git a/examples-Aspire/AspireApp1.AppHost/WireMockMappings/873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json b/examples-Aspire/AspireApp1.AppHost/__admin/mappings/873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json
similarity index 100%
rename from examples-Aspire/AspireApp1.AppHost/WireMockMappings/873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json
rename to examples-Aspire/AspireApp1.AppHost/__admin/mappings/873d495f-940e-4b86-a1f4-4f0fc7be8b8b.json
diff --git a/examples-Aspire/AspireApp1.AppHost/__admin/mappings/greet.proto b/examples-Aspire/AspireApp1.AppHost/__admin/mappings/greet.proto
new file mode 100644
index 000000000..f4e1ead02
--- /dev/null
+++ b/examples-Aspire/AspireApp1.AppHost/__admin/mappings/greet.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+package greet;
+
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply);
+}
+
+message HelloRequest {
+ string name = 1;
+}
+
+message HelloReply {
+ string message = 1;
+ enum PhoneType {
+ none = 0;
+ mobile = 1;
+ home = 2;
+ }
+ PhoneType phoneType = 2;
+}
\ No newline at end of file
diff --git a/examples-Aspire/AspireApp1.AppHost/__admin/mappings/protobuf-mapping-4.json b/examples-Aspire/AspireApp1.AppHost/__admin/mappings/protobuf-mapping-4.json
new file mode 100644
index 000000000..930dc941f
--- /dev/null
+++ b/examples-Aspire/AspireApp1.AppHost/__admin/mappings/protobuf-mapping-4.json
@@ -0,0 +1,40 @@
+{
+ "Guid": "351f0240-bba0-4bcb-93c6-1feba0fe0004",
+ "Title": "ProtoBuf Mapping 4",
+ "Request": {
+ "Path": {
+ "Matchers": [
+ {
+ "Name": "WildcardMatcher",
+ "Pattern": "/greet.Greeter/SayHello",
+ "IgnoreCase": false
+ }
+ ]
+ },
+ "Methods": [
+ "POST"
+ ],
+ "Body": {
+ "Matcher": {
+ "Name": "ProtoBufMatcher",
+ "ProtoBufMessageType": "greet.HelloRequest"
+ }
+ }
+ },
+ "Response": {
+ "BodyAsJson": {
+ "message": "hello {{request.BodyAsJson.name}} {{request.method}}"
+ },
+ "UseTransformer": true,
+ "TransformerType": "Handlebars",
+ "TransformerReplaceNodeOptions": "EvaluateAndTryToConvert",
+ "Headers": {
+ "Content-Type": "application/grpc"
+ },
+ "TrailingHeaders": {
+ "grpc-status": "0"
+ },
+ "ProtoBufMessageType": "greet.HelloReply"
+ },
+ "ProtoDefinition": "my-greeter"
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/StatusModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/StatusModel.cs
index 2259bc599..45e73528d 100644
--- a/src/WireMock.Net.Abstractions/Admin/Mappings/StatusModel.cs
+++ b/src/WireMock.Net.Abstractions/Admin/Mappings/StatusModel.cs
@@ -24,4 +24,13 @@ public class StatusModel
/// The error message.
///
public string? Error { get; set; }
+
+ ///
+ /// Returns a string that represents the current status model, including its unique identifier, status, and error information.
+ ///
+ /// A string containing the values of the Guid, Status, and Error properties formatted for display.
+ public override string ToString()
+ {
+ return $"StatusModel [Guid={Guid}, Status={Status}, Error={Error}]";
+ }
}
\ No newline at end of file
diff --git a/src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj b/src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj
index a835e090f..088098d4f 100644
--- a/src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj
+++ b/src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj
@@ -30,7 +30,10 @@
-
+
+
+
+
diff --git a/src/WireMock.Net.Aspire/WireMockMappingState.cs b/src/WireMock.Net.Aspire/WireMockMappingState.cs
index 2bb82108a..a8e6c01b7 100644
--- a/src/WireMock.Net.Aspire/WireMockMappingState.cs
+++ b/src/WireMock.Net.Aspire/WireMockMappingState.cs
@@ -4,7 +4,9 @@ namespace WireMock.Net.Aspire;
internal enum WireMockMappingState
{
- NoMappings,
- NotSubmitted,
- Submitted,
-}
+ NoMappings = 0,
+
+ NotSubmitted = 1,
+
+ Submitted = 2
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Aspire/WireMockServerArguments.cs b/src/WireMock.Net.Aspire/WireMockServerArguments.cs
index 8c059c1a1..3a28aefa7 100644
--- a/src/WireMock.Net.Aspire/WireMockServerArguments.cs
+++ b/src/WireMock.Net.Aspire/WireMockServerArguments.cs
@@ -1,7 +1,9 @@
// Copyright © WireMock.Net
using System.Diagnostics.CodeAnalysis;
+using Stef.Validation;
using WireMock.Client.Builders;
+using WireMock.Util;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
@@ -21,10 +23,15 @@ public class WireMockServerArguments
private const string DefaultLogger = "WireMockConsoleLogger";
///
- /// The HTTP port where WireMock.Net is listening.
+ /// The HTTP ports where WireMock.Net is listening on.
/// If not defined, .NET Aspire automatically assigns a random port.
///
- public int? HttpPort { get; set; }
+ public List HttpPorts { get; set; } = [];
+
+ ///
+ /// Additional Urls on which WireMock listens.
+ ///
+ public List AdditionalUrls { get; set; } = [];
///
/// The admin username.
@@ -67,6 +74,42 @@ public class WireMockServerArguments
///
public Func? ApiMappingBuilder { get; set; }
+ ///
+ /// Grpc ProtoDefinitions.
+ ///
+ public Dictionary ProtoDefinitions { get; set; } = [];
+
+ ///
+ /// Add an additional Urls on which WireMock should listen.
+ ///
+ /// The additional urls which the WireMock Server should listen on.
+ public void WithAdditionalUrls(params string[] additionalUrls)
+ {
+ foreach (var url in additionalUrls)
+ {
+ if (!PortUtils.TryExtract(Guard.NotNullOrEmpty(url), out _, out _, out _, out _, out var port))
+ {
+ throw new ArgumentException($"The URL '{url}' is not valid.");
+ }
+
+ AdditionalUrls.Add(Guard.NotNullOrWhiteSpace(url));
+ HttpPorts.Add(port);
+ }
+ }
+
+ ///
+ /// Add a Grpc ProtoDefinition at server-level.
+ ///
+ /// Unique identifier for the ProtoDefinition.
+ /// The ProtoDefinition as text.
+ public void WithProtoDefinition(string id, params string[] protoDefinitions)
+ {
+ Guard.NotNullOrWhiteSpace(id);
+ Guard.NotNullOrEmpty(protoDefinitions);
+
+ ProtoDefinitions[id] = protoDefinitions;
+ }
+
///
/// Converts the current instance's properties to an array of command-line arguments for starting the WireMock.Net server.
///
@@ -95,6 +138,11 @@ public string[] GetArgs()
Add(args, "--WatchStaticMappingsInSubdirectories", "true");
}
+ if (AdditionalUrls.Count > 0)
+ {
+ Add(args, "--Urls", $"http://*:{HttpContainerPort} {string.Join(' ', AdditionalUrls)}");
+ }
+
return args
.SelectMany(k => new[] { k.Key, k.Value })
.ToArray();
diff --git a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs
index b19e192e2..07c5dc79f 100644
--- a/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs
+++ b/src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs
@@ -8,6 +8,7 @@
using Stef.Validation;
using WireMock.Client.Builders;
using WireMock.Net.Aspire;
+using WireMock.Util;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
@@ -34,9 +35,31 @@ public static IResourceBuilder AddWireMock(this IDistrib
Guard.NotNullOrWhiteSpace(name);
Guard.Condition(port, p => p is null or > 0 and <= ushort.MaxValue);
- return builder.AddWireMock(name, callback =>
+ return builder.AddWireMock(name, serverArguments =>
{
- callback.HttpPort = port;
+ if (port != null)
+ {
+ serverArguments.HttpPorts = [port.Value];
+ }
+ });
+ }
+
+ ///
+ /// Adds a WireMock.Net Server resource to the application model.
+ ///
+ /// The .
+ /// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
+ /// The additional urls which the WireMock Server should listen on.
+ /// A reference to the .
+ public static IResourceBuilder AddWireMock(this IDistributedApplicationBuilder builder, string name, params string[] additionalUrls)
+ {
+ Guard.NotNull(builder);
+ Guard.NotNullOrWhiteSpace(name);
+ Guard.NotNull(additionalUrls);
+
+ return builder.AddWireMock(name, serverArguments =>
+ {
+ serverArguments.WithAdditionalUrls(additionalUrls);
});
}
@@ -67,10 +90,37 @@ public static IResourceBuilder AddWireMock(this IDistrib
.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 (arguments.HttpPorts.Count == 0)
+ {
+ resourceBuilder = resourceBuilder.WithHttpEndpoint(port: null, targetPort: WireMockServerArguments.HttpContainerPort);
+ }
+ else if (arguments.HttpPorts.Count == 1)
+ {
+ resourceBuilder = resourceBuilder.WithHttpEndpoint(port: arguments.HttpPorts[0], targetPort: WireMockServerArguments.HttpContainerPort);
+ }
+ else
+ {
+ // Required for the default admin endpoint and health checks
+ resourceBuilder = resourceBuilder.WithHttpEndpoint(port: null, targetPort: WireMockServerArguments.HttpContainerPort);
+
+ var anyIsHttp2 = false;
+ foreach (var url in arguments.AdditionalUrls)
+ {
+ PortUtils.TryExtract(url, out _, out var isHttp2, out var scheme, out _, out var httpPort);
+ anyIsHttp2 |= isHttp2;
+
+ resourceBuilder = resourceBuilder.WithEndpoint(port: httpPort, targetPort: httpPort, scheme: scheme, name: $"{scheme}-{httpPort}");
+ }
+
+ if (anyIsHttp2)
+ {
+ resourceBuilder = resourceBuilder.AsHttp2Service();
+ }
+ }
+
if (!string.IsNullOrEmpty(arguments.MappingsPath))
{
resourceBuilder = resourceBuilder.WithBindMount(arguments.MappingsPath, DefaultLinuxMappingsPath);
@@ -84,6 +134,9 @@ public static IResourceBuilder AddWireMock(this IDistrib
}
});
+ // Always add the lifecycle hook to support dynamic mappings and proto definitions
+ resourceBuilder.ApplicationBuilder.Services.TryAddLifecycleHook();
+
return resourceBuilder;
}
@@ -94,7 +147,10 @@ public static IResourceBuilder AddWireMock(this IDistrib
/// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
/// A callback that allows for setting the .
/// A reference to the .
- public static IResourceBuilder AddWireMock(this IDistributedApplicationBuilder builder, string name, Action callback)
+ public static IResourceBuilder AddWireMock(
+ this IDistributedApplicationBuilder builder,
+ string name,
+ Action callback)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
@@ -165,7 +221,7 @@ public static IResourceBuilder WithAdminUserNameAndPassw
///
/// The .
/// Delegate that will be invoked to configure the WireMock.Net resource.
- ///
+ /// A reference to the .
public static IResourceBuilder WithApiMappingBuilder(this IResourceBuilder wiremock, Func configure)
{
return wiremock.WithApiMappingBuilder((adminApiMappingBuilder, _) => configure.Invoke(adminApiMappingBuilder));
@@ -176,18 +232,31 @@ public static IResourceBuilder WithApiMappingBuilder(thi
///
/// The .
/// Delegate that will be invoked to configure the WireMock.Net resource.
- ///
+ /// A reference to the .
public static IResourceBuilder WithApiMappingBuilder(this IResourceBuilder wiremock, Func configure)
{
Guard.NotNull(wiremock);
- wiremock.ApplicationBuilder.Services.TryAddLifecycleHook();
wiremock.Resource.Arguments.ApiMappingBuilder = configure;
wiremock.Resource.ApiMappingState = WireMockMappingState.NotSubmitted;
return wiremock;
}
+ ///
+ /// Add a Grpc ProtoDefinition at server-level.
+ ///
+ /// The .
+ /// Unique identifier for the ProtoDefinition.
+ /// The ProtoDefinition as text.
+ /// A reference to the .
+ public static IResourceBuilder WithProtoDefinition(this IResourceBuilder wiremock, string id, params string[] protoDefinitions)
+ {
+ Guard.NotNull(wiremock).Resource.Arguments.WithProtoDefinition(id, protoDefinitions);
+
+ return wiremock;
+ }
+
///
/// Enables the WireMockInspect, a cross-platform UI app that facilitates WireMock troubleshooting.
/// This requires installation of the WireMockInspector tool.
@@ -195,11 +264,11 @@ public static IResourceBuilder WithApiMappingBuilder(thi
/// dotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources
///
///
- /// The .
- ///
- public static IResourceBuilder WithWireMockInspectorCommand(this IResourceBuilder builder)
+ /// The .
+ /// A reference to the .
+ public static IResourceBuilder WithWireMockInspectorCommand(this IResourceBuilder wiremock)
{
- Guard.NotNull(builder);
+ Guard.NotNull(wiremock);
CommandOptions commandOptions = new()
{
@@ -209,13 +278,13 @@ public static IResourceBuilder WithWireMockInspectorComm
IconVariant = IconVariant.Filled
};
- builder.WithCommand(
+ wiremock.WithCommand(
name: "wiremock-inspector",
displayName: "WireMock Inspector",
- executeCommand: _ => OnRunOpenInspectorCommandAsync(builder),
+ executeCommand: _ => OnRunOpenInspectorCommandAsync(wiremock),
commandOptions: commandOptions);
- return builder;
+ return wiremock;
}
private static Task OnRunOpenInspectorCommandAsync(IResourceBuilder builder)
diff --git a/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs b/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs
index 57df9ee81..024724dfe 100644
--- a/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs
+++ b/src/WireMock.Net.Aspire/WireMockServerLifecycleHook.cs
@@ -1,5 +1,6 @@
// Copyright © WireMock.Net
+using System.Diagnostics;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;
@@ -28,10 +29,12 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C
wireMockServerResource.SetLogger(loggerFactory.CreateLogger());
var endpoint = wireMockServerResource.GetEndpoint();
- System.Diagnostics.Debug.Assert(endpoint.IsAllocated);
+ Debug.Assert(endpoint.IsAllocated);
await wireMockServerResource.WaitForHealthAsync(_linkedCts.Token);
+ await wireMockServerResource.CallAddProtoDefinitionsAsync(_linkedCts.Token);
+
await wireMockServerResource.CallApiMappingBuilderActionAsync(_linkedCts.Token);
wireMockServerResource.StartWatchingStaticMappings(_linkedCts.Token);
diff --git a/src/WireMock.Net.Aspire/WireMockServerResource.cs b/src/WireMock.Net.Aspire/WireMockServerResource.cs
index 528b21003..d9529c112 100644
--- a/src/WireMock.Net.Aspire/WireMockServerResource.cs
+++ b/src/WireMock.Net.Aspire/WireMockServerResource.cs
@@ -70,6 +70,34 @@ internal async Task CallApiMappingBuilderActionAsync(CancellationToken cancellat
ApiMappingState = WireMockMappingState.Submitted;
}
+ internal async Task CallAddProtoDefinitionsAsync(CancellationToken cancellationToken)
+ {
+ _logger?.LogInformation("Calling AdminApi to add GRPC ProtoDefinition at server level to WireMock.Net");
+
+ foreach (var (id, protoDefinitions) in Arguments.ProtoDefinitions)
+ {
+ _logger?.LogInformation("Adding ProtoDefinition {Id}", id);
+ foreach (var protoDefinition in protoDefinitions)
+ {
+ try
+ {
+ var status = await AdminApi.Value.AddProtoDefinitionAsync(id, protoDefinition, cancellationToken);
+ _logger?.LogInformation("ProtoDefinition '{Id}' added with status: {Status}.", id, status.Status);
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogWarning(ex, "Error adding ProtoDefinition '{Id}'.", id);
+ }
+ }
+ }
+
+ // Force a reload of static mappings when ProtoDefinitions are added at server-level to fix #1382
+ if (Arguments.ProtoDefinitions.Count > 0)
+ {
+ await ReloadStaticMappingsAsync(default);
+ }
+ }
+
internal void StartWatchingStaticMappings(CancellationToken cancellationToken)
{
if (!Arguments.WatchStaticMappings || string.IsNullOrEmpty(Arguments.MappingsPath))
@@ -113,10 +141,17 @@ private IWireMockAdminApi CreateWireMockAdminApi()
private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArgs args)
{
- _logger?.LogInformation("MappingFile created, changed or deleted: '{0}'. Triggering ReloadStaticMappings.", args.FullPath);
+ _logger?.LogInformation("MappingFile created, changed or deleted: '{FullPath}'. Triggering ReloadStaticMappings.", args.FullPath);
+
+ await ReloadStaticMappingsAsync(default);
+ }
+
+ private async Task ReloadStaticMappingsAsync(CancellationToken cancellationToken)
+ {
try
{
- await AdminApi.Value.ReloadStaticMappingsAsync();
+ var status = await AdminApi.Value.ReloadStaticMappingsAsync(cancellationToken);
+ _logger?.LogInformation("ReloadStaticMappings called with status: {Status}.", status);
}
catch (Exception ex)
{
diff --git a/src/WireMock.Net.Minimal/Settings/SimpleSettingsParser.cs b/src/WireMock.Net.Minimal/Settings/SimpleSettingsParser.cs
index 5c282c233..a02e06d2c 100644
--- a/src/WireMock.Net.Minimal/Settings/SimpleSettingsParser.cs
+++ b/src/WireMock.Net.Minimal/Settings/SimpleSettingsParser.cs
@@ -55,7 +55,7 @@ public void Parse(string[] arguments, IDictionary? environment = null)
// Now also parse environment
if (environment != null)
{
- foreach (string key in environment.Keys)
+ foreach (var key in environment.Keys.OfType())
{
if (key.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase) && environment.TryGetStringValue(key, out var value))
{
diff --git a/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs b/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs
index 90c2b3357..0dba1c218 100644
--- a/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs
+++ b/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs
@@ -153,7 +153,7 @@ private static void ParsePortSettings(WireMockServerSettings settings, SimpleSet
}
else if (settings.HostingScheme is null)
{
- settings.Urls = parser.GetValues("Urls", ["http://*:9091/"]);
+ settings.Urls = parser.GetValues(nameof(WireMockServerSettings.Urls), defaultValue: ["http://*:9091/"]);
}
}
diff --git a/src/WireMock.Net.Minimal/Util/PortUtils.cs b/src/WireMock.Net.Minimal/Util/PortUtils.cs
index 1cc2aad8d..d83fc3a01 100644
--- a/src/WireMock.Net.Minimal/Util/PortUtils.cs
+++ b/src/WireMock.Net.Minimal/Util/PortUtils.cs
@@ -84,22 +84,22 @@ public static IReadOnlyList FindFreeTcpPorts(int count)
}
///
- /// Extract the isHttps, isHttp2, protocol, host and port from a URL.
+ /// Extract the isHttps, isHttp2, scheme, host and port from a URL.
///
- public static bool TryExtract(string url, out bool isHttps, out bool isHttp2, [NotNullWhen(true)] out string? protocol, [NotNullWhen(true)] out string? host, out int port)
+ public static bool TryExtract(string url, out bool isHttps, out bool isHttp2, [NotNullWhen(true)] out string? scheme, [NotNullWhen(true)] out string? host, out int port)
{
isHttps = false;
isHttp2 = false;
- protocol = null;
+ scheme = null;
host = null;
port = 0;
var match = UrlDetailsRegex.Match(url);
if (match.Success)
{
- protocol = match.Groups["proto"].Value;
- isHttps = protocol.StartsWith("https", StringComparison.OrdinalIgnoreCase) || protocol.StartsWith("grpcs", StringComparison.OrdinalIgnoreCase);
- isHttp2 = protocol.StartsWith("grpc", StringComparison.OrdinalIgnoreCase);
+ scheme = match.Groups["proto"].Value;
+ isHttps = scheme.StartsWith("https", StringComparison.OrdinalIgnoreCase) || scheme.StartsWith("grpcs", StringComparison.OrdinalIgnoreCase);
+ isHttp2 = scheme.StartsWith("grpc", StringComparison.OrdinalIgnoreCase);
host = match.Groups["host"].Value;
return int.TryParse(match.Groups["port"].Value, out port);
diff --git a/src/WireMock.Net.Testcontainers/WireMockContainer.cs b/src/WireMock.Net.Testcontainers/WireMockContainer.cs
index a01a6add4..002a04694 100644
--- a/src/WireMock.Net.Testcontainers/WireMockContainer.cs
+++ b/src/WireMock.Net.Testcontainers/WireMockContainer.cs
@@ -156,7 +156,8 @@ public async Task ReloadStaticMappingsAsync(CancellationToken cancellationToken
try
{
- await _adminApi.ReloadStaticMappingsAsync(cancellationToken);
+ var result = await _adminApi.ReloadStaticMappingsAsync(cancellationToken);
+ Logger.LogInformation("ReloadStaticMappings result: {Result}", result);
}
catch (Exception ex)
{
@@ -231,7 +232,8 @@ private async Task CallAdditionalActionsAfterStartedAsync()
{
try
{
- await _adminApi!.AddProtoDefinitionAsync(kvp.Key, protoDefinition);
+ var result = await _adminApi!.AddProtoDefinitionAsync(kvp.Key, protoDefinition);
+ Logger.LogInformation("AddProtoDefinition '{Id}' result: {Result}", kvp.Key, result);
}
catch (Exception ex)
{
@@ -239,6 +241,12 @@ private async Task CallAdditionalActionsAfterStartedAsync()
}
}
}
+
+ // Force a reload of static mappings when ProtoDefinitions are added at server-level to fix #1382
+ if (_configuration.ProtoDefinitions.Count > 0)
+ {
+ await ReloadStaticMappingsAsync();
+ }
}
private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArgs args)
@@ -246,6 +254,7 @@ private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArg
try
{
await ReloadStaticMappingsAsync(args.FullPath);
+ Logger.LogInformation("ReloadStaticMappings triggered from file change: '{FullPath}'.", args.FullPath);
}
catch (Exception ex)
{
diff --git a/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs b/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs
index ef634ff98..16cf55dd5 100644
--- a/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs
+++ b/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs
@@ -112,6 +112,7 @@ public WireMockContainerBuilder WithWatchStaticMappings(bool includeSubDirectori
{
DockerResourceConfiguration.WithWatchStaticMappings(includeSubDirectories);
return
+ WithCommand("--ReadStaticMappings true").
WithCommand("--WatchStaticMappings true").
WithCommand("--WatchStaticMappingsInSubdirectories", includeSubDirectories);
}
@@ -129,9 +130,7 @@ public WireMockContainerBuilder WithMappings(string path, bool includeSubDirecto
DockerResourceConfiguration.WithStaticMappingsPath(path);
- return
- WithReadStaticMappings().
- WithCommand("--WatchStaticMappingsInSubdirectories", includeSubDirectories);
+ return WithWatchStaticMappings(includeSubDirectories);
}
///
diff --git a/test/WireMock.Net.Aspire.Tests/WireMockServerArgumentsTests.cs b/test/WireMock.Net.Aspire.Tests/WireMockServerArgumentsTests.cs
index 15a4ca3c0..60571ab5f 100644
--- a/test/WireMock.Net.Aspire.Tests/WireMockServerArgumentsTests.cs
+++ b/test/WireMock.Net.Aspire.Tests/WireMockServerArgumentsTests.cs
@@ -13,7 +13,7 @@ public void DefaultValues_ShouldBeSetCorrectly()
var args = new WireMockServerArguments();
// Assert
- args.HttpPort.Should().BeNull();
+ args.HttpPorts.Should().BeEmpty();
args.AdminUsername.Should().BeNull();
args.AdminPassword.Should().BeNull();
args.ReadStaticMappings.Should().BeFalse();
diff --git a/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs b/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs
index 88ea28a05..8a2d61744 100644
--- a/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs
+++ b/test/WireMock.Net.Aspire.Tests/WireMockServerBuilderExtensionsTests.cs
@@ -3,6 +3,7 @@
using System.Net.Sockets;
using FluentAssertions;
using Moq;
+using WireMock.Util;
namespace WireMock.Net.Aspire.Tests;
@@ -40,7 +41,21 @@ public void AddWireMock_WithInvalidPort_ShouldThrowArgumentOutOfRangeException()
}
[Fact]
- public void AddWireMock()
+ public void AddWireMock_WithInvalidAdditionalUrls_ShouldThrowArgumentException()
+ {
+ // Arrange
+ string[] invalidUrls = { "err" };
+ var builder = Mock.Of();
+
+ // Act
+ Action act = () => builder.AddWireMock("ValidName", invalidUrls);
+
+ // Assert
+ act.Should().Throw().WithMessage("The URL 'err' is not valid.");
+ }
+
+ [Fact]
+ public void AddWireMockWithPort()
{
// Arrange
var name = $"apiservice{Guid.NewGuid()}";
@@ -65,7 +80,7 @@ public void AddWireMock()
ReadStaticMappings = true,
WatchStaticMappings = false,
MappingsPath = null,
- HttpPort = port
+ HttpPorts = [port]
});
wiremock.Resource.Annotations.Should().HaveCount(6);
@@ -90,9 +105,90 @@ public void AddWireMock()
));
wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull();
-
wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull();
+ wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull();
+ }
+
+ [Fact]
+ public void AddWireMockWithAdditionalUrls()
+ {
+ // Arrange
+ var name = $"apiservice{Guid.NewGuid()}";
+ var freePorts = PortUtils.FindFreeTcpPorts(2).ToList();
+ string[] additionalUrls = { $"http://*:{freePorts[0]}", $"grpc://*:{freePorts[1]}" };
+ const string username = "admin";
+ const string password = "test";
+ var builder = DistributedApplication.CreateBuilder();
+
+ // Act
+ var wiremock = builder
+ .AddWireMock(name, additionalUrls)
+ .WithAdminUserNameAndPassword(username, password)
+ .WithReadStaticMappings();
+
+ // Assert
+ wiremock.Resource.Should().NotBeNull();
+ wiremock.Resource.Name.Should().Be(name);
+ wiremock.Resource.Arguments.Should().BeEquivalentTo(new WireMockServerArguments
+ {
+ AdminPassword = password,
+ AdminUsername = username,
+ ReadStaticMappings = true,
+ WatchStaticMappings = false,
+ MappingsPath = null,
+ HttpPorts = freePorts,
+ AdditionalUrls = additionalUrls.ToList()
+ });
+ wiremock.Resource.Annotations.Should().HaveCount(9);
+
+ var containerImageAnnotation = wiremock.Resource.Annotations.OfType().FirstOrDefault();
+ containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation
+ {
+ Image = "sheyenrath/wiremock.net-alpine",
+ Registry = null,
+ Tag = "latest"
+ });
+
+ var endpointAnnotations = wiremock.Resource.Annotations.OfType().ToArray();
+ endpointAnnotations.Should().HaveCount(3);
+ var endpointAnnotationForHttp80 = endpointAnnotations[0];
+ endpointAnnotationForHttp80.Should().BeEquivalentTo(new EndpointAnnotation(
+ protocol: ProtocolType.Tcp,
+ uriScheme: "http",
+ transport: null,
+ name: null,
+ port: null,
+ targetPort: 80,
+ isExternal: null,
+ isProxied: true
+ ));
+ var endpointAnnotationForHttpFreePort = endpointAnnotations[1];
+ endpointAnnotationForHttpFreePort.Should().BeEquivalentTo(new EndpointAnnotation(
+ protocol: ProtocolType.Tcp,
+ uriScheme: "http",
+ transport: null,
+ name: $"http-{freePorts[0]}",
+ port: freePorts[0],
+ targetPort: freePorts[0],
+ isExternal: null,
+ isProxied: true
+ ));
+
+ var endpointAnnotationForGrpcFreePort = endpointAnnotations[2];
+ endpointAnnotationForGrpcFreePort.Should().BeEquivalentTo(new EndpointAnnotation(
+ protocol: ProtocolType.Tcp,
+ uriScheme: "grpc",
+ transport: null,
+ name: $"grpc-{freePorts[1]}",
+ port: freePorts[1],
+ targetPort: freePorts[1],
+ isExternal: null,
+ isProxied: true
+ ));
+
+ wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull();
+ wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull();
wiremock.Resource.Annotations.OfType().FirstOrDefault().Should().NotBeNull();
}
}
\ No newline at end of file
diff --git a/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs b/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs
index 12b903b2b..0afa831f2 100644
--- a/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs
+++ b/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs
@@ -56,7 +56,7 @@ public async Task WireMockContainer_Build_Grpc_TestPortsAndUrls1()
var grpcPort = wireMockContainer.GetMappedPublicPort(9090);
grpcPort.Should().BeGreaterThan(0);
- var grpcUrl = wireMockContainer.GetMappedPublicUrl(80);
+ var grpcUrl = wireMockContainer.GetMappedPublicUrl(9090);
grpcUrl.Should().StartWith("http://");
var adminClient = wireMockContainer.CreateWireMockAdminClient();
@@ -149,6 +149,18 @@ public async Task WireMockContainer_Build_Grpc_ProtoDefinitionAtServerLevel_Usin
await StopAsync(wireMockContainer);
}
+ [Fact]
+ public async Task WireMockContainer_Build_Grpc_ProtoDefinitionAtServerLevel_UsingGrpcGeneratedClient_AndWithWatchStaticMappings()
+ {
+ var wireMockContainer = await Given_WireMockContainerWithProtoDefinitionAtServerLevelWithWatchStaticMappingsIsStartedForHttpAndGrpcAsync();
+
+ var reply = await When_GrpcClient_Calls_SayHelloAsync(wireMockContainer);
+
+ Then_ReplyMessage_Should_BeCorrect(reply);
+
+ await StopAsync(wireMockContainer);
+ }
+
private static async Task Given_WireMockContainerIsStartedForHttpAndGrpcAsync()
{
var wireMockContainer = new WireMockContainerBuilder()
@@ -172,6 +184,19 @@ private static async Task Given_WireMockContainerWithProtoDef
return wireMockContainer;
}
+ private static async Task Given_WireMockContainerWithProtoDefinitionAtServerLevelWithWatchStaticMappingsIsStartedForHttpAndGrpcAsync()
+ {
+ var wireMockContainer = new WireMockContainerBuilder()
+ .AddUrl("grpc://*:9090")
+ .AddProtoDefinition("my-greeter", ReadFile("greet.proto"))
+ .WithMappings(Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings"))
+ .Build();
+
+ await wireMockContainer.StartAsync();
+
+ return wireMockContainer;
+ }
+
private static async Task Given_ProtoBufMappingIsAddedViaAdminInterfaceAsync(WireMockContainer wireMockContainer, string filename)
{
var mappingsJson = ReadFile(filename);
diff --git a/test/WireMock.Net.Tests/Util/PortUtilsTests.cs b/test/WireMock.Net.Tests/Util/PortUtilsTests.cs
index 90b7777fe..2915e75f0 100644
--- a/test/WireMock.Net.Tests/Util/PortUtilsTests.cs
+++ b/test/WireMock.Net.Tests/Util/PortUtilsTests.cs
@@ -15,13 +15,13 @@ public void PortUtils_TryExtract_InvalidUrl_Returns_False()
var url = "test";
// Act
- var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port);
+ var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port);
// Assert
result.Should().BeFalse();
isHttps.Should().BeFalse();
isGrpc.Should().BeFalse();
- proto.Should().BeNull();
+ scheme.Should().BeNull();
host.Should().BeNull();
port.Should().Be(default(int));
}
@@ -33,13 +33,13 @@ public void PortUtils_TryExtract_UrlIsMissingPort_Returns_False()
var url = "http://0.0.0.0";
// Act
- var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port);
+ var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port);
// Assert
result.Should().BeFalse();
isHttps.Should().BeFalse();
isGrpc.Should().BeFalse();
- proto.Should().BeNull();
+ scheme.Should().BeNull();
host.Should().BeNull();
port.Should().Be(default(int));
}
@@ -51,13 +51,13 @@ public void PortUtils_TryExtract_Http_Returns_True()
var url = "http://wiremock.net:1234";
// Act
- var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port);
+ var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port);
// Assert
result.Should().BeTrue();
isHttps.Should().BeFalse();
isGrpc.Should().BeFalse();
- proto.Should().Be("http");
+ scheme.Should().Be("http");
host.Should().Be("wiremock.net");
port.Should().Be(1234);
}
@@ -69,13 +69,13 @@ public void PortUtils_TryExtract_Https_Returns_True()
var url = "https://wiremock.net:5000";
// Act
- var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port);
+ var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port);
// Assert
result.Should().BeTrue();
isHttps.Should().BeTrue();
isGrpc.Should().BeFalse();
- proto.Should().Be("https");
+ scheme.Should().Be("https");
host.Should().Be("wiremock.net");
port.Should().Be(5000);
}
@@ -87,13 +87,13 @@ public void PortUtils_TryExtract_Grpc_Returns_True()
var url = "grpc://wiremock.net:1234";
// Act
- var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port);
+ var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port);
// Assert
result.Should().BeTrue();
isHttps.Should().BeFalse();
isGrpc.Should().BeTrue();
- proto.Should().Be("grpc");
+ scheme.Should().Be("grpc");
host.Should().Be("wiremock.net");
port.Should().Be(1234);
}
@@ -105,13 +105,13 @@ public void PortUtils_TryExtract_Https0_0_0_0_Returns_True()
var url = "https://0.0.0.0:5000";
// Act
- var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port);
+ var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port);
// Assert
result.Should().BeTrue();
isHttps.Should().BeTrue();
isGrpc.Should().BeFalse();
- proto.Should().Be("https");
+ scheme.Should().Be("https");
host.Should().Be("0.0.0.0");
port.Should().Be(5000);
}