diff --git a/src/WireMock.Net/Admin/Mappings/FaultModel.cs b/src/WireMock.Net/Admin/Mappings/FaultModel.cs new file mode 100644 index 00000000..3776d645 --- /dev/null +++ b/src/WireMock.Net/Admin/Mappings/FaultModel.cs @@ -0,0 +1,18 @@ +namespace WireMock.Admin.Mappings +{ + /// + /// Fault Model + /// + public class FaultModel + { + /// + /// Gets or sets the fault. Can be null, "", NONE, EMPTY_RESPONSE, MALFORMED_RESPONSE_CHUNK or RANDOM_DATA_THEN_CLOSE. + /// + public string Type { get; set; } + + /// + /// Gets or sets the fault percentage. + /// + public double? Percentage { get; set; } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Admin/Mappings/ResponseModel.cs b/src/WireMock.Net/Admin/Mappings/ResponseModel.cs index 29c71b44..d7725263 100644 --- a/src/WireMock.Net/Admin/Mappings/ResponseModel.cs +++ b/src/WireMock.Net/Admin/Mappings/ResponseModel.cs @@ -86,5 +86,10 @@ namespace WireMock.Admin.Mappings /// The client X509Certificate2 Thumbprint or SubjectName to use. /// public string X509Certificate2ThumbprintOrSubjectName { get; set; } + + /// + /// Gets or sets the fault. + /// + public FaultModel Fault { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Admin/Requests/LogResponseModel.cs b/src/WireMock.Net/Admin/Requests/LogResponseModel.cs index b7b51a76..ccd96ca1 100644 --- a/src/WireMock.Net/Admin/Requests/LogResponseModel.cs +++ b/src/WireMock.Net/Admin/Requests/LogResponseModel.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using WireMock.Admin.Mappings; +using WireMock.ResponseBuilders; using WireMock.Util; namespace WireMock.Admin.Requests @@ -68,5 +69,15 @@ namespace WireMock.Admin.Requests /// The detected body type (detection based on Content-Type). /// public BodyType DetectedBodyTypeFromContentType { get; set; } + + /// + /// The FaultType. + /// + public string FaultType { get; set; } + + /// + /// Gets or sets the Fault percentage. + /// + public double? FaultPercentage { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Owin/Mappers/OwinResponseMapper.cs b/src/WireMock.Net/Owin/Mappers/OwinResponseMapper.cs index 9d9b3c93..800a3e5f 100644 --- a/src/WireMock.Net/Owin/Mappers/OwinResponseMapper.cs +++ b/src/WireMock.Net/Owin/Mappers/OwinResponseMapper.cs @@ -4,8 +4,11 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; +using RandomDataGenerator.FieldOptions; +using RandomDataGenerator.Randomizers; using WireMock.Handlers; using WireMock.Http; +using WireMock.ResponseBuilders; using WireMock.Util; using WireMock.Validation; #if !USE_ASPNETCORE @@ -22,6 +25,8 @@ namespace WireMock.Owin.Mappers /// public class OwinResponseMapper : IOwinResponseMapper { + private readonly IRandomizerNumber _randomizerDouble = RandomizerFactory.GetRandomizer(new FieldOptionsDouble { Min = 0, Max = 1 }); + private readonly IRandomizerBytes _randomizerBytes = RandomizerFactory.GetRandomizer(new FieldOptionsBytes { Min = 100, Max = 200 }); private readonly IFileSystemHandler _fileSystemHandler; private readonly Encoding _utf8NoBom = new UTF8Encoding(false); @@ -53,8 +58,43 @@ namespace WireMock.Owin.Mappers return; } - response.StatusCode = responseMessage.StatusCode; + byte[] bytes; + switch (responseMessage.FaultType) + { + case FaultType.EMPTY_RESPONSE: + bytes = IsFault(responseMessage) ? new byte[0] : GetNormalBody(responseMessage); + break; + case FaultType.MALFORMED_RESPONSE_CHUNK: + bytes = GetNormalBody(responseMessage) ?? new byte[0]; + if (IsFault(responseMessage)) + { + bytes = bytes.Take(bytes.Length / 2).Union(_randomizerBytes.Generate()).ToArray(); + } + + break; + + default: + bytes = GetNormalBody(responseMessage); + break; + } + + response.StatusCode = responseMessage.StatusCode; + SetResponseHeaders(responseMessage, response); + + if (bytes != null) + { + await response.Body.WriteAsync(bytes, 0, bytes.Length); + } + } + + private bool IsFault(ResponseMessage responseMessage) + { + return responseMessage.FaultPercentage == null || _randomizerDouble.Generate() <= responseMessage.FaultPercentage; + } + + private byte[] GetNormalBody(ResponseMessage responseMessage) + { byte[] bytes = null; switch (responseMessage.BodyData?.DetectedBodyType) { @@ -63,7 +103,9 @@ namespace WireMock.Owin.Mappers break; case BodyType.Json: - Formatting formatting = responseMessage.BodyData.BodyAsJsonIndented == true ? Formatting.Indented : Formatting.None; + Formatting formatting = responseMessage.BodyData.BodyAsJsonIndented == true + ? Formatting.Indented + : Formatting.None; string jsonBody = JsonConvert.SerializeObject(responseMessage.BodyData.BodyAsJson, new JsonSerializerSettings { Formatting = formatting, NullValueHandling = NullValueHandling.Ignore }); bytes = (responseMessage.BodyData.Encoding ?? _utf8NoBom).GetBytes(jsonBody); break; @@ -77,12 +119,7 @@ namespace WireMock.Owin.Mappers break; } - SetResponseHeaders(responseMessage, response); - - if (bytes != null) - { - await response.Body.WriteAsync(bytes, 0, bytes.Length); - } + return bytes; } private void SetResponseHeaders(ResponseMessage responseMessage, IResponse response) diff --git a/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs index 06469fd1..464d62f7 100644 --- a/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs +++ b/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs @@ -1,13 +1,14 @@ using JetBrains.Annotations; using System; using WireMock.Matchers; +using WireMock.Matchers.Request; namespace WireMock.RequestBuilders { /// /// The BodyRequestBuilder interface. /// - public interface IBodyRequestBuilder + public interface IBodyRequestBuilder : IRequestMatcher { /// /// WithBody: IMatcher diff --git a/src/WireMock.Net/RequestBuilders/IHeadersAndCookiesRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IHeadersAndCookiesRequestBuilder.cs index 254938d9..d1e77ea0 100644 --- a/src/WireMock.Net/RequestBuilders/IHeadersAndCookiesRequestBuilder.cs +++ b/src/WireMock.Net/RequestBuilders/IHeadersAndCookiesRequestBuilder.cs @@ -1,15 +1,14 @@ -using System; +using JetBrains.Annotations; +using System; using System.Collections.Generic; -using JetBrains.Annotations; using WireMock.Matchers; -using WireMock.Matchers.Request; namespace WireMock.RequestBuilders { /// /// The HeadersAndCookieRequestBuilder interface. /// - public interface IHeadersAndCookiesRequestBuilder : IBodyRequestBuilder, IRequestMatcher, IParamsRequestBuilder + public interface IHeadersAndCookiesRequestBuilder : IParamsRequestBuilder { /// /// WithHeader: matching based on name, pattern and matchBehaviour. diff --git a/src/WireMock.Net/RequestBuilders/IParamsRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IParamsRequestBuilder.cs index 06bb0a3b..381fec3b 100644 --- a/src/WireMock.Net/RequestBuilders/IParamsRequestBuilder.cs +++ b/src/WireMock.Net/RequestBuilders/IParamsRequestBuilder.cs @@ -9,7 +9,7 @@ namespace WireMock.RequestBuilders /// /// The ParamsRequestBuilder interface. /// - public interface IParamsRequestBuilder + public interface IParamsRequestBuilder : IBodyRequestBuilder { /// /// WithParam: matching on key only. diff --git a/src/WireMock.Net/ResponseBuilders/FaultType.cs b/src/WireMock.Net/ResponseBuilders/FaultType.cs new file mode 100644 index 00000000..5c922868 --- /dev/null +++ b/src/WireMock.Net/ResponseBuilders/FaultType.cs @@ -0,0 +1,23 @@ +namespace WireMock.ResponseBuilders +{ + /// + /// The FaultType enumeration + /// + public enum FaultType + { + /// + /// No Fault + /// + NONE, + + /// + /// Return a completely empty response. + /// + EMPTY_RESPONSE, + + /// + /// Send a defined status header, then garbage, then close the connection. + /// + MALFORMED_RESPONSE_CHUNK + } +} \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/IBodyResponseBuilder.cs b/src/WireMock.Net/ResponseBuilders/IBodyResponseBuilder.cs index 0ebd1c25..b61c4bb1 100644 --- a/src/WireMock.Net/ResponseBuilders/IBodyResponseBuilder.cs +++ b/src/WireMock.Net/ResponseBuilders/IBodyResponseBuilder.cs @@ -7,7 +7,7 @@ namespace WireMock.ResponseBuilders /// /// The BodyResponseBuilder interface. /// - public interface IBodyResponseBuilder : ITransformResponseBuilder + public interface IBodyResponseBuilder : IFaultResponseBuilder { /// /// WithBody : Create a ... response based on a string. diff --git a/src/WireMock.Net/ResponseBuilders/IFaultRequestBuilder.cs b/src/WireMock.Net/ResponseBuilders/IFaultRequestBuilder.cs new file mode 100644 index 00000000..954d69fe --- /dev/null +++ b/src/WireMock.Net/ResponseBuilders/IFaultRequestBuilder.cs @@ -0,0 +1,18 @@ +using JetBrains.Annotations; + +namespace WireMock.ResponseBuilders +{ + /// + /// The FaultRequestBuilder interface. + /// + public interface IFaultResponseBuilder : ITransformResponseBuilder + { + /// + /// WithBody : Create a fault response. + /// + /// The FaultType. + /// The percentage when this fault should occur. When null, it's always. + /// A . + IResponseBuilder WithFault(FaultType faultType, [CanBeNull] double? percentage = null); + } +} \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/Response.WithFault.cs b/src/WireMock.Net/ResponseBuilders/Response.WithFault.cs new file mode 100644 index 00000000..b8e511ec --- /dev/null +++ b/src/WireMock.Net/ResponseBuilders/Response.WithFault.cs @@ -0,0 +1,14 @@ +namespace WireMock.ResponseBuilders +{ + public partial class Response + { + /// + public IResponseBuilder WithFault(FaultType faultType, double? percentage = null) + { + ResponseMessage.FaultType = faultType; + ResponseMessage.FaultPercentage = percentage; + + return this; + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index 4f09f361..7b703bf1 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -19,7 +19,7 @@ namespace WireMock.ResponseBuilders /// /// The Response. /// - public class Response : IResponseBuilder + public partial class Response : IResponseBuilder { private HttpClient _httpClientForProxy; @@ -375,6 +375,7 @@ namespace WireMock.ResponseBuilders public async Task ProvideResponseAsync(RequestMessage requestMessage, IFluentMockServerSettings settings) { Check.NotNull(requestMessage, nameof(requestMessage)); + Check.NotNull(settings, nameof(settings)); if (Delay != null) { diff --git a/src/WireMock.Net/ResponseMessage.cs b/src/WireMock.Net/ResponseMessage.cs index 1d5c49c3..a089aa3f 100644 --- a/src/WireMock.Net/ResponseMessage.cs +++ b/src/WireMock.Net/ResponseMessage.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using WireMock.ResponseBuilders; using WireMock.Util; using WireMock.Validation; @@ -35,6 +36,16 @@ namespace WireMock /// public BodyData BodyData { get; set; } + /// + /// The FaultType. + /// + public FaultType FaultType { get; set; } + + /// + /// Gets or sets the Fault percentage. + /// + public double? FaultPercentage { get; set; } + /// /// Adds the header. /// diff --git a/src/WireMock.Net/Serialization/LogEntryMapper.cs b/src/WireMock.Net/Serialization/LogEntryMapper.cs index cda15bab..5fceb246 100644 --- a/src/WireMock.Net/Serialization/LogEntryMapper.cs +++ b/src/WireMock.Net/Serialization/LogEntryMapper.cs @@ -2,6 +2,7 @@ using WireMock.Admin.Mappings; using WireMock.Admin.Requests; using WireMock.Logging; +using WireMock.ResponseBuilders; using WireMock.Util; namespace WireMock.Serialization @@ -61,6 +62,12 @@ namespace WireMock.Serialization Headers = logEntry.ResponseMessage.Headers }; + if (logEntry.ResponseMessage.FaultType != FaultType.NONE) + { + logResponseModel.FaultType = logEntry.ResponseMessage.FaultType.ToString(); + logResponseModel.FaultPercentage = logEntry.ResponseMessage.FaultPercentage; + } + if (logEntry.ResponseMessage.BodyData != null) { logResponseModel.BodyOriginal = logEntry.ResponseMessage.BodyOriginal; diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs index ff302574..ae6ee354 100644 --- a/src/WireMock.Net/Serialization/MappingConverter.cs +++ b/src/WireMock.Net/Serialization/MappingConverter.cs @@ -114,6 +114,7 @@ namespace WireMock.Serialization mappingModel.Response.UseTransformer = null; mappingModel.Response.BodyEncoding = null; mappingModel.Response.ProxyUrl = response.ProxyUrl; + mappingModel.Response.Fault = null; } else { @@ -161,6 +162,15 @@ namespace WireMock.Serialization }; } } + + if (response.ResponseMessage.FaultType != FaultType.NONE) + { + mappingModel.Response.Fault = new FaultModel + { + Type = response.ResponseMessage.FaultType.ToString(), + Percentage = response.ResponseMessage.FaultPercentage + }; + } } return mappingModel; diff --git a/src/WireMock.Net/Server/FluentMockServer.Admin.cs b/src/WireMock.Net/Server/FluentMockServer.Admin.cs index 78ef8795..36c55ab7 100644 --- a/src/WireMock.Net/Server/FluentMockServer.Admin.cs +++ b/src/WireMock.Net/Server/FluentMockServer.Admin.cs @@ -820,6 +820,11 @@ namespace WireMock.Server responseBuilder = responseBuilder.WithBodyFromFile(responseModel.BodyAsFile); } + if (responseModel.Fault != null && Enum.TryParse(responseModel.Fault.Type, out FaultType faultType)) + { + responseBuilder.WithFault(faultType, responseModel.Fault.Percentage); + } + return responseBuilder; } diff --git a/src/WireMock.Net/Transformers/ResponseMessageTransformer.cs b/src/WireMock.Net/Transformers/ResponseMessageTransformer.cs index a87f5845..a755acff 100644 --- a/src/WireMock.Net/Transformers/ResponseMessageTransformer.cs +++ b/src/WireMock.Net/Transformers/ResponseMessageTransformer.cs @@ -45,6 +45,9 @@ namespace WireMock.Transformers break; } + responseMessage.FaultType = original.FaultType; + responseMessage.FaultPercentage = original.FaultPercentage; + // Headers var newHeaders = new Dictionary>(); foreach (var header in original.Headers) diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index dd434df4..4751e888 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -57,11 +57,16 @@ + + + + + All @@ -75,7 +80,7 @@ runtime; build; native; contentfiles; analyzers - + diff --git a/test/WireMock.Net.Tests/Owin/Mappers/OwinResponseMapperTests.cs b/test/WireMock.Net.Tests/Owin/Mappers/OwinResponseMapperTests.cs index 696999f6..5c5f8ef7 100644 --- a/test/WireMock.Net.Tests/Owin/Mappers/OwinResponseMapperTests.cs +++ b/test/WireMock.Net.Tests/Owin/Mappers/OwinResponseMapperTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using System.Threading; using WireMock.Handlers; using WireMock.Owin.Mappers; +using WireMock.ResponseBuilders; using WireMock.Util; #if NET452 using Microsoft.Owin; @@ -97,7 +98,7 @@ namespace WireMock.Net.Tests.Owin.Mappers public async Task OwinResponseMapper_MapAsync_Body() { // Arrange - string body = "abc"; + string body = "abcd"; var responseMessage = new ResponseMessage { Headers = new Dictionary>(), @@ -108,7 +109,7 @@ namespace WireMock.Net.Tests.Owin.Mappers await _sut.MapAsync(responseMessage, _responseMock.Object); // Assert - _stream.Verify(s => s.WriteAsync(new byte[] { 97, 98, 99 }, 0, 3, It.IsAny()), Times.Once); + _stream.Verify(s => s.WriteAsync(new byte[] { 97, 98, 99, 100 }, 0, 4, It.IsAny()), Times.Once); } [Fact] @@ -167,5 +168,47 @@ namespace WireMock.Net.Tests.Owin.Mappers _headers.Verify(h => h.TryGetValue("h", out v), Times.Once); #endif } + + [Fact] + public async Task OwinResponseMapper_MapAsync_WithFault_EMPTY_RESPONSE() + { + // Arrange + string body = "abc"; + var responseMessage = new ResponseMessage + { + Headers = new Dictionary>(), + BodyData = new BodyData { DetectedBodyType = BodyType.String, BodyAsString = body }, + FaultType = FaultType.EMPTY_RESPONSE + }; + + // Act + await _sut.MapAsync(responseMessage, _responseMock.Object); + + // Assert + _stream.Verify(s => s.WriteAsync(new byte[0], 0, 0, It.IsAny()), Times.Once); + } + + [Theory] + [InlineData("abcd", BodyType.String)] + [InlineData("", BodyType.String)] + [InlineData(null, BodyType.None)] + public async Task OwinResponseMapper_MapAsync_WithFault_MALFORMED_RESPONSE_CHUNK(string body, BodyType detected) + { + // Arrange + var responseMessage = new ResponseMessage + { + Headers = new Dictionary>(), + BodyData = new BodyData { DetectedBodyType = detected, BodyAsString = body }, + StatusCode = 100, + FaultType = FaultType.MALFORMED_RESPONSE_CHUNK + }; + + // Act + await _sut.MapAsync(responseMessage, _responseMock.Object); + + // Assert + _responseMock.VerifySet(r => r.StatusCode = 100, Times.Once); + _stream.Verify(s => s.WriteAsync(It.IsAny(), 0, It.Is(count => count >= 0), It.IsAny()), Times.Once); + } } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithWithFaultTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithWithFaultTests.cs new file mode 100644 index 00000000..8dbc9415 --- /dev/null +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithWithFaultTests.cs @@ -0,0 +1,49 @@ +using FluentAssertions; +using Moq; +using System.Threading.Tasks; +using WireMock.Models; +using WireMock.ResponseBuilders; +using WireMock.Settings; +using Xunit; + +namespace WireMock.Net.Tests.ResponseBuilders +{ + public class ResponseWithWithFaultTests + { + private readonly Mock _settingsMock = new Mock(); + private const string ClientIp = "::1"; + + [Theory] + [InlineData(FaultType.EMPTY_RESPONSE)] + [InlineData(FaultType.MALFORMED_RESPONSE_CHUNK)] + public async Task Response_ProvideResponse_WithFault(FaultType faultType) + { + // Arrange + var request = new RequestMessage(new UrlDetails("http://localhost/fault"), "GET", ClientIp); + + // Act + var response = Response.Create().WithFault(faultType); + var responseMessage = await response.ProvideResponseAsync(request, _settingsMock.Object); + + // Assert + responseMessage.FaultType.Should().Be(faultType); + responseMessage.FaultPercentage.Should().BeNull(); + } + + [Theory] + [InlineData(FaultType.EMPTY_RESPONSE, 0.5)] + public async Task Response_ProvideResponse_WithFault_IncludingPercentage(FaultType faultType, double percentage) + { + // Arrange + var request = new RequestMessage(new UrlDetails("http://localhost/fault"), "GET", ClientIp); + + // Act + var response = Response.Create().WithFault(faultType, percentage); + var responseMessage = await response.ProvideResponseAsync(request, _settingsMock.Object); + + // Assert + responseMessage.FaultType.Should().Be(faultType); + responseMessage.FaultPercentage.Should().Be(percentage); + } + } +} diff --git a/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.cs b/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.cs index 3552cb99..6838e40f 100644 --- a/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.cs +++ b/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.cs @@ -1,6 +1,8 @@ -using NFluent; +using FluentAssertions; +using NFluent; using WireMock.Logging; using WireMock.Models; +using WireMock.ResponseBuilders; using WireMock.Serialization; using WireMock.Util; using Xunit; @@ -59,8 +61,7 @@ namespace WireMock.Net.Tests.Serialization // Assign var logEntry = new LogEntry { - RequestMessage = new RequestMessage(new UrlDetails("http://localhost"), "get", "::1" - ), + RequestMessage = new RequestMessage(new UrlDetails("http://localhost"), "get", "::1"), ResponseMessage = new ResponseMessage { BodyData = new BodyData @@ -88,5 +89,32 @@ namespace WireMock.Net.Tests.Serialization Check.That(result.Response.BodyAsJson).IsNull(); Check.That(result.Response.BodyAsFile).IsEqualTo("test"); } + + [Fact] + public void LogEntryMapper_Map_LogEntry_WithFault() + { + // Assign + var logEntry = new LogEntry + { + RequestMessage = new RequestMessage(new UrlDetails("http://localhost"), "get", "::1"), + ResponseMessage = new ResponseMessage + { + BodyData = new BodyData + { + DetectedBodyType = BodyType.File, + BodyAsFile = "test" + }, + FaultType = FaultType.EMPTY_RESPONSE, + FaultPercentage = 0.5 + } + }; + + // Act + var result = LogEntryMapper.Map(logEntry); + + // Assert + result.Response.FaultType.Should().Be("EMPTY_RESPONSE"); + result.Response.FaultPercentage.Should().Be(0.5); + } } } \ 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 35bc98f5..6e19cf80 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -41,7 +41,7 @@ - +