Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "AddUrl" to WireMockContainerBuilder to support grpc #1246

Merged
merged 6 commits into from
Jan 29, 2025
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
2 changes: 1 addition & 1 deletion examples/WireMock.Net.TestcontainersExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ private static async Task TestAsync(string? image = null)
var dummyNetwork = new NetworkBuilder()
.WithName($"Dummy Network for {image ?? "null"}")
.WithReuse(true)
.WithCleanUp(true)
// .WithCleanUp(true)
.Build();

var builder = new WireMockContainerBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,19 @@
<ItemGroup>
<Compile Include="..\WireMock.Net\Http\HttpClientFactory2.cs" Link="Http\HttpClientFactory2.cs" />
<Compile Include="..\WireMock.Net\Util\EnhancedFileSystemWatcher.cs" Link="Utils\EnhancedFileSystemWatcher.cs" />
<Compile Include="..\WireMock.Net\Util\PortUtils.cs" Link="Util\PortUtils.cs" />
<Compile Include="..\WireMock.Net\Constants\WireMockConstants.cs" Link="Constants\WireMockConstants.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="IsExternalInit" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Nullable" Version="1.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Stef.Validation" Version="0.1.1" />
<PackageReference Include="Testcontainers" Version="4.0.0" />
</ItemGroup>
Expand Down
16 changes: 16 additions & 0 deletions src/WireMock.Net.Testcontainers/WireMockConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright © WireMock.Net

using System.Collections.Generic;
using System.Linq;
using Docker.DotNet.Models;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
Expand All @@ -24,6 +26,8 @@ public sealed class WireMockConfiguration : ContainerConfiguration

public bool HasBasicAuthentication => !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password);

public List<string> AdditionalUrls { get; private set; } = [];

public WireMockConfiguration(string? username = null, string? password = null)
{
Username = username;
Expand Down Expand Up @@ -70,6 +74,7 @@ public WireMockConfiguration(WireMockConfiguration oldValue, WireMockConfigurati
StaticMappingsPath = BuildConfiguration.Combine(oldValue.StaticMappingsPath, newValue.StaticMappingsPath);
WatchStaticMappings = BuildConfiguration.Combine(oldValue.WatchStaticMappings, newValue.WatchStaticMappings);
WatchStaticMappingsInSubdirectories = BuildConfiguration.Combine(oldValue.WatchStaticMappingsInSubdirectories, newValue.WatchStaticMappingsInSubdirectories);
AdditionalUrls = BuildConfiguration.Combine(oldValue.AdditionalUrls.AsEnumerable(), newValue.AdditionalUrls.AsEnumerable()).ToList();
}

/// <summary>
Expand All @@ -94,4 +99,15 @@ public WireMockConfiguration WithWatchStaticMappings(bool includeSubDirectories)
WatchStaticMappingsInSubdirectories = includeSubDirectories;
return this;
}

/// <summary>
/// An additional Url on which WireMock listens.
/// </summary>
/// <param name="url">The url to add.</param>
/// <returns><see cref="WireMockConfiguration"/></returns>
public WireMockConfiguration WithAdditionalUrl(string url)
{
AdditionalUrls.Add(url);
return this;
}
}
53 changes: 50 additions & 3 deletions src/WireMock.Net.Testcontainers/WireMockContainer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright © WireMock.Net

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -30,6 +32,7 @@ public sealed class WireMockContainer : DockerContainer

private IWireMockAdminApi? _adminApi;
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
private IDictionary<int, Uri>? _publicUris;

/// <summary>
/// Initializes a new instance of the <see cref="WireMockContainer" /> class.
Expand All @@ -48,6 +51,21 @@ public WireMockContainer(WireMockConfiguration configuration) : base(configurati
[PublicAPI]
public string GetPublicUrl() => GetPublicUri().ToString();

/// <summary>
/// Gets the public Urls as a dictionary with the internal port as the key.
/// </summary>
[PublicAPI]
public IDictionary<int, string> GetPublicUrls() => GetPublicUris().ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString());

/// <summary>
/// Gets the mapped public port for the given container port.
/// </summary>
[PublicAPI]
public string GetMappedPublicUrl(int containerPort)
{
return GetPublicUris()[containerPort].ToString();
}

/// <summary>
/// Create a RestEase Admin client which can be used to call the admin REST endpoint.
/// </summary>
Expand Down Expand Up @@ -121,7 +139,7 @@ public HttpClient CreateClient(HttpMessageHandler innerHandler, params Delegatin
await ReloadStaticMappingsAsync(target, ct);
}
}

/// <summary>
/// Reload the static mappings.
/// </summary>
Expand Down Expand Up @@ -198,7 +216,14 @@ private void WireMockContainer_Started(object sender, EventArgs e)

private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArgs args)
{
await ReloadStaticMappingsAsync(args.FullPath);
try
{
await ReloadStaticMappingsAsync(args.FullPath);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error reloading static mappings from '{FullPath}'.", args.FullPath);
}
}

private async Task ReloadStaticMappingsAsync(string path, CancellationToken cancellationToken = default)
Expand All @@ -207,5 +232,27 @@ private async Task ReloadStaticMappingsAsync(string path, CancellationToken canc
await ReloadStaticMappingsAsync(cancellationToken);
}

private Uri GetPublicUri() => new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(ContainerPort)).Uri;
private Uri GetPublicUri() => GetPublicUris()[ContainerPort];

private IDictionary<int, Uri> GetPublicUris()
{
if (_publicUris != null)
{
return _publicUris;
}

_publicUris = _configuration.ExposedPorts.Keys
.Select(int.Parse)
.ToDictionary(port => port, port => new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(port)).Uri);

foreach (var url in _configuration.AdditionalUrls)
{
if (PortUtils.TryExtract(url, out _, out _, out _, out _, out var port))
{
_publicUris[port] = new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(port)).Uri;
}
}

return _publicUris;
}
}
37 changes: 37 additions & 0 deletions src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// Copyright © WireMock.Net

using System;
using System.Linq;
using System.Runtime.InteropServices;
using Docker.DotNet.Models;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using JetBrains.Annotations;
using Stef.Validation;
using WireMock.Net.Testcontainers.Utils;
using WireMock.Util;

namespace WireMock.Net.Testcontainers;

Expand Down Expand Up @@ -132,6 +134,36 @@ public WireMockContainerBuilder WithMappings(string path, bool includeSubDirecto
WithCommand("--WatchStaticMappingsInSubdirectories", includeSubDirectories);
}

/// <summary>
/// Use Http version 2.
/// </summary>
/// <returns>A configured instance of <see cref="WireMockContainerBuilder"/></returns>
[PublicAPI]
public WireMockContainerBuilder WithHttp2()
{
return WithCommand("--UseHttp2 true");
}

/// <summary>
/// Adds another URL to the WireMock container. By default, the WireMock container will listen on <c>http://*:80</c>.
///
/// This method can be used to also host the WireMock container on another port or protocol (like grpc).
/// </summary>
/// <example>grpc://*:9090</example>
/// <returns>A configured instance of <see cref="WireMockContainerBuilder"/></returns>
[PublicAPI]
public WireMockContainerBuilder AddUrl(string url)
{
if (!PortUtils.TryExtract(Guard.NotNullOrEmpty(url), out _, out _, out _, out _, out var port))
{
throw new ArgumentException("The URL is not valid.", nameof(url));
}

DockerResourceConfiguration.WithAdditionalUrl(url);

return WithPortBinding(port, true);
}

private WireMockContainerBuilder WithCommand(string param, bool value)
{
return !value ? this : WithCommand($"{param} true");
Expand Down Expand Up @@ -172,6 +204,11 @@ public override WireMockContainer Build()
builder = builder.WithBindMount(builder.DockerResourceConfiguration.StaticMappingsPath, ContainerInfoProvider.Info[_imageOS.Value].MappingsPath);
}

if (builder.DockerResourceConfiguration.AdditionalUrls.Any())
{
builder = builder.WithCommand($"--Urls http://*:80 {string.Join(" ", builder.DockerResourceConfiguration.AdditionalUrls)}");
}

builder.Validate();

return new WireMockContainer(builder.DockerResourceConfiguration);
Expand Down
28 changes: 1 addition & 27 deletions src/WireMock.Net/Util/PortUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,32 +83,6 @@ public static IReadOnlyList<int> FindFreeTcpPorts(int count)
}
}

///// <summary>
///// Finds free TCP ports.
///// </summary>
//public static IReadOnlyList<int> FindFreeTcpPorts(int numPorts)
//{
// var freePorts = new List<int>();

// TcpListener? tcpListener = null;
// try
// {
// for (var i = 0; i < numPorts; i++)
// {
// tcpListener = new TcpListener(IPAddress.Loopback, 0);
// tcpListener.Start();

// freePorts.Add(((IPEndPoint)tcpListener.LocalEndpoint).Port);
// }
// }
// finally
// {
// tcpListener?.Stop();
// }

// return freePorts;
//}

/// <summary>
/// Extract the isHttps, isHttp2, protocol, host and port from a URL.
/// </summary>
Expand All @@ -118,7 +92,7 @@ public static bool TryExtract(string url, out bool isHttps, out bool isHttp2, [N
isHttp2 = false;
protocol = null;
host = null;
port = default;
port = 0;

var match = UrlDetailsRegex.Match(url);
if (match.Success)
Expand Down
31 changes: 21 additions & 10 deletions test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
using Google.Protobuf.WellKnownTypes;
using Greet;
using Grpc.Net.Client;
using NarrowIntegrationTest.Lookup;
using ExampleIntegrationTest.Lookup;
using WireMock.Constants;
using WireMock.Matchers;
using WireMock.RequestBuilders;
Expand Down Expand Up @@ -668,7 +668,7 @@ public async Task WireMockServer_WithBodyAsProtoBuf_Enum_UsingPolicyGrpcGenerate
.RespondWith(Response.Create()
.WithHeader("Content-Type", "application/grpc")
.WithTrailingHeader("grpc-status", "0")
.WithBodyAsProtoBuf(definition, "NarrowIntegrationTest.Lookup.GetVersionResponse",
.WithBodyAsProtoBuf(definition, "ExampleIntegrationTest.Lookup.GetVersionResponse",
new GetVersionResponse
{
Version = version,
Expand All @@ -677,9 +677,9 @@ public async Task WireMockServer_WithBodyAsProtoBuf_Enum_UsingPolicyGrpcGenerate
Seconds = seconds,
Nanos = nanos
},
Client = new NarrowIntegrationTest.Lookup.Client
Client = new ExampleIntegrationTest.Lookup.Client
{
ClientName = NarrowIntegrationTest.Lookup.Client.Types.Clients.BillingCenter,
ClientName = ExampleIntegrationTest.Lookup.Client.Types.Clients.Test,
CorrelationId = correlationId
}
}
Expand All @@ -695,23 +695,34 @@ public async Task WireMockServer_WithBodyAsProtoBuf_Enum_UsingPolicyGrpcGenerate
// Assert
reply.Version.Should().Be(version);
reply.DateHired.Should().Be(new Timestamp { Seconds = seconds, Nanos = nanos });
reply.Client.ClientName.Should().Be(NarrowIntegrationTest.Lookup.Client.Types.Clients.BillingCenter);
reply.Client.ClientName.Should().Be(ExampleIntegrationTest.Lookup.Client.Types.Clients.Test);
reply.Client.CorrelationId.Should().Be(correlationId);
}

[Fact]
public async Task WireMockServer_WithBodyAsProtoBuf_FromJson_UsingGrpcGeneratedClient()
{
var server = Given_When_ServerStarted_And_RunningOnHttpAndGrpc();
await Given_When_ProtoBufMappingIsAddedViaAdminInterfaceAsync(server, "protobuf-mapping-1.json");

var reply = await When_GrpcClient_Calls_SayHelloAsync(server.Urls[1]);

Then_ReplyMessage_Should_BeCorrect(reply);
}

[Fact]
public async Task WireMockServer_WithBodyAsProtoBuf_ServerProtoDefinitionFromJson_UsingGrpcGeneratedClient()
{
var server = Given_When_ServerStartedUsingHttp2();
var server = Given_When_ServerStarted_And_RunningOnHttpAndGrpc();
Given_ProtoDefinition_IsAddedOnServerLevel(server);
await Given_When_ProtoBufMappingIsAddedViaAdminInterfaceAsync(server);
await Given_When_ProtoBufMappingIsAddedViaAdminInterfaceAsync(server, "protobuf-mapping-3.json");

var reply = await When_GrpcClient_Calls_SayHelloAsync(server.Urls[1]);

Then_ReplyMessage_Should_BeCorrect(reply);
}

private static WireMockServer Given_When_ServerStartedUsingHttp2()
private static WireMockServer Given_When_ServerStarted_And_RunningOnHttpAndGrpc()
{
var ports = PortUtils.FindFreeTcpPorts(2);

Expand All @@ -728,9 +739,9 @@ private static void Given_ProtoDefinition_IsAddedOnServerLevel(WireMockServer se
server.AddProtoDefinition("my-greeter", ReadProtoFile("greet.proto"));
}

private static async Task Given_When_ProtoBufMappingIsAddedViaAdminInterfaceAsync(WireMockServer server)
private static async Task Given_When_ProtoBufMappingIsAddedViaAdminInterfaceAsync(WireMockServer server, string filename)
{
var mappingsJson = ReadMappingFile("protobuf-mapping-3.json");
var mappingsJson = ReadMappingFile(filename);

using var httpClient = server.CreateClient();

Expand Down
15 changes: 3 additions & 12 deletions test/WireMock.Net.Tests/Grpc/policy.proto
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
syntax = "proto3";

option csharp_namespace = "NarrowIntegrationTest.Lookup";
option csharp_namespace = "ExampleIntegrationTest.Lookup";

import "google/protobuf/timestamp.proto";

Expand All @@ -24,17 +24,8 @@ message Client {
string CorrelationId = 1;
enum Clients {
Unknown = 0;
QMS = 1;
BillingCenter = 2;
PAS = 3;
Payroll = 4;
Portal = 5;
SFO = 6;
QuoteAndBind = 7;
LegacyConversion = 8;
BindNow = 9;
PaymentPortal = 10 ;
PricingEngine = 11;
Other = 1;
Test = 2;
}
Clients ClientName = 2;
}
Loading
Loading