From 0d5fb07da6c12c2c863a127be720399366f266c0 Mon Sep 17 00:00:00 2001 From: Stef Date: Thu, 9 Jan 2025 20:11:04 +0100 Subject: [PATCH 1/4] Fix ProtoBuf Mappings --- .../IWireMockAdminApi.cs | 11 +- .../ResponseBuilders/Response.WithHeaders.cs | 6 +- .../Serialization/MappingConverter.cs | 2 +- .../Serialization/MatcherMapper.cs | 2 +- .../Server/WireMockServer.ConvertMapping.cs | 45 +++- ...s => WireMockAdminApiTests.GetMappings.cs} | 35 ++- ...uldReturnCorrectMappingModels.verified.txt | 235 ++++++++++++++++++ .../WireMockAdminApiTests.PostMappings.cs | 196 +++++++++++++++ .../AdminApi/WireMockAdminApiTests.cs | 124 +-------- .../WireMockServer.Admin.cs | 10 +- .../__admin/mappings/protobuf-mapping-1.json | 49 ++++ .../__admin/mappings/protobuf-mapping-2.json | 48 ++++ .../__admin/mappings/protobuf-mapping-3.json | 48 ++++ .../__admin/mappings/protobuf-mapping-4.json | 40 +++ 14 files changed, 713 insertions(+), 138 deletions(-) rename test/WireMock.Net.Tests/AdminApi/{WireMockAdminApiTests.GetMappingsAsync.cs => WireMockAdminApiTests.GetMappings.cs} (85%) create mode 100644 test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.HttpClient_GetMappingsAsync_WithBodyAsProtoBuf_ShouldReturnCorrectMappingModels.verified.txt create mode 100644 test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.PostMappings.cs create mode 100644 test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-1.json create mode 100644 test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-2.json create mode 100644 test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-3.json create mode 100644 test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-4.json diff --git a/src/WireMock.Net.RestClient/IWireMockAdminApi.cs b/src/WireMock.Net.RestClient/IWireMockAdminApi.cs index 53bedebb..fc4e4d72 100644 --- a/src/WireMock.Net.RestClient/IWireMockAdminApi.cs +++ b/src/WireMock.Net.RestClient/IWireMockAdminApi.cs @@ -130,7 +130,7 @@ public interface IWireMockAdminApi Task ReloadStaticMappingsAsync(CancellationToken cancellationToken = default); /// - /// Get a mapping based on the guid + /// Get a mapping based on the guid. /// /// The Guid /// MappingModel @@ -138,6 +138,15 @@ public interface IWireMockAdminApi [Get("mappings/{guid}")] Task GetMappingAsync([Path] Guid guid, CancellationToken cancellationToken = default); + /// + /// Get a mapping based on the guid. + /// + /// The Guid + /// MappingModel + /// The optional cancellationToken. + [Get("mappings/{guid}")] + Task GetMappingAsync([Path] string guid, CancellationToken cancellationToken = default); + /// /// Get the C# code from a mapping based on the guid /// diff --git a/src/WireMock.Net/ResponseBuilders/Response.WithHeaders.cs b/src/WireMock.Net/ResponseBuilders/Response.WithHeaders.cs index 3f19d28d..1b457988 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.WithHeaders.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.WithHeaders.cs @@ -49,7 +49,7 @@ public IResponseBuilder WithHeaders(IDictionary> he public IResponseBuilder WithTrailingHeader(string name, params string[] values) { #if !TRAILINGHEADERS - throw new System.NotSupportedException("The WithBodyAsProtoBuf method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); + throw new System.NotSupportedException("The WithTrailingHeader method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); #else Guard.NotNull(name); @@ -63,7 +63,7 @@ public IResponseBuilder WithTrailingHeader(string name, params string[] values) public IResponseBuilder WithTrailingHeaders(IDictionary headers) { #if !TRAILINGHEADERS - throw new System.NotSupportedException("The WithBodyAsProtoBuf method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); + throw new System.NotSupportedException("The WithTrailingHeaders method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); #else Guard.NotNull(headers); @@ -77,7 +77,7 @@ public IResponseBuilder WithTrailingHeaders(IDictionary headers) public IResponseBuilder WithTrailingHeaders(IDictionary headers) { #if !TRAILINGHEADERS - throw new System.NotSupportedException("The WithBodyAsProtoBuf method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); + throw new System.NotSupportedException("The WithTrailingHeaders method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); #else Guard.NotNull(headers); diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs index 260fba53..1bb70ba0 100644 --- a/src/WireMock.Net/Serialization/MappingConverter.cs +++ b/src/WireMock.Net/Serialization/MappingConverter.cs @@ -379,7 +379,7 @@ public MappingModel ToMappingModel(IMapping mapping) } var bodyMatchers = - protoBufMatcher?.Matcher != null ? new[] { protoBufMatcher.Matcher } : null ?? + protoBufMatcher?.Matcher != null ? [protoBufMatcher.Matcher] : null ?? multiPartMatcher?.Matchers ?? graphQLMatcher?.Matchers ?? bodyMatcher?.Matchers; diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs index 8cadbb62..d3c987af 100644 --- a/src/WireMock.Net/Serialization/MatcherMapper.cs +++ b/src/WireMock.Net/Serialization/MatcherMapper.cs @@ -217,7 +217,7 @@ public MatcherMapper(WireMockServerSettings settings) { model.Pattern = texts[0]; } - else + else if (texts.Count > 1) { model.Patterns = texts.Cast().ToArray(); } diff --git a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs index 9e3559e0..4457d869 100644 --- a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs +++ b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs @@ -116,6 +116,15 @@ private Guid ConvertMappingAndRegisterAsRespondProvider(MappingModel mappingMode respondProvider.WithProbability(mappingModel.Probability.Value); } + if (mappingModel.ProtoDefinition != null) + { + respondProvider.WithProtoDefinition(mappingModel.ProtoDefinition); + } + else if (mappingModel.ProtoDefinitions != null) + { + respondProvider.WithProtoDefinition(mappingModel.ProtoDefinitions); + } + var responseBuilder = InitResponseBuilder(mappingModel.Response); respondProvider.RespondWith(responseBuilder); @@ -317,6 +326,22 @@ private static IResponseBuilder InitResponseBuilder(ResponseModel responseModel) } } + if (responseModel.TrailingHeaders != null) + { + foreach (var entry in responseModel.TrailingHeaders) + { + if (entry.Value is string value) + { + responseBuilder.WithTrailingHeader(entry.Key, value); + } + else + { + var headers = JsonUtils.ParseJTokenToObject(entry.Value); + responseBuilder.WithTrailingHeader(entry.Key, headers); + } + } + } + if (responseModel.BodyAsBytes != null) { responseBuilder = responseBuilder.WithBody(responseModel.BodyAsBytes, responseModel.BodyDestination, ToEncoding(responseModel.BodyEncoding)); @@ -327,7 +352,25 @@ private static IResponseBuilder InitResponseBuilder(ResponseModel responseModel) } else if (responseModel.BodyAsJson != null) { - responseBuilder = responseBuilder.WithBodyAsJson(responseModel.BodyAsJson, ToEncoding(responseModel.BodyEncoding), responseModel.BodyAsJsonIndented == true); + if (responseModel.ProtoBufMessageType != null) + { + if (responseModel.ProtoDefinition != null) + { + responseBuilder = responseBuilder.WithBodyAsProtoBuf(responseModel.ProtoDefinition, responseModel.ProtoBufMessageType, responseModel.BodyAsJson); + } + else if (responseModel.ProtoDefinitions != null) + { + responseBuilder = responseBuilder.WithBodyAsProtoBuf(responseModel.ProtoDefinitions, responseModel.ProtoBufMessageType, responseModel.BodyAsJson); + } + else + { + responseBuilder = responseBuilder.WithBodyAsProtoBuf(responseModel.ProtoBufMessageType, responseModel.BodyAsJson); + } + } + else + { + responseBuilder = responseBuilder.WithBodyAsJson(responseModel.BodyAsJson, ToEncoding(responseModel.BodyEncoding), responseModel.BodyAsJsonIndented == true); + } } else if (responseModel.BodyAsFile != null) { diff --git a/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.GetMappingsAsync.cs b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.GetMappings.cs similarity index 85% rename from test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.GetMappingsAsync.cs rename to test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.GetMappings.cs index 9db83fdc..a98b2cf5 100644 --- a/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.GetMappingsAsync.cs +++ b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.GetMappings.cs @@ -37,7 +37,32 @@ message HelloReply { public async Task IWireMockAdminApi_GetMappingsAsync_WithBodyAsProtoBuf_ShouldReturnCorrectMappingModels() { // Arrange - using var server = WireMockServer.StartWithAdminInterface(); + using var server = Given_WithBodyAsProtoBuf_AddedToServer(); + + // Act + var api = RestClient.For(server.Url); + var getMappingsResult = await api.GetMappingsAsync().ConfigureAwait(false); + + await Verifier.Verify(getMappingsResult, VerifySettings); + } + + [Fact] + public async Task HttpClient_GetMappingsAsync_WithBodyAsProtoBuf_ShouldReturnCorrectMappingModels() + { + // Arrange + using var server = Given_WithBodyAsProtoBuf_AddedToServer(); + + // Act + var client = server.CreateClient(); + var getMappingsResult = await client.GetStringAsync("/__admin/mappings").ConfigureAwait(false); + + await Verifier.VerifyJson(getMappingsResult, VerifySettings); + } + + public WireMockServer Given_WithBodyAsProtoBuf_AddedToServer() + { + // Arrange + var server = WireMockServer.StartWithAdminInterface(); var protoBufJsonMatcher = new JsonPartialWildcardMatcher(new { name = "*" }); @@ -122,13 +147,7 @@ public async Task IWireMockAdminApi_GetMappingsAsync_WithBodyAsProtoBuf_ShouldRe .WithTransformer() ); - // Act - var api = RestClient.For(server.Url); - var getMappingsResult = await api.GetMappingsAsync().ConfigureAwait(false); - - await Verifier.Verify(getMappingsResult, VerifySettings); - - server.Stop(); + return server; } } #endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.HttpClient_GetMappingsAsync_WithBodyAsProtoBuf_ShouldReturnCorrectMappingModels.verified.txt b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.HttpClient_GetMappingsAsync_WithBodyAsProtoBuf_ShouldReturnCorrectMappingModels.verified.txt new file mode 100644 index 00000000..6da88594 --- /dev/null +++ b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.HttpClient_GetMappingsAsync_WithBodyAsProtoBuf_ShouldReturnCorrectMappingModels.verified.txt @@ -0,0 +1,235 @@ +[ + { + Guid: Guid_1, + UpdatedAt: DateTimeOffset_1, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /grpc/greet.Greeter/SayHello, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Body: { + Matcher: { + Name: ProtoBufMatcher, + Pattern: +syntax = "proto3"; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +, + ContentMatcher: { + Name: JsonPartialWildcardMatcher, + Pattern: { + name: * + }, + IgnoreCase: false, + Regex: false + }, + ProtoBufMessageType: greet.HelloRequest + } + } + }, + Response: { + BodyAsJson: { + message: hello {{request.BodyAsJson.name}} + }, + UseTransformer: true, + TransformerType: Handlebars, + TransformerReplaceNodeOptions: EvaluateAndTryToConvert, + Headers: { + Content-Type: application/grpc + }, + TrailingHeaders: { + grpc-status: 0 + }, + ProtoDefinition: +syntax = "proto3"; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +, + ProtoBufMessageType: greet.HelloReply + } + }, + { + Guid: Guid_2, + UpdatedAt: DateTimeOffset_2, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /grpc2/greet.Greeter/SayHello, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Body: { + Matcher: { + Name: ProtoBufMatcher, + ContentMatcher: { + Name: JsonPartialWildcardMatcher, + Pattern: { + name: * + }, + IgnoreCase: false, + Regex: false + }, + ProtoBufMessageType: greet.HelloRequest + } + } + }, + Response: { + BodyAsJson: { + message: hello {{request.BodyAsJson.name}} + }, + UseTransformer: true, + TransformerType: Handlebars, + TransformerReplaceNodeOptions: EvaluateAndTryToConvert, + Headers: { + Content-Type: application/grpc + }, + TrailingHeaders: { + grpc-status: 0 + }, + ProtoBufMessageType: greet.HelloReply + }, + ProtoDefinition: +syntax = "proto3"; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} + + }, + { + Guid: Guid_3, + UpdatedAt: DateTimeOffset_3, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /grpc3/greet.Greeter/SayHello, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Body: { + Matcher: { + Name: ProtoBufMatcher, + ContentMatcher: { + Name: JsonPartialWildcardMatcher, + Pattern: { + name: * + }, + IgnoreCase: false, + Regex: false + }, + ProtoBufMessageType: greet.HelloRequest + } + } + }, + Response: { + BodyAsJson: { + message: hello {{request.BodyAsJson.name}} + }, + UseTransformer: true, + TransformerType: Handlebars, + TransformerReplaceNodeOptions: EvaluateAndTryToConvert, + Headers: { + Content-Type: application/grpc + }, + TrailingHeaders: { + grpc-status: 0 + }, + ProtoBufMessageType: greet.HelloReply + }, + ProtoDefinition: my-greeter + }, + { + Guid: Guid_4, + UpdatedAt: DateTimeOffset_4, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /grpc4/greet.Greeter/SayHello, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Body: { + Matcher: { + Name: ProtoBufMatcher, + ProtoBufMessageType: greet.HelloRequest + } + } + }, + Response: { + BodyAsJson: { + message: hello {{request.BodyAsJson.name}} + }, + 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/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.PostMappings.cs b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.PostMappings.cs new file mode 100644 index 00000000..e02c5217 --- /dev/null +++ b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.PostMappings.cs @@ -0,0 +1,196 @@ +// Copyright © WireMock.Net + +#if !(NET452 || NET461 || NETCOREAPP3_1) +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using NFluent; +using RestEase; +using VerifyTests; +using VerifyXunit; +using WireMock.Admin.Mappings; +using WireMock.Admin.Settings; +using WireMock.Client; +using WireMock.Client.Extensions; +using WireMock.Constants; +using WireMock.Handlers; +using WireMock.Logging; +using WireMock.Matchers; +using WireMock.Models; +using WireMock.Net.Tests.VerifyExtensions; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; +using WireMock.Settings; +using WireMock.Types; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.AdminApi; + +public partial class WireMockAdminApiTests +{ + public static string RemoveLineContainingUpdatedAt(string text) + { + var lines = text.Split([Environment.NewLine], StringSplitOptions.None); + var filteredLines = lines.Where(line => !line.Contains("\"UpdatedAt\": ")); + return string.Join(Environment.NewLine, filteredLines); + } + + [Theory] + [InlineData("protobuf-mapping-1.json", "351f0240-bba0-4bcb-93c6-1feba0fe0001")] + [InlineData("protobuf-mapping-2.json", "351f0240-bba0-4bcb-93c6-1feba0fe0002")] + [InlineData("protobuf-mapping-3.json", "351f0240-bba0-4bcb-93c6-1feba0fe0003")] + [InlineData("protobuf-mapping-4.json", "351f0240-bba0-4bcb-93c6-1feba0fe0004")] + public async Task HttpClient_PostMappingsAsync_ForProtoBufMapping(string mappingFile, string guid) + { + // Arrange + var mappingsJson = ReadMappingFile(mappingFile); + + using var server = WireMockServer.StartWithAdminInterface(); + var httpClient = server.CreateClient(); + + // Act + var result = await httpClient.PostAsync("/__admin/mappings", new StringContent(mappingsJson, Encoding.UTF8, WireMockConstants.ContentTypeJson)); + result.EnsureSuccessStatusCode(); + + // Assert + var mappings = server.Mappings + .Where(m => !m.IsAdminInterface) + .OrderBy(m => m.Title) + .ToArray(); + + var mapping = await httpClient.GetStringAsync($"/__admin/mappings/{guid}"); + + RemoveLineContainingUpdatedAt(mapping).Should().Be(mappingsJson); + } + + [Fact] + public async Task IWireMockAdminApi_PostMappingsAsync() + { + // Arrange + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Urls[0]); + + // Act + var model1 = new MappingModel + { + Request = new RequestModel { Path = "/1" }, + Response = new ResponseModel { Body = "txt 1" }, + Title = "test 1" + }; + var model2 = new MappingModel + { + Request = new RequestModel { Path = "/2" }, + Response = new ResponseModel { Body = "txt 2" }, + Title = "test 2" + }; + var result = await api.PostMappingsAsync(new[] { model1, model2 }).ConfigureAwait(false); + + // Assert + Check.That(result).IsNotNull(); + Check.That(result.Status).IsNotNull(); + Check.That(result.Guid).IsNull(); + Check.That(server.Mappings.Where(m => !m.IsAdminInterface)).HasSize(2); + + server.Stop(); + } + + [Theory] + [InlineData(null, null)] + [InlineData(-1, -1)] + [InlineData(0, 0)] + [InlineData(200, 200)] + [InlineData("200", "200")] + public async Task IWireMockAdminApi_PostMappingAsync_WithStatusCode(object statusCode, object expectedStatusCode) + { + // Arrange + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Urls[0]); + + // Act + var model = new MappingModel + { + Request = new RequestModel { Path = "/1" }, + Response = new ResponseModel { Body = "txt", StatusCode = statusCode }, + Priority = 500, + Title = "test" + }; + var result = await api.PostMappingAsync(model).ConfigureAwait(false); + + // Assert + Check.That(result).IsNotNull(); + Check.That(result.Status).IsNotNull(); + Check.That(result.Guid).IsNotNull(); + + var mapping = server.Mappings.Single(m => m.Priority == 500); + Check.That(mapping).IsNotNull(); + Check.That(mapping.Title).Equals("test"); + + var response = await mapping.ProvideResponseAsync(new RequestMessage(new UrlDetails("http://localhost/1"), "GET", "")).ConfigureAwait(false); + Check.That(response.Message.StatusCode).Equals(expectedStatusCode); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_PostMappingsAsync_WithDuplicateGuids_Should_Return_400() + { + // Arrange + var guid = Guid.Parse("1b731398-4a5b-457f-a6e3-d65e541c428f"); + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Urls[0]); + + // Act + var model1WithGuid = new MappingModel + { + Guid = guid, + Request = new RequestModel { Path = "/1g" }, + Response = new ResponseModel { Body = "txt 1g" }, + Title = "test 1g" + }; + var model2WithGuid = new MappingModel + { + Guid = guid, + Request = new RequestModel { Path = "/2g" }, + Response = new ResponseModel { Body = "txt 2g" }, + Title = "test 2g" + }; + var model1 = new MappingModel + { + Request = new RequestModel { Path = "/1" }, + Response = new ResponseModel { Body = "txt 1" }, + Title = "test 1" + }; + var model2 = new MappingModel + { + Request = new RequestModel { Path = "/2" }, + Response = new ResponseModel { Body = "txt 2" }, + Title = "test 2" + }; + + var models = new[] + { + model1WithGuid, + model2WithGuid, + model1, + model2 + }; + + var sutMethod = async () => await api.PostMappingsAsync(models); + var exceptionAssertions = await sutMethod.Should().ThrowAsync(); + exceptionAssertions.Which.Content.Should().Be(@"{""Status"":""The following Guids are duplicate : '1b731398-4a5b-457f-a6e3-d65e541c428f' (Parameter 'mappingModels')""}"); + + server.Stop(); + } +} +#endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.cs b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.cs index 8e19470d..0ef68c82 100644 --- a/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.cs +++ b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.cs @@ -184,124 +184,7 @@ public async Task IWireMockAdminApi_PutMappingAsync() server.Stop(); } - [Theory] - [InlineData(null, null)] - [InlineData(-1, -1)] - [InlineData(0, 0)] - [InlineData(200, 200)] - [InlineData("200", "200")] - public async Task IWireMockAdminApi_PostMappingAsync_WithStatusCode(object statusCode, object expectedStatusCode) - { - // Arrange - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Urls[0]); - - // Act - var model = new MappingModel - { - Request = new RequestModel { Path = "/1" }, - Response = new ResponseModel { Body = "txt", StatusCode = statusCode }, - Priority = 500, - Title = "test" - }; - var result = await api.PostMappingAsync(model).ConfigureAwait(false); - - // Assert - Check.That(result).IsNotNull(); - Check.That(result.Status).IsNotNull(); - Check.That(result.Guid).IsNotNull(); - - var mapping = server.Mappings.Single(m => m.Priority == 500); - Check.That(mapping).IsNotNull(); - Check.That(mapping.Title).Equals("test"); - - var response = await mapping.ProvideResponseAsync(new RequestMessage(new UrlDetails("http://localhost/1"), "GET", "")).ConfigureAwait(false); - Check.That(response.Message.StatusCode).Equals(expectedStatusCode); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_PostMappingsAsync() - { - // Arrange - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Urls[0]); - - // Act - var model1 = new MappingModel - { - Request = new RequestModel { Path = "/1" }, - Response = new ResponseModel { Body = "txt 1" }, - Title = "test 1" - }; - var model2 = new MappingModel - { - Request = new RequestModel { Path = "/2" }, - Response = new ResponseModel { Body = "txt 2" }, - Title = "test 2" - }; - var result = await api.PostMappingsAsync(new[] { model1, model2 }).ConfigureAwait(false); - - // Assert - Check.That(result).IsNotNull(); - Check.That(result.Status).IsNotNull(); - Check.That(result.Guid).IsNull(); - Check.That(server.Mappings.Where(m => !m.IsAdminInterface)).HasSize(2); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_PostMappingsAsync_WithDuplicateGuids_Should_Return_400() - { - // Arrange - var guid = Guid.Parse("1b731398-4a5b-457f-a6e3-d65e541c428f"); - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Urls[0]); - - // Act - var model1WithGuid = new MappingModel - { - Guid = guid, - Request = new RequestModel { Path = "/1g" }, - Response = new ResponseModel { Body = "txt 1g" }, - Title = "test 1g" - }; - var model2WithGuid = new MappingModel - { - Guid = guid, - Request = new RequestModel { Path = "/2g" }, - Response = new ResponseModel { Body = "txt 2g" }, - Title = "test 2g" - }; - var model1 = new MappingModel - { - Request = new RequestModel { Path = "/1" }, - Response = new ResponseModel { Body = "txt 1" }, - Title = "test 1" - }; - var model2 = new MappingModel - { - Request = new RequestModel { Path = "/2" }, - Response = new ResponseModel { Body = "txt 2" }, - Title = "test 2" - }; - - var models = new[] - { - model1WithGuid, - model2WithGuid, - model1, - model2 - }; - - var sutMethod = async () => await api.PostMappingsAsync(models); - var exceptionAssertions = await sutMethod.Should().ThrowAsync(); - exceptionAssertions.Which.Content.Should().Be(@"{""Status"":""The following Guids are duplicate : '1b731398-4a5b-457f-a6e3-d65e541c428f' (Parameter 'mappingModels')""}"); - - server.Stop(); - } + [Fact] public async Task IWireMockAdminApi_FindRequestsAsync() @@ -1140,5 +1023,10 @@ public async Task IWireMockAdminApi_ReadStaticMappingsAsync() // Assert status.Status.Should().Be("Static Mappings reloaded"); } + + private static string ReadMappingFile(string filename) + { + return File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings", filename)); + } } #endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMockServer.Admin.cs b/test/WireMock.Net.Tests/WireMockServer.Admin.cs index aa4f518d..684633f9 100644 --- a/test/WireMock.Net.Tests/WireMockServer.Admin.cs +++ b/test/WireMock.Net.Tests/WireMockServer.Admin.cs @@ -15,7 +15,6 @@ using WireMock.Client; using WireMock.Handlers; using WireMock.Logging; -using WireMock.Matchers; using WireMock.Matchers.Request; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; @@ -27,7 +26,8 @@ namespace WireMock.Net.Tests; public class WireMockServerAdminTests { - // For for AppVeyor + OpenCover + private const int NumStaticMappings = 10; + private static string GetCurrentFolder() { return Directory.GetCurrentDirectory(); @@ -40,8 +40,8 @@ public void WireMockServer_Admin_ResetMappings() string folder = Path.Combine(GetCurrentFolder(), "__admin", "mappings"); server.ReadStaticMappings(folder); - Check.That(server.Mappings).HasSize(6); - Check.That(server.MappingModels).HasSize(6); + Check.That(server.Mappings).HasSize(NumStaticMappings); + Check.That(server.MappingModels).HasSize(NumStaticMappings); // Act server.ResetMappings(); @@ -220,7 +220,7 @@ public void WireMockServer_Admin_ReadStaticMappings() server.ReadStaticMappings(folder); var mappings = server.Mappings.ToArray(); - Check.That(mappings).HasSize(6); + Check.That(mappings).HasSize(NumStaticMappings); server.Stop(); } diff --git a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-1.json b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-1.json new file mode 100644 index 00000000..b4063561 --- /dev/null +++ b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-1.json @@ -0,0 +1,49 @@ +{ + "Guid": "351f0240-bba0-4bcb-93c6-1feba0fe0001", + "Title": "ProtoBuf Mapping 1", + "Request": { + "Path": { + "Matchers": [ + { + "Name": "WildcardMatcher", + "Pattern": "/grpc/greet.Greeter/SayHello", + "IgnoreCase": false + } + ] + }, + "Methods": [ + "POST" + ], + "Body": { + "Matcher": { + "Name": "ProtoBufMatcher", + "Pattern": "\r\nsyntax = \"proto3\";\r\n\r\npackage greet;\r\n\r\nservice Greeter {\r\n rpc SayHello (HelloRequest) returns (HelloReply);\r\n}\r\n\r\nmessage HelloRequest {\r\n string name = 1;\r\n}\r\n\r\nmessage HelloReply {\r\n string message = 1;\r\n}\r\n", + "ContentMatcher": { + "Name": "JsonPartialWildcardMatcher", + "Pattern": { + "name": "*" + }, + "IgnoreCase": false, + "Regex": false + }, + "ProtoBufMessageType": "greet.HelloRequest" + } + } + }, + "Response": { + "BodyAsJson": { + "message": "hello {{request.BodyAsJson.name}}" + }, + "UseTransformer": true, + "TransformerType": "Handlebars", + "TransformerReplaceNodeOptions": "EvaluateAndTryToConvert", + "Headers": { + "Content-Type": "application/grpc" + }, + "TrailingHeaders": { + "grpc-status": "0" + }, + "ProtoDefinition": "\r\nsyntax = \"proto3\";\r\n\r\npackage greet;\r\n\r\nservice Greeter {\r\n rpc SayHello (HelloRequest) returns (HelloReply);\r\n}\r\n\r\nmessage HelloRequest {\r\n string name = 1;\r\n}\r\n\r\nmessage HelloReply {\r\n string message = 1;\r\n}\r\n", + "ProtoBufMessageType": "greet.HelloReply" + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-2.json b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-2.json new file mode 100644 index 00000000..dec4c05b --- /dev/null +++ b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-2.json @@ -0,0 +1,48 @@ +{ + "Guid": "351f0240-bba0-4bcb-93c6-1feba0fe0002", + "Title": "ProtoBuf Mapping 2", + "Request": { + "Path": { + "Matchers": [ + { + "Name": "WildcardMatcher", + "Pattern": "/grpc2/greet.Greeter/SayHello", + "IgnoreCase": false + } + ] + }, + "Methods": [ + "POST" + ], + "Body": { + "Matcher": { + "Name": "ProtoBufMatcher", + "ContentMatcher": { + "Name": "JsonPartialWildcardMatcher", + "Pattern": { + "name": "*" + }, + "IgnoreCase": false, + "Regex": false + }, + "ProtoBufMessageType": "greet.HelloRequest" + } + } + }, + "Response": { + "BodyAsJson": { + "message": "hello {{request.BodyAsJson.name}}" + }, + "UseTransformer": true, + "TransformerType": "Handlebars", + "TransformerReplaceNodeOptions": "EvaluateAndTryToConvert", + "Headers": { + "Content-Type": "application/grpc" + }, + "TrailingHeaders": { + "grpc-status": "0" + }, + "ProtoBufMessageType": "greet.HelloReply" + }, + "ProtoDefinition": "\r\nsyntax = \"proto3\";\r\n\r\npackage greet;\r\n\r\nservice Greeter {\r\n rpc SayHello (HelloRequest) returns (HelloReply);\r\n}\r\n\r\nmessage HelloRequest {\r\n string name = 1;\r\n}\r\n\r\nmessage HelloReply {\r\n string message = 1;\r\n}\r\n" +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-3.json b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-3.json new file mode 100644 index 00000000..bc767b09 --- /dev/null +++ b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-3.json @@ -0,0 +1,48 @@ +{ + "Guid": "351f0240-bba0-4bcb-93c6-1feba0fe0003", + "Title": "ProtoBuf Mapping 3", + "Request": { + "Path": { + "Matchers": [ + { + "Name": "WildcardMatcher", + "Pattern": "/grpc3/greet.Greeter/SayHello", + "IgnoreCase": false + } + ] + }, + "Methods": [ + "POST" + ], + "Body": { + "Matcher": { + "Name": "ProtoBufMatcher", + "ContentMatcher": { + "Name": "JsonPartialWildcardMatcher", + "Pattern": { + "name": "*" + }, + "IgnoreCase": false, + "Regex": false + }, + "ProtoBufMessageType": "greet.HelloRequest" + } + } + }, + "Response": { + "BodyAsJson": { + "message": "hello {{request.BodyAsJson.name}}" + }, + "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/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-4.json b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-4.json new file mode 100644 index 00000000..ed188621 --- /dev/null +++ b/test/WireMock.Net.Tests/__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": "/grpc4/greet.Greeter/SayHello", + "IgnoreCase": false + } + ] + }, + "Methods": [ + "POST" + ], + "Body": { + "Matcher": { + "Name": "ProtoBufMatcher", + "ProtoBufMessageType": "greet.HelloRequest" + } + } + }, + "Response": { + "BodyAsJson": { + "message": "hello {{request.BodyAsJson.name}}" + }, + "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 From 3e06b5dfe5b2d996650b2ecb791c05e844a1bb09 Mon Sep 17 00:00:00 2001 From: Stef Date: Sun, 12 Jan 2025 10:25:27 +0100 Subject: [PATCH 2/4] [Fact(Skip = "#1233")] --- .../Server/IWireMockServer.cs | 6 +- .../Server/RespondWithAProvider.cs | 6 +- .../WireMockAdminApiTests.PostMappings.cs | 28 +------ .../Grpc/WireMockServerTests.Grpc.cs | 81 ++++++++++++++----- .../WireMock.Net.Tests.csproj | 1 + .../__admin/mappings/protobuf-mapping-3.json | 2 +- 6 files changed, 74 insertions(+), 50 deletions(-) diff --git a/src/WireMock.Net.Abstractions/Server/IWireMockServer.cs b/src/WireMock.Net.Abstractions/Server/IWireMockServer.cs index 013244de..f296550e 100644 --- a/src/WireMock.Net.Abstractions/Server/IWireMockServer.cs +++ b/src/WireMock.Net.Abstractions/Server/IWireMockServer.cs @@ -215,14 +215,16 @@ public interface IWireMockServer : IDisposable /// This can be used if you have 1 or more defined and want to register these in WireMock.Net directly instead of using the fluent syntax. /// /// The MappingModels + /// IWireMockServer WithMapping(params MappingModel[] mappings); /// /// Register the mappings (via json string). /// - /// This can be used if you the mappings as json string defined and want to register these in WireMock.Net directly instead of using the fluent syntax. + /// This can be used if you've the mappings as json string defined and want to register these in WireMock.Net directly instead of using the fluent syntax. /// /// The mapping(s) as json string. + /// IWireMockServer WithMapping(string mappings); /// @@ -238,5 +240,5 @@ public interface IWireMockServer : IDisposable /// /// The /// C# code - public string MappingsToCSharpCode(MappingConverterType converterType); + string MappingsToCSharpCode(MappingConverterType converterType); } \ No newline at end of file diff --git a/src/WireMock.Net/Server/RespondWithAProvider.cs b/src/WireMock.Net/Server/RespondWithAProvider.cs index 0ea2332d..a45eb714 100644 --- a/src/WireMock.Net/Server/RespondWithAProvider.cs +++ b/src/WireMock.Net/Server/RespondWithAProvider.cs @@ -296,7 +296,7 @@ public IRespondWithAProvider WithWebhook( Guard.NotNull(url); Guard.NotNull(method); - Webhooks = new[] { InitWebhook(url, method, headers, useTransformer, transformerType) }; + Webhooks = [InitWebhook(url, method, headers, useTransformer, transformerType)]; if (body != null) { @@ -323,7 +323,7 @@ public IRespondWithAProvider WithWebhook( Guard.NotNull(url); Guard.NotNull(method); - Webhooks = new[] { InitWebhook(url, method, headers, useTransformer, transformerType) }; + Webhooks = [InitWebhook(url, method, headers, useTransformer, transformerType)]; if (body != null) { @@ -371,7 +371,7 @@ public IRespondWithAProvider WithProtoDefinition(params string[] protoDefinition { _protoDefinition = new(null, protoDefinitionOrId); } - + return this; } diff --git a/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.PostMappings.cs b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.PostMappings.cs index e02c5217..2dfad302 100644 --- a/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.PostMappings.cs +++ b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.PostMappings.cs @@ -2,37 +2,18 @@ #if !(NET452 || NET461 || NETCOREAPP3_1) using System; -using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Net; using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.Http.Json; using System.Text; using System.Threading.Tasks; using FluentAssertions; -using Moq; using NFluent; using RestEase; -using VerifyTests; -using VerifyXunit; using WireMock.Admin.Mappings; -using WireMock.Admin.Settings; using WireMock.Client; -using WireMock.Client.Extensions; using WireMock.Constants; -using WireMock.Handlers; -using WireMock.Logging; -using WireMock.Matchers; using WireMock.Models; -using WireMock.Net.Tests.VerifyExtensions; -using WireMock.RequestBuilders; -using WireMock.ResponseBuilders; using WireMock.Server; -using WireMock.Settings; -using WireMock.Types; -using WireMock.Util; using Xunit; namespace WireMock.Net.Tests.AdminApi; @@ -64,14 +45,9 @@ public async Task HttpClient_PostMappingsAsync_ForProtoBufMapping(string mapping result.EnsureSuccessStatusCode(); // Assert - var mappings = server.Mappings - .Where(m => !m.IsAdminInterface) - .OrderBy(m => m.Title) - .ToArray(); - var mapping = await httpClient.GetStringAsync($"/__admin/mappings/{guid}"); - - RemoveLineContainingUpdatedAt(mapping).Should().Be(mappingsJson); + mapping = RemoveLineContainingUpdatedAt(mapping); + mapping.Should().Be(mappingsJson); } [Fact] diff --git a/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs b/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs index 063a2c69..f5820bf8 100644 --- a/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs +++ b/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs @@ -2,17 +2,21 @@ #if PROTOBUF using System; +using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Text; using System.Threading.Tasks; using FluentAssertions; using Greet; using Grpc.Net.Client; +using WireMock.Constants; using WireMock.Matchers; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; +using WireMock.Settings; using Xunit; // ReSharper disable once CheckNamespace @@ -128,8 +132,6 @@ public async Task WireMockServer_WithBodyAsProtoBuf_JsonPartialWildcardMatcher() // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); - - server.Stop(); } [Theory] @@ -171,8 +173,6 @@ public async Task WireMockServer_WithBodyAsProtoBuf(string data) var responseBytes = await response.Content.ReadAsByteArrayAsync(); Convert.ToBase64String(responseBytes).Should().Be("AAAAAAcKBWhlbGxv"); - - server.Stop(); } [Fact] @@ -209,8 +209,6 @@ public async Task WireMockServer_WithBodyAsProtoBuf_WithWellKnownTypes() var responseBytes = await response.Content.ReadAsByteArrayAsync(); Convert.ToBase64String(responseBytes).Should().Be(""); - - server.Stop(); } [Fact] @@ -251,8 +249,6 @@ public async Task WireMockServer_WithBodyAsProtoBuf_ServerProtoDefinition_WithWe var responseBytes = await response.Content.ReadAsByteArrayAsync(); Convert.ToBase64String(responseBytes).Should().Be(""); - - server.Stop(); } [Fact] @@ -294,8 +290,6 @@ public async Task WireMockServer_WithBodyAsProtoBuf_MultipleFiles() var responseBytes = await response.Content.ReadAsByteArrayAsync(); Convert.ToBase64String(responseBytes).Should().Be("AAAAAAcKBWhlbGxv"); - - server.Stop(); } [Fact] @@ -333,8 +327,6 @@ public async Task WireMockServer_WithBodyAsProtoBuf_InlineProtoDefinition_UsingG // Assert reply.Message.Should().Be("hello stef POST"); - - server.Stop(); } [Fact] @@ -374,8 +366,6 @@ public async Task WireMockServer_WithBodyAsProtoBuf_MappingProtoDefinition_Using // Assert reply.Message.Should().Be("hello stef POST"); - - server.Stop(); } [Fact] @@ -410,16 +400,71 @@ public async Task WireMockServer_WithBodyAsProtoBuf_ServerProtoDefinition_UsingG ); // Act - var channel = GrpcChannel.ForAddress(server.Url!); + var reply = await When_GrpcClient_Calls_SayHelloAsync(server.Url!); + + // Assert + Then_ReplyMessage_Should_BeCorrect(reply); + } + + [Fact(Skip = "#1233")] + public async Task WireMockServer_WithBodyAsProtoBuf_ServerProtoDefinitionFromJson_UsingGrpcGeneratedClient() + { + var server = Given_When_ServerStartedUsingHttp2(); + Given_ProtoDefinition_IsAddedOnServerLevel(server); + await Given_When_ProtoBufMappingIsAddedViaAdminInterfaceAsync(server); + + var reply = await When_GrpcClient_Calls_SayHelloAsync(server.Url!); + + Then_ReplyMessage_Should_BeCorrect(reply); + } + + private static WireMockServer Given_When_ServerStartedUsingHttp2() + { + var settings = new WireMockServerSettings + { + UseHttp2 = false, + StartAdminInterface = true + }; + return WireMockServer.Start(settings); + } + + private static void Given_ProtoDefinition_IsAddedOnServerLevel(WireMockServer server) + { + server.AddProtoDefinition("my-greeter-351f0240-bba0-4bcb-93c6-1feba0fe0003", ReadProtoFile("greet.proto")); + } + + private static async Task Given_When_ProtoBufMappingIsAddedViaAdminInterfaceAsync(WireMockServer server) + { + var mappingsJson = ReadMappingFile("protobuf-mapping-3.json"); + + using var httpClient = server.CreateClient(); + + var result = await httpClient.PostAsync("/__admin/mappings", new StringContent(mappingsJson, Encoding.UTF8, WireMockConstants.ContentTypeJson)); + result.EnsureSuccessStatusCode(); + } + + private static async Task When_GrpcClient_Calls_SayHelloAsync(string address) + { + var channel = GrpcChannel.ForAddress(address); var client = new Greeter.GreeterClient(channel); - var reply = await client.SayHelloAsync(new HelloRequest { Name = "stef" }); + return await client.SayHelloAsync(new HelloRequest { Name = "stef" }); + } - // Assert + private static void Then_ReplyMessage_Should_BeCorrect(HelloReply reply) + { reply.Message.Should().Be("hello stef POST"); + } - server.Stop(); + private static string ReadMappingFile(string filename) + { + return File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings", filename)); + } + + private static string ReadProtoFile(string filename) + { + return File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "Grpc", filename)); } } #endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj index 2468b7e4..3583956e 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -134,6 +134,7 @@ Client + PreserveNewest PreserveNewest diff --git a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-3.json b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-3.json index bc767b09..627f878f 100644 --- a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-3.json +++ b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-3.json @@ -44,5 +44,5 @@ }, "ProtoBufMessageType": "greet.HelloReply" }, - "ProtoDefinition": "my-greeter" + "ProtoDefinition": "my-greeter-351f0240-bba0-4bcb-93c6-1feba0fe0003" } \ No newline at end of file From f406e6ea26029cdc07262a2f47079ed0990a8697 Mon Sep 17 00:00:00 2001 From: Stef Date: Mon, 13 Jan 2025 18:15:37 +0100 Subject: [PATCH 3/4] fix? --- .../Helpers/BodyDataMatchScoreCalculator.cs | 6 ++++ src/WireMock.Net/Matchers/ProtoBufMatcher.cs | 3 +- .../RequestBuilders/IBodyRequestBuilder.cs | 1 - src/WireMock.Net/RequestBuilders/Request.cs | 15 ++++++++++ src/WireMock.Net/ResponseBuilders/Response.cs | 12 ++++---- .../Serialization/MatcherMapper.cs | 22 ++------------- .../Server/IRespondWithAProvider.cs | 6 ++-- .../Server/RespondWithAProvider.cs | 27 ++++-------------- .../Server/WireMockServer.ConvertMapping.cs | 22 +++++++++++---- src/WireMock.Net/Util/PortUtils.cs | 28 +++++++++++++++++++ .../Util/ProtoDefinitionHelper.cs | 27 ++++++++++++++++++ .../Grpc/WireMockServerTests.Grpc.cs | 13 +++++---- .../__admin/mappings/protobuf-mapping-1.json | 2 +- .../__admin/mappings/protobuf-mapping-2.json | 2 +- .../__admin/mappings/protobuf-mapping-3.json | 8 +++--- .../__admin/mappings/protobuf-mapping-4.json | 2 +- 16 files changed, 124 insertions(+), 72 deletions(-) create mode 100644 src/WireMock.Net/Util/ProtoDefinitionHelper.cs diff --git a/src/WireMock.Net/Matchers/Helpers/BodyDataMatchScoreCalculator.cs b/src/WireMock.Net/Matchers/Helpers/BodyDataMatchScoreCalculator.cs index bd660cf6..cb3bbb61 100644 --- a/src/WireMock.Net/Matchers/Helpers/BodyDataMatchScoreCalculator.cs +++ b/src/WireMock.Net/Matchers/Helpers/BodyDataMatchScoreCalculator.cs @@ -66,6 +66,12 @@ public static MatchResult CalculateMatchScore(IBodyData? requestMessage, IMatche return stringMatcher.IsMatch(requestMessage.BodyAsString); } + // In case the matcher is a IProtoBufMatcher, use the BodyAsBytes to match on. + if (matcher is IProtoBufMatcher protoBufMatcher) + { + return protoBufMatcher.IsMatchAsync(requestMessage.BodyAsBytes).GetAwaiter().GetResult(); + } + return default; } } \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/ProtoBufMatcher.cs b/src/WireMock.Net/Matchers/ProtoBufMatcher.cs index b29dcc8d..12e0c61b 100644 --- a/src/WireMock.Net/Matchers/ProtoBufMatcher.cs +++ b/src/WireMock.Net/Matchers/ProtoBufMatcher.cs @@ -2,7 +2,6 @@ #if PROTOBUF using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; using ProtoBufJsonConverter; @@ -28,7 +27,7 @@ public class ProtoBufMatcher : IProtoBufMatcher /// /// The Func to define the proto definition as id or texts. /// - public Func ProtoDefinition { get; } + public Func ProtoDefinition { get; internal set; } /// /// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". diff --git a/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs index 8e046b4a..ac01e3ea 100644 --- a/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs +++ b/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using JsonConverter.Abstractions; using WireMock.Matchers; using WireMock.Util; diff --git a/src/WireMock.Net/RequestBuilders/Request.cs b/src/WireMock.Net/RequestBuilders/Request.cs index 2d5d90f0..b37e92ab 100644 --- a/src/WireMock.Net/RequestBuilders/Request.cs +++ b/src/WireMock.Net/RequestBuilders/Request.cs @@ -5,8 +5,10 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Stef.Validation; +using WireMock.Matchers; using WireMock.Matchers.Request; namespace WireMock.RequestBuilders; @@ -71,6 +73,19 @@ public IList GetRequestMessageMatchers() where T : IRequestMatcher return _requestMatchers.OfType().FirstOrDefault(func); } + internal bool TryGetProtoBufMatcher([NotNullWhen(true)] out IProtoBufMatcher? protoBufMatcher) + { + protoBufMatcher = GetRequestMessageMatcher()?.Matcher; + if (protoBufMatcher != null) + { + return true; + } + + var bodyMatcher = GetRequestMessageMatcher(); + protoBufMatcher = bodyMatcher?.Matchers?.OfType().FirstOrDefault(); + return protoBufMatcher != null; + } + private IRequestBuilder Add(T requestMatcher) where T : IRequestMatcher { foreach (var existing in _requestMatchers.OfType().ToArray()) diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index 017f38e9..b04b7970 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Stef.Validation; -using WireMock.Matchers.Request; using WireMock.Proxy; using WireMock.RequestBuilders; using WireMock.Settings; @@ -264,16 +263,15 @@ string RemoveFirstOccurrence(string source, string find) if (UseTransformer) { - // Check if the body matcher is a RequestMessageProtoBufMatcher and try to decode the byte-array to a BodyAsJson. - if (mapping.RequestMatcher is Request requestMatcher && requestMessage is RequestMessage request) + // If the body matcher is a RequestMessageProtoBufMatcher or BodyMatcher with a ProtoBufMatcher then try to decode the byte-array to a BodyAsJson. + if (mapping.RequestMatcher is Request request && requestMessage is RequestMessage requestMessageImplementation) { - var protoBufMatcher = requestMatcher.GetRequestMessageMatcher()?.Matcher; - if (protoBufMatcher != null) + if (request.TryGetProtoBufMatcher(out var protoBufMatcher)) { - var decoded = await protoBufMatcher.DecodeAsync(request.BodyData?.BodyAsBytes).ConfigureAwait(false); + var decoded = await protoBufMatcher.DecodeAsync(requestMessage.BodyData?.BodyAsBytes).ConfigureAwait(false); if (decoded != null) { - request.BodyAsJson = JsonUtils.ConvertValueToJToken(decoded); + requestMessageImplementation.BodyAsJson = JsonUtils.ConvertValueToJToken(decoded); } } } diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs index d3c987af..9cc4e36e 100644 --- a/src/WireMock.Net/Serialization/MatcherMapper.cs +++ b/src/WireMock.Net/Serialization/MatcherMapper.cs @@ -293,27 +293,9 @@ private ProtoBufMatcher CreateProtoBufMatcher(MatchBehaviour? matchBehaviour, IR { var objectMatcher = Map(matcher.ContentMatcher) as IObjectMatcher; - IdOrTexts protoDefinitionAsIdOrTexts; - if (protoDefinitions.Count == 1) - { - var idOrText = protoDefinitions[0]; - if (_settings.ProtoDefinitions?.TryGetValue(idOrText, out var protoDefinitionFromSettings) == true) - { - protoDefinitionAsIdOrTexts = new(idOrText, protoDefinitionFromSettings); - } - else - { - protoDefinitionAsIdOrTexts = new(null, protoDefinitions); - } - } - else - { - protoDefinitionAsIdOrTexts = new(null, protoDefinitions); - } - return new ProtoBufMatcher( - () => protoDefinitionAsIdOrTexts, - matcher!.ProtoBufMessageType!, + () => ProtoDefinitionHelper.GetIdOrTexts(_settings, protoDefinitions.ToArray()), + matcher.ProtoBufMessageType!, matchBehaviour ?? MatchBehaviour.AcceptOnMatch, objectMatcher ); diff --git a/src/WireMock.Net/Server/IRespondWithAProvider.cs b/src/WireMock.Net/Server/IRespondWithAProvider.cs index 963e3e10..4d1ffa51 100644 --- a/src/WireMock.Net/Server/IRespondWithAProvider.cs +++ b/src/WireMock.Net/Server/IRespondWithAProvider.cs @@ -123,14 +123,14 @@ public interface IRespondWithAProvider void ThenRespondWithStatusCode(HttpStatusCode code); /// - /// Sets the the scenario. + /// Sets the scenario. /// /// The scenario. /// The . IRespondWithAProvider InScenario(string scenario); /// - /// Sets the the scenario with an integer value. + /// Sets the scenario with an integer value. /// /// The scenario. /// The . @@ -220,7 +220,7 @@ IRespondWithAProvider WithWebhook( /// /// Data Object which can be used when WithTransformer is used. - /// e.g. lookup an path in this object using + /// e.g. lookup a path in this object using /// The data dictionary object. /// /// lookup data "1" diff --git a/src/WireMock.Net/Server/RespondWithAProvider.cs b/src/WireMock.Net/Server/RespondWithAProvider.cs index a45eb714..3840c9c3 100644 --- a/src/WireMock.Net/Server/RespondWithAProvider.cs +++ b/src/WireMock.Net/Server/RespondWithAProvider.cs @@ -17,7 +17,7 @@ namespace WireMock.Server; /// -/// The respond with a provider. +/// The RespondWithAProvider. /// internal class RespondWithAProvider : IRespondWithAProvider { @@ -37,7 +37,6 @@ internal class RespondWithAProvider : IRespondWithAProvider private int _timesInSameState = 1; private bool? _useWebhookFireAndForget; private double? _probability; - private IdOrTexts? _protoDefinition; private GraphQLSchemaDetails? _graphQLSchemaDetails; public Guid Guid { get; private set; } @@ -48,6 +47,8 @@ internal class RespondWithAProvider : IRespondWithAProvider public object? Data { get; private set; } + public IdOrTexts? ProtoDefinition { get; private set; } + /// /// Initializes a new instance of the class. /// @@ -104,9 +105,9 @@ public void RespondWith(IResponseProvider provider) mapping.WithProbability(_probability.Value); } - if (_protoDefinition != null) + if (ProtoDefinition != null) { - mapping.WithProtoDefinition(_protoDefinition.Value); + mapping.WithProtoDefinition(ProtoDefinition.Value); } _registrationCallback(mapping, _saveToFile); @@ -355,23 +356,7 @@ public IRespondWithAProvider WithProtoDefinition(params string[] protoDefinition { Guard.NotNull(protoDefinitionOrId); - if (protoDefinitionOrId.Length == 1) - { - var idOrText = protoDefinitionOrId[0]; - if (_settings.ProtoDefinitions?.TryGetValue(idOrText, out var protoDefinitions) == true) - { - _protoDefinition = new(idOrText, protoDefinitions); - } - else - { - _protoDefinition = new(null, protoDefinitionOrId); - } - } - else - { - _protoDefinition = new(null, protoDefinitionOrId); - } - + ProtoDefinition = ProtoDefinitionHelper.GetIdOrTexts(_settings, protoDefinitionOrId); return this; } diff --git a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs index 4457d869..eaea8a1a 100644 --- a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs +++ b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs @@ -42,9 +42,9 @@ private Guid ConvertMappingAndRegisterAsRespondProvider(MappingModel mappingMode Guard.NotNull(mappingModel.Request); Guard.NotNull(mappingModel.Response); - var requestBuilder = InitRequestBuilder(mappingModel.Request); + var request = (Request)InitRequestBuilder(mappingModel.Request, mappingModel); - var respondProvider = Given(requestBuilder, mappingModel.SaveToFile == true); + var respondProvider = Given(request, mappingModel.SaveToFile == true); if (guid != null) { @@ -116,6 +116,7 @@ private Guid ConvertMappingAndRegisterAsRespondProvider(MappingModel mappingMode respondProvider.WithProbability(mappingModel.Probability.Value); } + // ProtoDefinition is defined at Mapping level if (mappingModel.ProtoDefinition != null) { respondProvider.WithProtoDefinition(mappingModel.ProtoDefinition); @@ -131,7 +132,7 @@ private Guid ConvertMappingAndRegisterAsRespondProvider(MappingModel mappingMode return respondProvider.Guid; } - private IRequestBuilder InitRequestBuilder(RequestModel requestModel) + private IRequestBuilder InitRequestBuilder(RequestModel requestModel, MappingModel? mappingModel = null) { var requestBuilder = Request.Create(); @@ -225,7 +226,7 @@ private IRequestBuilder InitRequestBuilder(RequestModel requestModel) if (requestModel.Params != null) { - foreach (var paramModel in requestModel.Params.Where(p => p is { Matchers: { } })) + foreach (var paramModel in requestModel.Params.Where(p => p is { Matchers: not null })) { var ignoreCase = paramModel.IgnoreCase == true; requestBuilder = requestBuilder.WithParam(paramModel.Name, ignoreCase, paramModel.Matchers!.Select(_matcherMapper.Map).OfType().ToArray()); @@ -234,7 +235,15 @@ private IRequestBuilder InitRequestBuilder(RequestModel requestModel) if (requestModel.Body?.Matcher != null) { - requestBuilder = requestBuilder.WithBody(_matcherMapper.Map(requestModel.Body.Matcher)!); + var bodyMatcher = _matcherMapper.Map(requestModel.Body.Matcher)!; +#if PROTOBUF + // If the BodyMatcher is a ProtoBufMatcher, and if ProtoDefinition is defined on Mapping-level, set the ProtoDefinition from that Mapping. + if (bodyMatcher is ProtoBufMatcher protoBufMatcher && mappingModel?.ProtoDefinition != null) + { + protoBufMatcher.ProtoDefinition = () => ProtoDefinitionHelper.GetIdOrTexts(_settings, mappingModel.ProtoDefinition); + } +#endif + requestBuilder = requestBuilder.WithBody(bodyMatcher); } else if (requestModel.Body?.Matchers != null) { @@ -317,7 +326,7 @@ private static IResponseBuilder InitResponseBuilder(ResponseModel responseModel) } else if (responseModel.HeadersRaw != null) { - foreach (string headerLine in responseModel.HeadersRaw.Split(["\n", "\r\n"], StringSplitOptions.RemoveEmptyEntries)) + foreach (var headerLine in responseModel.HeadersRaw.Split(["\n", "\r\n"], StringSplitOptions.RemoveEmptyEntries)) { int indexColon = headerLine.IndexOf(":", StringComparison.Ordinal); string key = headerLine.Substring(0, indexColon).TrimStart(' ', '\t'); @@ -364,6 +373,7 @@ private static IResponseBuilder InitResponseBuilder(ResponseModel responseModel) } else { + // ProtoDefinition(s) is/are defined at Mapping/Server level responseBuilder = responseBuilder.WithBodyAsProtoBuf(responseModel.ProtoBufMessageType, responseModel.BodyAsJson); } } diff --git a/src/WireMock.Net/Util/PortUtils.cs b/src/WireMock.Net/Util/PortUtils.cs index 843ce8d7..d5795aa6 100644 --- a/src/WireMock.Net/Util/PortUtils.cs +++ b/src/WireMock.Net/Util/PortUtils.cs @@ -1,6 +1,7 @@ // Copyright © WireMock.Net using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; @@ -36,6 +37,33 @@ public static int FindFreeTcpPort() } } + /// + /// Finds a free TCP ports. + /// + /// see http://stackoverflow.com/questions/138043/find-the-next-tcp-port-in-net. + public static IReadOnlyList FindFreeTcpPorts(int numPorts) + { + var freePorts = new List(); + + 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; + } + /// /// Extract the isHttps, isHttp2, protocol, host and port from a URL. /// diff --git a/src/WireMock.Net/Util/ProtoDefinitionHelper.cs b/src/WireMock.Net/Util/ProtoDefinitionHelper.cs new file mode 100644 index 00000000..91edc51e --- /dev/null +++ b/src/WireMock.Net/Util/ProtoDefinitionHelper.cs @@ -0,0 +1,27 @@ +// Copyright © WireMock.Net + +using WireMock.Models; +using WireMock.Settings; + +namespace WireMock.Util; + +internal static class ProtoDefinitionHelper +{ + internal static IdOrTexts GetIdOrTexts(WireMockServerSettings settings, params string[] protoDefinitionOrId) + { + switch (protoDefinitionOrId.Length) + { + case 1: + var idOrText = protoDefinitionOrId[0]; + if (settings.ProtoDefinitions?.TryGetValue(idOrText, out var protoDefinitions) == true) + { + return new(idOrText, protoDefinitions); + } + + return new(null, protoDefinitionOrId); + + default: + return new(null, protoDefinitionOrId); + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs b/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs index f5820bf8..69d8a4aa 100644 --- a/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs +++ b/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs @@ -17,6 +17,7 @@ using WireMock.ResponseBuilders; using WireMock.Server; using WireMock.Settings; +using WireMock.Util; using Xunit; // ReSharper disable once CheckNamespace @@ -365,7 +366,7 @@ public async Task WireMockServer_WithBodyAsProtoBuf_MappingProtoDefinition_Using var reply = await client.SayHelloAsync(new HelloRequest { Name = "stef" }); // Assert - reply.Message.Should().Be("hello stef POST"); + Then_ReplyMessage_Should_BeCorrect(reply); } [Fact] @@ -406,23 +407,25 @@ public async Task WireMockServer_WithBodyAsProtoBuf_ServerProtoDefinition_UsingG Then_ReplyMessage_Should_BeCorrect(reply); } - [Fact(Skip = "#1233")] + [Fact] public async Task WireMockServer_WithBodyAsProtoBuf_ServerProtoDefinitionFromJson_UsingGrpcGeneratedClient() { var server = Given_When_ServerStartedUsingHttp2(); Given_ProtoDefinition_IsAddedOnServerLevel(server); await Given_When_ProtoBufMappingIsAddedViaAdminInterfaceAsync(server); - var reply = await When_GrpcClient_Calls_SayHelloAsync(server.Url!); + var reply = await When_GrpcClient_Calls_SayHelloAsync(server.Urls[1]); Then_ReplyMessage_Should_BeCorrect(reply); } private static WireMockServer Given_When_ServerStartedUsingHttp2() { + var ports = PortUtils.FindFreeTcpPorts(2); + var settings = new WireMockServerSettings { - UseHttp2 = false, + Urls = [$"http://*:{ports[0]}/", $"grpc://*:{ports[1]}/"], StartAdminInterface = true }; return WireMockServer.Start(settings); @@ -430,7 +433,7 @@ private static WireMockServer Given_When_ServerStartedUsingHttp2() private static void Given_ProtoDefinition_IsAddedOnServerLevel(WireMockServer server) { - server.AddProtoDefinition("my-greeter-351f0240-bba0-4bcb-93c6-1feba0fe0003", ReadProtoFile("greet.proto")); + server.AddProtoDefinition("my-greeter", ReadProtoFile("greet.proto")); } private static async Task Given_When_ProtoBufMappingIsAddedViaAdminInterfaceAsync(WireMockServer server) diff --git a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-1.json b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-1.json index b4063561..b157ac61 100644 --- a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-1.json +++ b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-1.json @@ -6,7 +6,7 @@ "Matchers": [ { "Name": "WildcardMatcher", - "Pattern": "/grpc/greet.Greeter/SayHello", + "Pattern": "/greet.Greeter/SayHello", "IgnoreCase": false } ] diff --git a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-2.json b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-2.json index dec4c05b..41f1e95e 100644 --- a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-2.json +++ b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-2.json @@ -6,7 +6,7 @@ "Matchers": [ { "Name": "WildcardMatcher", - "Pattern": "/grpc2/greet.Greeter/SayHello", + "Pattern": "/greet.Greeter/SayHello", "IgnoreCase": false } ] diff --git a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-3.json b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-3.json index 627f878f..d45a0a9c 100644 --- a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-3.json +++ b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-3.json @@ -6,7 +6,7 @@ "Matchers": [ { "Name": "WildcardMatcher", - "Pattern": "/grpc3/greet.Greeter/SayHello", + "Pattern": "/greet.Greeter/SayHello", "IgnoreCase": false } ] @@ -22,7 +22,7 @@ "Pattern": { "name": "*" }, - "IgnoreCase": false, + "IgnoreCase": true, "Regex": false }, "ProtoBufMessageType": "greet.HelloRequest" @@ -31,7 +31,7 @@ }, "Response": { "BodyAsJson": { - "message": "hello {{request.BodyAsJson.name}}" + "message": "hello {{request.BodyAsJson.name}} {{request.method}}" }, "UseTransformer": true, "TransformerType": "Handlebars", @@ -44,5 +44,5 @@ }, "ProtoBufMessageType": "greet.HelloReply" }, - "ProtoDefinition": "my-greeter-351f0240-bba0-4bcb-93c6-1feba0fe0003" + "ProtoDefinition": "my-greeter" } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-4.json b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-4.json index ed188621..a56d4354 100644 --- a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-4.json +++ b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-4.json @@ -6,7 +6,7 @@ "Matchers": [ { "Name": "WildcardMatcher", - "Pattern": "/grpc4/greet.Greeter/SayHello", + "Pattern": "/greet.Greeter/SayHello", "IgnoreCase": false } ] From cb14de105549a420e7ce3aab228e9e4f83139f31 Mon Sep 17 00:00:00 2001 From: Stef Date: Mon, 13 Jan 2025 19:37:06 +0100 Subject: [PATCH 4/4] PortUtils --- src/WireMock.Net/Util/PortUtils.cs | 83 +++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 19 deletions(-) diff --git a/src/WireMock.Net/Util/PortUtils.cs b/src/WireMock.Net/Util/PortUtils.cs index d5795aa6..f50a106d 100644 --- a/src/WireMock.Net/Util/PortUtils.cs +++ b/src/WireMock.Net/Util/PortUtils.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; @@ -18,52 +19,96 @@ internal static class PortUtils private static readonly Regex UrlDetailsRegex = new(@"^((?\w+)://)(?[^/]+?):(?\d+)\/?$", RegexOptions.Compiled, WireMockConstants.DefaultRegexTimeout); /// - /// Finds a free TCP port. + /// Finds a random, free port to be listened on. /// - /// see http://stackoverflow.com/questions/138043/find-the-next-tcp-port-in-net. + /// A random, free port to be listened on. + /// https://github.com/SeleniumHQ/selenium/blob/trunk/dotnet/src/webdriver/Internal/PortUtilities.cs public static int FindFreeTcpPort() { - TcpListener? tcpListener = null; + // Locate a free port on the local machine by binding a socket to an IPEndPoint using IPAddress.Any and port 0. + // The socket will select a free port. + var portSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { - tcpListener = new TcpListener(IPAddress.Loopback, 0); - tcpListener.Start(); - - return ((IPEndPoint)tcpListener.LocalEndpoint).Port; + var socketEndPoint = new IPEndPoint(IPAddress.Any, 0); + portSocket.Bind(socketEndPoint); + socketEndPoint = (IPEndPoint)portSocket.LocalEndPoint!; + return socketEndPoint.Port; } finally { - tcpListener?.Stop(); +#if !NETSTANDARD1_3 + portSocket.Close(); +#endif + portSocket.Dispose(); } } /// - /// Finds a free TCP ports. + /// Finds a specified number of random, free ports to be listened on. /// - /// see http://stackoverflow.com/questions/138043/find-the-next-tcp-port-in-net. - public static IReadOnlyList FindFreeTcpPorts(int numPorts) + /// The number of free ports to find. + /// A list of random, free ports to be listened on. + public static IReadOnlyList FindFreeTcpPorts(int count) { + var sockets = Enumerable + .Range(0, count) + .Select(_ => new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + .ToArray(); + var freePorts = new List(); - TcpListener? tcpListener = null; try { - for (var i = 0; i < numPorts; i++) + foreach (var socket in sockets) { - tcpListener = new TcpListener(IPAddress.Loopback, 0); - tcpListener.Start(); + var socketEndPoint = new IPEndPoint(IPAddress.Any, 0); + socket.Bind(socketEndPoint); + socketEndPoint = (IPEndPoint)socket.LocalEndPoint!; - freePorts.Add(((IPEndPoint)tcpListener.LocalEndpoint).Port); + freePorts.Add(socketEndPoint.Port); } + + return freePorts; } finally { - tcpListener?.Stop(); + foreach (var socket in sockets) + { +#if !NETSTANDARD1_3 + socket.Close(); +#endif + socket.Dispose(); + } } - - return freePorts; } + ///// + ///// Finds free TCP ports. + ///// + //public static IReadOnlyList FindFreeTcpPorts(int numPorts) + //{ + // var freePorts = new List(); + + // 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; + //} + /// /// Extract the isHttps, isHttp2, protocol, host and port from a URL. ///