Compare commits

...

16 Commits

Author SHA1 Message Date
Stef Heyenrath
f3d52adbb2 1.5.22 2023-04-08 21:37:18 +02:00
Stef Heyenrath
a8775c3b77 Include WireMockOpenApiParser project (#916)
* Fix some nullability warnings for WireMockOpenApiParser

* .

* .

* .

* opt

* FromText

* ab

* .

* private const string AdminOpenApi = "/__admin/openapi";

* fix test

* rnd

* .

* urldetails

* 0

* ,

* .

* tests

* .

* CompressionUtilsTests

* ut

* .
2023-04-08 21:25:17 +02:00
Stef Heyenrath
3e24e3452b Add Blogs 2023-04-01 11:21:16 +02:00
Walid Haidari
95bf8e31aa #912 add excluded params to proxy mapping (#914) 2023-03-24 16:35:09 +01:00
Stef Heyenrath
090989ea7f Update comments for models (#913) 2023-03-24 09:20:23 +01:00
Stef Heyenrath
651486f718 Make some classes internal + chnage some files to file-scoped namespaces 2023-03-22 21:57:23 +01:00
Stef Heyenrath
9dea577da1 1.5.21 2023-03-22 16:18:19 +01:00
Stef Heyenrath
7ca4294de6 Fixed QueryStringParser for UrlEncoded values (#911) 2023-03-22 16:15:50 +01:00
Stef Heyenrath
66245409f9 RequestBuilder : add WithBodyAsJson and WithBody (with IJsonConverter) (#908)
* RequestBuilder : add WithBodyAsJson and WithBody (with IJsonConverter)

* tests
2023-03-22 15:47:58 +01:00
Stef Heyenrath
da6cb9fe0a 1.5.20 2023-03-19 10:23:24 +01:00
Stef Heyenrath
b30e4faab6 Fix issue with application/x-www-form-urlencoded and ExactMatcher (#907) 2023-03-19 10:21:47 +01:00
Stef Heyenrath
1221d52c69 Add DeserializeFormUrl Encoded to the settings (#905)
* Add DeserializeFormUrlEncoded to Settings

* EmptyArray<>.Value

* .
2023-03-19 09:19:53 +01:00
Stef Heyenrath
52d2109c7e packagereleasenotes 2023-03-17 17:16:35 +01:00
Stef Heyenrath
30064b922b 1.5.19 2023-03-17 17:15:19 +01:00
Stef Heyenrath
78b94d2ebc Add WithBody with IDictionary (form-urlencoded values) (#903)
* .

* x

* fx

* fix

* f

* tests

* fix tests

* add tst
2023-03-17 17:08:45 +01:00
Stef Heyenrath
19701f5260 Update Handlebars.Net.Helpers to 2.3.15 (#904) 2023-03-15 18:30:34 +01:00
90 changed files with 3461 additions and 1143 deletions

View File

@@ -1,3 +1,22 @@
# 1.5.22 (08 April 2023)
- [#914](https://github.com/WireMock-Net/WireMock.Net/pull/914) - #912 add excluded params to proxy mapping [feature] contributed by [walidhaidarii](https://github.com/walidhaidarii)
- [#916](https://github.com/WireMock-Net/WireMock.Net/pull/916) - Include WireMockOpenApiParser project [feature] contributed by [StefH](https://github.com/StefH)
- [#912](https://github.com/WireMock-Net/WireMock.Net/issues/912) - Feature: adding excluded params to proxy and records settings [feature]
# 1.5.21 (22 March 2023)
- [#908](https://github.com/WireMock-Net/WireMock.Net/pull/908) - RequestBuilder : add WithBodyAsJson and WithBody (with IJsonConverter) [feature] contributed by [StefH](https://github.com/StefH)
- [#911](https://github.com/WireMock-Net/WireMock.Net/pull/911) - Fixed QueryStringParser for UrlEncoded values [bug] contributed by [StefH](https://github.com/StefH)
- [#901](https://github.com/WireMock-Net/WireMock.Net/issues/901) - Matching one form-urlencoded value [feature]
# 1.5.20 (19 March 2023)
- [#905](https://github.com/WireMock-Net/WireMock.Net/pull/905) - Add DeserializeFormUrl Encoded to the settings [feature] contributed by [StefH](https://github.com/StefH)
- [#907](https://github.com/WireMock-Net/WireMock.Net/pull/907) - Fix issue with application/x-www-form-urlencoded and ExactMatcher [bug] contributed by [StefH](https://github.com/StefH)
- [#906](https://github.com/WireMock-Net/WireMock.Net/issues/906) - Upgrade to 1.5.19 breaks a form data test [bug]
# 1.5.19 (17 March 2023)
- [#903](https://github.com/WireMock-Net/WireMock.Net/pull/903) - Add WithBody with IDictionary (form-urlencoded values) [feature] contributed by [StefH](https://github.com/StefH)
- [#904](https://github.com/WireMock-Net/WireMock.Net/pull/904) - Update Handlebars.Net.Helpers to 2.3.15 [feature] contributed by [StefH](https://github.com/StefH)
# 1.5.18 (09 March 2023)
- [#893](https://github.com/WireMock-Net/WireMock.Net/pull/893) - Add 'Data' to response which can be used during transforming the response [feature] contributed by [StefH](https://github.com/StefH)
- [#896](https://github.com/WireMock-Net/WireMock.Net/pull/896) - Bump Microsoft.Owin from 2.0.2 to 4.2.2 in /examples/WireMock.Net.Service [dependencies] contributed by [dependabot[bot]](https://github.com/apps/dependabot)

View File

@@ -4,7 +4,7 @@
</PropertyGroup>
<PropertyGroup>
<VersionPrefix>1.5.18</VersionPrefix>
<VersionPrefix>1.5.22</VersionPrefix>
<PackageIcon>WireMock.Net-Logo.png</PackageIcon>
<PackageProjectUrl>https://github.com/WireMock-Net/WireMock.Net</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>

View File

@@ -1,6 +1,6 @@
rem https://github.com/StefH/GitHubReleaseNotes
SET version=1.5.18
SET version=1.5.22
GitHubReleaseNotes --output CHANGELOG.md --skip-empty-releases --exclude-labels question invalid doc duplicate --version %version% --token %GH_TOKEN%

View File

@@ -1,8 +1,6 @@
# 1.5.18 (09 March 2023)
- #893 Add 'Data' to response which can be used during transforming the response [feature]
- #896 Bump Microsoft.Owin from 2.0.2 to 4.2.2 in /examples/WireMock.Net.Service [dependencies]
- #900 ProxySettings : Add logic to not save some requests depending on HttpMethods [feature]
- #897 WebHostBuilder.ConfigureServices method not found when using nunit3testadapter 4.4.0 [bug]
- #899 Ignore OPTIONS request when using proxyandrecord [feature]
# 1.5.22 (08 April 2023)
- #914 #912 add excluded params to proxy mapping [feature]
- #916 Include WireMockOpenApiParser project [feature]
- #912 Feature: adding excluded params to proxy and records settings [feature]
The full release notes can be found here: https://github.com/WireMock-Net/WireMock.Net/blob/master/CHANGELOG.md

View File

@@ -14,7 +14,11 @@ For more info, see also this WIKI page: [What is WireMock.Net](https://github.co
* Response templating / transformation using Handlebars and extensions
* Can be used locally or in CI/CD scenarios
## Info
## Blogs
- [mStack.nl : Generate C# Code from Mapping(s)](https://mstack.nl/blog/20230201-wiremock.net-tocode/)
## Project Info
| | |
| --- | --- |
| ***Project*** | &nbsp; |

View File

@@ -27,6 +27,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Guids/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Heyenrath/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Jmes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=openapi/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pacticipant/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Raml/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=randomizer/@EntryIndexedValue">True</s:Boolean>

View File

@@ -11,7 +11,7 @@ public class DynamicDataGeneration : WireMockOpenApiParserDynamicExampleValues
get
{
// Since you have your Schema, you can get if max-length is set. You can generate accurate examples with this settings
var maxLength = Schema.MaxLength ?? 9;
var maxLength = Schema?.MaxLength ?? 9;
return RandomizerFactory.GetRandomizer(new FieldOptionsTextRegex
{

View File

@@ -17,7 +17,7 @@ class Program
private static void RunMockServerWithDynamicExampleGeneration()
{
//Run your mocking framework specifieing youur Example Values generator class.
// Run your mocking framework specifying your Example Values generator class.
var serverCustomer_V2_json = Run.RunServer(Path.Combine(Folder, "Swagger_Customer_V2.0.json"), "http://localhost:8090/", true, new DynamicDataGeneration(), Types.ExampleValueType.Value, Types.ExampleValueType.Value);
Console.WriteLine("Press any key to stop the servers");
@@ -27,15 +27,15 @@ class Program
private static void RunOthersOpenApiParserExample()
{
var serverOpenAPIExamples = Run.RunServer(Path.Combine(Folder, "openAPIExamples.yaml"), "https://localhost:9091/");
var serverPetstore_V2_json = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V2.0.json"), "https://localhost:9092/");
var serverPetstore_V2_yaml = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V2.0.yaml"), "https://localhost:9093/");
var serverPetstore_V300_yaml = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V3.0.0.yaml"), "https://localhost:9094/");
var serverPetstore_V302_json = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V3.0.2.json"), "https://localhost:9095/");
var testopenapifile_json = Run.RunServer(Path.Combine(Folder, "testopenapifile.json"), "https://localhost:9096/");
var file_errorYaml = Run.RunServer(Path.Combine(Folder, "file_error.yaml"), "https://localhost:9097/");
var file_petJson = Run.RunServer(Path.Combine(Folder, "pet.json"), "https://localhost:9098/");
var refsYaml = Run.RunServer(Path.Combine(Folder, "refs.yaml"), "https://localhost:9099/");
var serverOpenAPIExamples = Run.RunServer(Path.Combine(Folder, "openAPIExamples.yaml"), "http://localhost:9091/");
var serverPetstore_V2_json = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V2.0.json"), "http://localhost:9092/");
var serverPetstore_V2_yaml = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V2.0.yaml"), "http://localhost:9093/");
var serverPetstore_V300_yaml = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V3.0.0.yaml"), "http://localhost:9094/");
var serverPetstore_V302_json = Run.RunServer(Path.Combine(Folder, "Swagger_Petstore_V3.0.2.json"), "http://localhost:9095/");
var testopenapifile_json = Run.RunServer(Path.Combine(Folder, "testopenapifile.json"), "http://localhost:9096/");
var file_errorYaml = Run.RunServer(Path.Combine(Folder, "file_error.yaml"), "http://localhost:9097/");
var file_petJson = Run.RunServer(Path.Combine(Folder, "pet.json"), "http://localhost:9098/");
var refsYaml = Run.RunServer(Path.Combine(Folder, "refs.yaml"), "http://localhost:9099/");
testopenapifile_json
.Given(Request.Create().WithPath("/x").UsingGet())

View File

@@ -9,64 +9,70 @@ using WireMock.Net.OpenApiParser.Types;
using WireMock.Server;
using WireMock.Settings;
namespace WireMock.Net.OpenApiParser.ConsoleApp
namespace WireMock.Net.OpenApiParser.ConsoleApp;
public static class Run
{
public static class Run
public static WireMockServer RunServer(
string path,
string url,
bool dynamicExamples = true,
IWireMockOpenApiParserExampleValues? examplesValuesGenerator = null,
ExampleValueType pathPatternToUse = ExampleValueType.Wildcard,
ExampleValueType headerPatternToUse = ExampleValueType.Wildcard
)
{
public static WireMockServer RunServer(string path, string url, bool dynamicExamples = true, IWireMockOpenApiParserExampleValues examplesValuesGenerator = null, ExampleValueType pathPatternToUse = ExampleValueType.Wildcard, ExampleValueType headerPatternToUse = ExampleValueType.Wildcard)
var server = WireMockServer.Start(new WireMockServerSettings
{
var server = WireMockServer.Start(new WireMockServerSettings
{
AllowCSharpCodeMatcher = true,
Urls = new[] { url },
StartAdminInterface = true,
ReadStaticMappings = true,
WatchStaticMappings = false,
WatchStaticMappingsInSubdirectories = false,
Logger = new WireMockConsoleLogger(),
SaveUnmatchedRequests = true
});
AllowCSharpCodeMatcher = true,
Urls = new[] { url },
StartAdminInterface = true,
ReadStaticMappings = true,
WatchStaticMappings = false,
WatchStaticMappingsInSubdirectories = false,
Logger = new WireMockConsoleLogger(),
SaveUnmatchedRequests = true
});
Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls));
Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls));
//server.SetBasicAuthentication("a", "b");
//server.SetBasicAuthentication("a", "b");
var settings = new WireMockOpenApiParserSettings
{
DynamicExamples = dynamicExamples,
ExampleValues = examplesValuesGenerator,
PathPatternToUse = pathPatternToUse,
HeaderPatternToUse = headerPatternToUse,
};
server.WithMappingFromOpenApiFile(path, settings, out var diag);
return server;
}
public static void RunServer(IEnumerable<MappingModel> mappings)
var settings = new WireMockOpenApiParserSettings
{
string url1 = "http://localhost:9091/";
DynamicExamples = dynamicExamples,
ExampleValues = examplesValuesGenerator,
PathPatternToUse = pathPatternToUse,
HeaderPatternToUse = headerPatternToUse,
};
var server = WireMockServer.Start(new WireMockServerSettings
{
AllowCSharpCodeMatcher = true,
Urls = new[] { url1 },
StartAdminInterface = true,
ReadStaticMappings = false,
WatchStaticMappings = false,
WatchStaticMappingsInSubdirectories = false,
Logger = new WireMockConsoleLogger(),
});
Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls));
server.WithMappingFromOpenApiFile(path, settings, out var diag);
server.SetBasicAuthentication("a", "b");
return server;
}
server.WithMapping(mappings.ToArray());
public static void RunServer(IEnumerable<MappingModel> mappings)
{
string url1 = "http://localhost:9091/";
Console.WriteLine("Press any key to stop the server");
System.Console.ReadKey();
server.Stop();
}
var server = WireMockServer.Start(new WireMockServerSettings
{
AllowCSharpCodeMatcher = true,
Urls = new[] { url1 },
StartAdminInterface = true,
ReadStaticMappings = false,
WatchStaticMappings = false,
WatchStaticMappingsInSubdirectories = false,
Logger = new WireMockConsoleLogger(),
});
Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls));
server.SetBasicAuthentication("a", "b");
server.WithMapping(mappings.ToArray());
Console.WriteLine("Press any key to stop the server");
System.Console.ReadKey();
server.Stop();
}
}

View File

@@ -24,8 +24,8 @@ static class Program
static async Task Main(string[] args)
{
await TestAsync().ConfigureAwait(false);
return;
//await TestAsync().ConfigureAwait(false);
//return;
XmlConfigurator.Configure(LogRepository, new FileInfo("log4net.config"));

View File

@@ -1,44 +1,43 @@
using System;
using System;
using log4net;
using Newtonsoft.Json;
using WireMock.Admin.Requests;
using WireMock.Logging;
namespace WireMock.Net.StandAlone.NETCoreApp
namespace WireMock.Net.StandAlone.NETCoreApp;
internal class WireMockLog4NetLogger : IWireMockLogger
{
internal class WireMockLog4NetLogger : IWireMockLogger
private static readonly ILog Log = LogManager.GetLogger(typeof(Program));
public void Debug(string formatString, params object[] args)
{
private static readonly ILog Log = LogManager.GetLogger(typeof(Program));
Log.DebugFormat(formatString, args);
}
public void Debug(string formatString, params object[] args)
{
Log.DebugFormat(formatString, args);
}
public void Info(string formatString, params object[] args)
{
Log.InfoFormat(formatString, args);
}
public void Info(string formatString, params object[] args)
{
Log.InfoFormat(formatString, args);
}
public void Warn(string formatString, params object[] args)
{
Log.WarnFormat(formatString, args);
}
public void Warn(string formatString, params object[] args)
{
Log.WarnFormat(formatString, args);
}
public void Error(string formatString, params object[] args)
{
Log.ErrorFormat(formatString, args);
}
public void Error(string formatString, params object[] args)
{
Log.ErrorFormat(formatString, args);
}
public void Error(string message, Exception exception)
{
Log.Error(message, exception);
}
public void Error(string message, Exception exception)
{
Log.Error(message, exception);
}
public void DebugRequestResponse(LogEntryModel logEntryModel, bool isAdminRequest)
{
string message = JsonConvert.SerializeObject(logEntryModel, Formatting.Indented);
Log.DebugFormat("Admin[{0}] {1}", isAdminRequest, message);
}
public void DebugRequestResponse(LogEntryModel logEntryModel, bool isAdminRequest)
{
string message = JsonConvert.SerializeObject(logEntryModel, Formatting.Indented);
Log.DebugFormat("Admin[{0}] {1}", isAdminRequest, message);
}
}

View File

@@ -19,12 +19,12 @@ public class CookieModel
public IList<MatcherModel>? Matchers { get; set; }
/// <summary>
/// Gets or sets the ignore case.
/// Gets or sets the ignore case for the Cookie Name.
/// </summary>
public bool? IgnoreCase { get; set; }
/// <summary>
/// Reject on match.
/// Gets or sets the Reject on match for the Cookie Name.
/// </summary>
public bool? RejectOnMatch { get; set; }

View File

@@ -9,7 +9,7 @@ namespace WireMock.Admin.Mappings;
public class HeaderModel
{
/// <summary>
/// Gets or sets the name.
/// Gets or sets the name (key).
/// </summary>
public string Name { get; set; } = null!;
@@ -19,12 +19,12 @@ public class HeaderModel
public IList<MatcherModel>? Matchers { get; set; }
/// <summary>
/// Gets or sets the ignore case.
/// Gets or sets the ignore case for the Header Key.
/// </summary>
public bool? IgnoreCase { get; set; }
/// <summary>
/// Reject on match.
/// Gets or sets the Reject on match for the Header Key.
/// </summary>
public bool? RejectOnMatch { get; set; }

View File

@@ -31,27 +31,49 @@ public class SettingsModel
public int? MaxRequestLogCount { get; set; }
/// <summary>
/// Allow a Body for all HTTP Methods. (default set to false).
/// Allow a Body for all HTTP Methods. (default set to <c>false</c>).
/// </summary>
public bool? AllowBodyForAllHttpMethods { get; set; }
/// <summary>
/// Handle all requests synchronously. (default set to false).
/// Allow only a HttpStatus Code in the response which is defined. (default set to <c>false</c>).
/// - false : also null, 0, empty or invalid HttpStatus codes are allowed.
/// - true : only codes defined in <see cref="System.Net.HttpStatusCode"/> are allowed.
/// </summary>
public bool? AllowOnlyDefinedHttpStatusCodeInResponse { get; set; }
/// <summary>
/// Set to true to disable Json deserialization when processing requests. (default set to <c>false</c>).
/// </summary>
public bool? DisableJsonBodyParsing { get; set; }
/// <summary>
/// Disable support for GZip and Deflate request body decompression. (default set to <c>false</c>).
/// </summary>
public bool? DisableRequestBodyDecompressing { get; set; }
/// <summary>
/// Set to true to disable FormUrlEncoded deserializing when processing requests. (default set to <c>false</c>).
/// </summary>
public bool? DisableDeserializeFormUrlEncoded { get; set; }
/// <summary>
/// Handle all requests synchronously. (default set to <c>false</c>).
/// </summary>
public bool? HandleRequestsSynchronously { get; set; }
/// <summary>
/// Throw an exception when the Matcher fails because of invalid input. (default set to false).
/// Throw an exception when the Matcher fails because of invalid input. (default set to <c>false</c>).
/// </summary>
public bool? ThrowExceptionWhenMatcherFails { get; set; }
/// <summary>
/// Use the RegexExtended instead of the default <see cref="Regex"/>. (default set to true).
/// Use the RegexExtended instead of the default <see cref="Regex"/>. (default set to <c>true</c>).
/// </summary>
public bool? UseRegexExtended { get; set; }
/// <summary>
/// Save unmatched requests to a file using the <see cref="IFileSystemHandler"/>. (default set to false).
/// Save unmatched requests to a file using the <see cref="IFileSystemHandler"/>. (default set to <c>false</c>).
/// </summary>
public bool? SaveUnmatchedRequests { get; set; }
@@ -86,7 +108,7 @@ public class SettingsModel
public HostingScheme? HostingScheme { get; set; }
/// <summary>
/// Don't save the response-string in the LogEntry when WithBody(Func{IRequestMessage, string}) or WithBody(Func{IRequestMessage, Task{string}}) is used. (default set to false).
/// Don't save the response-string in the LogEntry when WithBody(Func{IRequestMessage, string}) or WithBody(Func{IRequestMessage, Task{string}}) is used. (default set to <c>false</c>).
/// </summary>
public bool? DoNotSaveDynamicResponseInLogEntry { get; set; }

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text;
using WireMock.Types;
@@ -38,6 +39,11 @@ public interface IBodyData
/// </summary>
string? BodyAsString { get; set; }
/// <summary>
/// The body as Form UrlEncoded dictionary.
/// </summary>
IDictionary<string, string>? BodyAsFormUrlEncoded { get; set; }
/// <summary>
/// The detected body type (detection based on body content).
/// </summary>

View File

@@ -1,38 +1,42 @@
namespace WireMock.Types
namespace WireMock.Types;
/// <summary>
/// The BodyType
/// </summary>
public enum BodyType
{
/// <summary>
/// The BodyType
/// No body present
/// </summary>
public enum BodyType
{
/// <summary>
/// No body present
/// </summary>
None,
None,
/// <summary>
/// Body is a String
/// </summary>
String,
/// <summary>
/// Body is a String
/// </summary>
String,
/// <summary>
/// Body is a Json object
/// </summary>
Json,
/// <summary>
/// Body is a Json object
/// </summary>
Json,
/// <summary>
/// Body is a Byte array
/// </summary>
Bytes,
/// <summary>
/// Body is a Byte array
/// </summary>
Bytes,
/// <summary>
/// Body is a File
/// </summary>
File,
/// <summary>
/// Body is a File
/// </summary>
File,
/// <summary>
/// Body is a MultiPart
/// </summary>
MultiPart
}
/// <summary>
/// Body is a MultiPart
/// </summary>
MultiPart,
/// <summary>
/// Body is a String which is x-www-form-urlencoded.
/// </summary>
FormUrlEncoded
}

View File

@@ -35,7 +35,7 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<!-- See also https://mstack.nl/blog/20210801-source-generators -->
<PackageReference Include="FluentBuilder" Version="0.7.0">
<PackageReference Include="FluentBuilder" Version="0.9.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -31,13 +31,13 @@ public static class WireMockServerExtensions
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="path">Path containing OpenAPI file to parse and use the mappings.</param>
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing</param>
[PublicAPI]
public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
Guard.NotNull(server, nameof(server));
Guard.NotNullOrEmpty(path, nameof(path));
Guard.NotNull(server);
Guard.NotNullOrEmpty(path);
var mappings = new WireMockOpenApiParser().FromFile(path, settings, out diagnostic);
@@ -80,9 +80,9 @@ public static class WireMockServerExtensions
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="document">The OpenAPI document to use as mappings.</param>
/// <param name="settings">Additional settings [optional]</param>
/// <param name="settings">Additional settings [optional].</param>
[PublicAPI]
public static IWireMockServer WithMappingFromOpenApiDocument(this IWireMockServer server, OpenApiDocument document, WireMockOpenApiParserSettings settings)
public static IWireMockServer WithMappingFromOpenApiDocument(this IWireMockServer server, OpenApiDocument document, WireMockOpenApiParserSettings? settings = null)
{
Guard.NotNull(server);
Guard.NotNull(document);

View File

@@ -5,53 +5,69 @@ using Microsoft.OpenApi.Readers;
using WireMock.Admin.Mappings;
using WireMock.Net.OpenApiParser.Settings;
namespace WireMock.Net.OpenApiParser
namespace WireMock.Net.OpenApiParser;
/// <summary>
/// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock MappingModels.
/// </summary>
public interface IWireMockOpenApiParser
{
/// <summary>
/// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock MappingModels.
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a file-path.
/// </summary>
public interface IWireMockOpenApiParser
{
/// <summary>
/// Generate <see cref="IEnumerable{MappingModel}"/> from a file-path.
/// </summary>
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3 or Raml file.</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IEnumerable<MappingModel> FromFile(string path, out OpenApiDiagnostic diagnostic);
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3 or Raml file.</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromFile(string path, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IEnumerable{MappingModel}"/> from a file-path.
/// </summary>
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3 or Raml file.</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IEnumerable<MappingModel> FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a file-path.
/// </summary>
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3 or Raml file.</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IEnumerable{MappingModel}"/> from an <seealso cref="OpenApiDocument"/>.
/// </summary>
/// <param name="document">The source OpenApiDocument</param>
/// <param name="settings">Additional settings [optional]</param>
/// <returns>MappingModel</returns>
IEnumerable<MappingModel> FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from an <seealso cref="OpenApiDocument"/>.
/// </summary>
/// <param name="document">The source OpenApiDocument</param>
/// <param name="settings">Additional settings [optional]</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null);
/// <summary>
/// Generate <see cref="IEnumerable{MappingModel}"/> from a <seealso cref="Stream"/>.
/// </summary>
/// <param name="stream">The source stream</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IEnumerable<MappingModel> FromStream(Stream stream, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a <seealso cref="Stream"/>.
/// </summary>
/// <param name="stream">The source stream</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromStream(Stream stream, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IEnumerable{MappingModel}"/> from a <seealso cref="Stream"/>.
/// </summary>
/// <param name="stream">The source stream</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IEnumerable<MappingModel> FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic);
}
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a <seealso cref="Stream"/>.
/// </summary>
/// <param name="stream">The source stream</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a <seealso cref="string"/>.
/// </summary>
/// <param name="text">The source text</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromText(string text, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a <seealso cref="string"/>.
/// </summary>
/// <param name="text">The source text</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic);
}

View File

@@ -31,19 +31,29 @@ internal class OpenApiPathsMapper
_exampleValueGenerator = new ExampleValueGenerator(settings);
}
public IEnumerable<MappingModel> ToMappingModels(OpenApiPaths paths, IList<OpenApiServer> servers)
public IReadOnlyList<MappingModel> ToMappingModels(OpenApiPaths? paths, IList<OpenApiServer> servers)
{
return paths.Select(p => MapPath(p.Key, p.Value, servers)).SelectMany(x => x);
return paths?
.OrderBy(p => p.Key)
.Select(p => MapPath(p.Key, p.Value, servers))
.SelectMany(x => x)
.ToArray() ??
Array.Empty<MappingModel>();
}
private IEnumerable<MappingModel> MapPaths(OpenApiPaths paths, IList<OpenApiServer> servers)
private IReadOnlyList<MappingModel> MapPaths(OpenApiPaths? paths, IList<OpenApiServer> servers)
{
return paths.Select(p => MapPath(p.Key, p.Value, servers)).SelectMany(x => x);
return paths?
.OrderBy(p => p.Key)
.Select(p => MapPath(p.Key, p.Value, servers))
.SelectMany(x => x)
.ToArray() ??
Array.Empty<MappingModel>();
}
private IEnumerable<MappingModel> MapPath(string path, OpenApiPathItem pathItem, IList<OpenApiServer> servers)
private IReadOnlyList<MappingModel> MapPath(string path, OpenApiPathItem pathItem, IList<OpenApiServer> servers)
{
return pathItem.Operations.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers));
return pathItem.Operations.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers)).ToArray();
}
private MappingModel MapOperationToMappingModel(string path, string httpMethod, OpenApiOperation operation, IList<OpenApiServer> servers)
@@ -123,7 +133,7 @@ internal class OpenApiPathsMapper
};
}
private bool TryGetContent(IDictionary<string, OpenApiMediaType>? contents, [NotNullWhen(true)] out OpenApiMediaType? openApiMediaType, [NotNullWhen(true)] out string? contentType)
private static bool TryGetContent(IDictionary<string, OpenApiMediaType>? contents, [NotNullWhen(true)] out OpenApiMediaType? openApiMediaType, [NotNullWhen(true)] out string? contentType)
{
openApiMediaType = null;
contentType = null;
@@ -305,19 +315,19 @@ internal class OpenApiPathsMapper
return JObject.Parse(outputString.ToString());
}
private IDictionary<string, object?>? MapHeaders(string responseContentType, IDictionary<string, OpenApiHeader> headers)
private IDictionary<string, object>? MapHeaders(string? responseContentType, IDictionary<string, OpenApiHeader>? headers)
{
var mappedHeaders = headers.ToDictionary(
var mappedHeaders = headers?.ToDictionary(
item => item.Key,
_ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern
_ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern!
);
if (!string.IsNullOrEmpty(responseContentType))
{
mappedHeaders.TryAdd(HeaderContentType, responseContentType);
mappedHeaders.TryAdd(HeaderContentType, responseContentType!);
}
return mappedHeaders.Keys.Any() ? mappedHeaders : null;
return mappedHeaders?.Keys.Any() == true ? mappedHeaders : null;
}
private IList<ParamModel>? MapQueryParameters(IEnumerable<OpenApiParameter> queryParameters)
@@ -360,9 +370,18 @@ internal class OpenApiPathsMapper
{
return type switch
{
ExampleValueType.Value => new MatcherModel { Name = "ExactMatcher", Pattern = GetExampleValueAsStringForSchemaType(schema), IgnoreCase = _settings.IgnoreCaseExampleValues },
ExampleValueType.Value => new MatcherModel
{
Name = "ExactMatcher",
Pattern = GetExampleValueAsStringForSchemaType(schema),
IgnoreCase = _settings.IgnoreCaseExampleValues
},
_ => new MatcherModel { Name = "WildcardMatcher", Pattern = "*" }
_ => new MatcherModel
{
Name = "WildcardMatcher",
Pattern = "*"
}
};
}

View File

@@ -36,5 +36,5 @@ public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleV
public virtual string String { get; set; } = "example-string";
/// <inheritdoc />
public virtual OpenApiSchema? Schema { get; set; } = new OpenApiSchema();
public virtual OpenApiSchema? Schema { get; set; } = new();
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Stef.Validation;
@@ -11,116 +10,108 @@ namespace WireMock.Net.OpenApiParser.Utils;
internal class ExampleValueGenerator
{
private readonly WireMockOpenApiParserSettings _settings;
private readonly IWireMockOpenApiParserExampleValues _exampleValues;
public ExampleValueGenerator(WireMockOpenApiParserSettings settings)
{
_settings = Guard.NotNull(settings);
Guard.NotNull(settings);
// Check if user provided an own implementation
if (settings.ExampleValues is null)
{
if (_settings.DynamicExamples)
if (settings.DynamicExamples)
{
_settings.ExampleValues = new WireMockOpenApiParserDynamicExampleValues();
_exampleValues = new WireMockOpenApiParserDynamicExampleValues();
}
else
{
_settings.ExampleValues = new WireMockOpenApiParserExampleValues();
_exampleValues = new WireMockOpenApiParserExampleValues();
}
}
else
{
_exampleValues = settings.ExampleValues;
}
}
public object GetExampleValue(OpenApiSchema? schema)
{
var schemaExample = schema?.Example;
var schemaEnum = GetRandomEnumValue(schema?.Enum);
var schemaEnum = schema?.Enum?.FirstOrDefault();
_settings.ExampleValues.Schema = schema;
_exampleValues.Schema = schema;
switch (schema?.GetSchemaType())
{
case SchemaType.Boolean:
var exampleBoolean = schemaExample as OpenApiBoolean;
return exampleBoolean is null ? _settings.ExampleValues.Boolean : exampleBoolean.Value;
return exampleBoolean?.Value ?? _exampleValues.Boolean;
case SchemaType.Integer:
switch (schema?.GetSchemaFormat())
{
case SchemaFormat.Int64:
var exampleLong = (OpenApiLong)schemaExample;
var enumLong = (OpenApiLong)schemaEnum;
var valueLongEnumOrExample = enumLong is null ? exampleLong?.Value : enumLong?.Value;
return valueLongEnumOrExample ?? _settings.ExampleValues.Integer;
var exampleLong = schemaExample as OpenApiLong;
var enumLong = schemaEnum as OpenApiLong;
var valueLongEnumOrExample = enumLong?.Value ?? exampleLong?.Value;
return valueLongEnumOrExample ?? _exampleValues.Integer;
default:
var exampleInteger = (OpenApiInteger)schemaExample;
var enumInteger = (OpenApiInteger)schemaEnum;
var valueIntegerEnumOrExample = enumInteger is null ? exampleInteger?.Value : enumInteger?.Value;
return valueIntegerEnumOrExample ?? _settings.ExampleValues.Integer;
var exampleInteger = schemaExample as OpenApiInteger;
var enumInteger = schemaEnum as OpenApiInteger;
var valueIntegerEnumOrExample = enumInteger?.Value ?? exampleInteger?.Value;
return valueIntegerEnumOrExample ?? _exampleValues.Integer;
}
case SchemaType.Number:
switch (schema?.GetSchemaFormat())
{
case SchemaFormat.Float:
var exampleFloat = (OpenApiFloat)schemaExample;
var enumFloat = (OpenApiFloat)schemaEnum;
var valueFloatEnumOrExample = enumFloat is null ? exampleFloat?.Value : enumFloat?.Value;
return valueFloatEnumOrExample ?? _settings.ExampleValues.Float;
var exampleFloat = schemaExample as OpenApiFloat;
var enumFloat = schemaEnum as OpenApiFloat;
var valueFloatEnumOrExample = enumFloat?.Value ?? exampleFloat?.Value;
return valueFloatEnumOrExample ?? _exampleValues.Float;
default:
var exampleDouble = (OpenApiDouble)schemaExample;
var enumDouble = (OpenApiDouble)schemaEnum;
var valueDoubleEnumOrExample = enumDouble is null ? exampleDouble?.Value : enumDouble?.Value;
return valueDoubleEnumOrExample ?? _settings.ExampleValues.Double;
var exampleDouble = schemaExample as OpenApiDouble;
var enumDouble = schemaEnum as OpenApiDouble;
var valueDoubleEnumOrExample = enumDouble?.Value ?? exampleDouble?.Value;
return valueDoubleEnumOrExample ?? _exampleValues.Double;
}
default:
switch (schema?.GetSchemaFormat())
{
case SchemaFormat.Date:
var exampleDate = (OpenApiDate)schemaExample;
var enumDate = (OpenApiDate)schemaEnum;
var valueDateEnumOrExample = enumDate is null ? exampleDate?.Value : enumDate?.Value;
return DateTimeUtils.ToRfc3339Date(valueDateEnumOrExample ?? _settings.ExampleValues.Date());
var exampleDate = schemaExample as OpenApiDate;
var enumDate = schemaEnum as OpenApiDate;
var valueDateEnumOrExample = enumDate?.Value ?? exampleDate?.Value;
return DateTimeUtils.ToRfc3339Date(valueDateEnumOrExample ?? _exampleValues.Date());
case SchemaFormat.DateTime:
var exampleDateTime = (OpenApiDateTime)schemaExample;
var enumDateTime = (OpenApiDateTime)schemaEnum;
var valueDateTimeEnumOrExample = enumDateTime is null ? exampleDateTime?.Value : enumDateTime?.Value;
return DateTimeUtils.ToRfc3339DateTime(valueDateTimeEnumOrExample?.DateTime ?? _settings.ExampleValues.DateTime());
var exampleDateTime = schemaExample as OpenApiDateTime;
var enumDateTime = schemaEnum as OpenApiDateTime;
var valueDateTimeEnumOrExample = enumDateTime?.Value ?? exampleDateTime?.Value;
return DateTimeUtils.ToRfc3339DateTime(valueDateTimeEnumOrExample?.DateTime ?? _exampleValues.DateTime());
case SchemaFormat.Byte:
var exampleByte = (OpenApiByte)schemaExample;
var enumByte = (OpenApiByte)schemaEnum;
var valueByteEnumOrExample = enumByte is null ? exampleByte?.Value : enumByte?.Value;
return valueByteEnumOrExample ?? _settings.ExampleValues.Bytes;
var exampleByte = schemaExample as OpenApiByte;
var enumByte = schemaEnum as OpenApiByte;
var valueByteEnumOrExample = enumByte?.Value ?? exampleByte?.Value;
return valueByteEnumOrExample ?? _exampleValues.Bytes;
case SchemaFormat.Binary:
var exampleBinary = (OpenApiBinary)schemaExample;
var enumBinary = (OpenApiBinary)schemaEnum;
var valueBinaryEnumOrExample = enumBinary is null ? exampleBinary?.Value : enumBinary?.Value;
return valueBinaryEnumOrExample ?? _settings.ExampleValues.Object;
var exampleBinary = schemaExample as OpenApiBinary;
var enumBinary = schemaEnum as OpenApiBinary;
var valueBinaryEnumOrExample = enumBinary?.Value ?? exampleBinary?.Value;
return valueBinaryEnumOrExample ?? _exampleValues.Object;
default:
var exampleString = (OpenApiString)schemaExample;
var enumString = (OpenApiString)schemaEnum;
var valueStringEnumOrExample = enumString is null ? exampleString?.Value : enumString?.Value;
return valueStringEnumOrExample ?? _settings.ExampleValues.String;
var exampleString = schemaExample as OpenApiString;
var enumString = schemaEnum as OpenApiString;
var valueStringEnumOrExample = enumString?.Value ?? exampleString?.Value;
return valueStringEnumOrExample ?? _exampleValues.String;
}
}
}
private static IOpenApiAny? GetRandomEnumValue(IList<IOpenApiAny>? schemaEnum)
{
if (schemaEnum?.Count > 0)
{
int maxValue = schemaEnum.Count - 1;
int randomEnum = new Random().Next(0, maxValue);
return schemaEnum[randomEnum];
}
return null;
}
}

View File

@@ -4,7 +4,7 @@
<Description>An OpenApi (swagger) parser to generate MappingModel or mapping.json file.</Description>
<TargetFrameworks>net46;netstandard2.0;netstandard2.1</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>wiremock;openapi;OAS;converter;parser;openapiparser</PackageTags>
<PackageTags>wiremock;openapi;OAS;raml;converter;parser;openapiparser</PackageTags>
<ProjectGuid>{D3804228-91F4-4502-9595-39584E5AADAD}</ProjectGuid>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using JetBrains.Annotations;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
@@ -16,18 +17,18 @@ namespace WireMock.Net.OpenApiParser;
/// </summary>
public class WireMockOpenApiParser : IWireMockOpenApiParser
{
private readonly OpenApiStreamReader _reader = new OpenApiStreamReader();
private readonly OpenApiStreamReader _reader = new();
/// <inheritdoc cref="IWireMockOpenApiParser.FromFile(string, out OpenApiDiagnostic)" />
/// <inheritdoc />
[PublicAPI]
public IEnumerable<MappingModel> FromFile(string path, out OpenApiDiagnostic diagnostic)
public IReadOnlyList<MappingModel> FromFile(string path, out OpenApiDiagnostic diagnostic)
{
return FromFile(path, new WireMockOpenApiParserSettings(), out diagnostic);
}
/// <inheritdoc cref="IWireMockOpenApiParser.FromFile(string, WireMockOpenApiParserSettings, out OpenApiDiagnostic)" />
/// <inheritdoc />
[PublicAPI]
public IEnumerable<MappingModel> FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
public IReadOnlyList<MappingModel> FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
OpenApiDocument document;
if (Path.GetExtension(path).EndsWith("raml", StringComparison.OrdinalIgnoreCase))
@@ -44,24 +45,38 @@ public class WireMockOpenApiParser : IWireMockOpenApiParser
return FromDocument(document, settings);
}
/// <inheritdoc cref="IWireMockOpenApiParser.FromStream(Stream, out OpenApiDiagnostic)" />
/// <inheritdoc />
[PublicAPI]
public IEnumerable<MappingModel> FromStream(Stream stream, out OpenApiDiagnostic diagnostic)
public IReadOnlyList<MappingModel> FromDocument(OpenApiDocument openApiDocument, WireMockOpenApiParserSettings? settings = null)
{
return new OpenApiPathsMapper(settings ?? new WireMockOpenApiParserSettings()).ToMappingModels(openApiDocument.Paths, openApiDocument.Servers);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromStream(Stream stream, out OpenApiDiagnostic diagnostic)
{
return FromDocument(_reader.Read(stream, out diagnostic));
}
/// <inheritdoc cref="IWireMockOpenApiParser.FromStream(Stream, WireMockOpenApiParserSettings, out OpenApiDiagnostic)" />
/// <inheritdoc />
[PublicAPI]
public IEnumerable<MappingModel> FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
public IReadOnlyList<MappingModel> FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
return FromDocument(_reader.Read(stream, out diagnostic), settings);
}
/// <inheritdoc cref="IWireMockOpenApiParser.FromDocument(OpenApiDocument, WireMockOpenApiParserSettings)" />
/// <inheritdoc />
[PublicAPI]
public IEnumerable<MappingModel> FromDocument(OpenApiDocument openApiDocument, WireMockOpenApiParserSettings? settings = null)
public IReadOnlyList<MappingModel> FromText(string text, out OpenApiDiagnostic diagnostic)
{
return new OpenApiPathsMapper(settings).ToMappingModels(openApiDocument.Paths, openApiDocument.Servers);
return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), out diagnostic);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), settings, out diagnostic);
}
}

View File

@@ -280,4 +280,20 @@ public interface IWireMockAdminApi
/// <param name="cancellationToken">The optional cancellationToken.</param>
[Head("files/{filename}")]
Task FileExistsAsync([Path] string filename, CancellationToken cancellationToken = default);
/// <summary>
/// Convert an OpenApi / RAML document to mappings.
/// </summary>
/// <param name="text">The OpenApi or RAML document as text.</param>
/// <param name="cancellationToken">The optional cancellationToken.</param>
[Post("openapi/convert")]
Task<IReadOnlyList<MappingModel>> OpenApiConvertAsync([Body] string text, CancellationToken cancellationToken = default);
/// <summary>
/// Convert an OpenApi / RAML document to mappings and save these.
/// </summary>
/// <param name="text">The OpenApi or RAML document as text.</param>
/// <param name="cancellationToken">The optional cancellationToken.</param>
[Post("openapi/save")]
Task<StatusModel> OpenApiSaveAsync([Body] string text, CancellationToken cancellationToken = default);
}

View File

@@ -1,35 +1,34 @@
using System;
using System;
namespace WireMock.Exceptions
namespace WireMock.Exceptions;
/// <summary>
/// WireMockException
/// </summary>
/// <seealso cref="Exception" />
public class WireMockException : Exception
{
/// <summary>
/// WireMockException
/// Initializes a new instance of the <see cref="WireMockException"/> class.
/// </summary>
/// <seealso cref="Exception" />
public class WireMockException : Exception
public WireMockException()
{
/// <summary>
/// Initializes a new instance of the <see cref="WireMockException"/> class.
/// </summary>
public WireMockException()
{
}
}
/// <summary>
/// Initializes a new instance of the <see cref="WireMockException"/> class.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public WireMockException(string message) : base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WireMockException"/> class.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public WireMockException(string message) : base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WireMockException"/> class.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="inner">The inner.</param>
public WireMockException(string message, Exception inner) : base(message, inner)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="WireMockException"/> class.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="inner">The inner.</param>
public WireMockException(string message, Exception inner) : base(message, inner)
{
}
}

View File

@@ -3,179 +3,178 @@ using System.IO;
using WireMock.Util;
using Stef.Validation;
namespace WireMock.Handlers
namespace WireMock.Handlers;
/// <summary>
/// Default implementation for a handler to interact with the local file system to read and write static mapping files.
/// </summary>
public class LocalFileSystemHandler : IFileSystemHandler
{
private static readonly string AdminMappingsFolder = Path.Combine("__admin", "mappings");
private static readonly string UnmatchedRequestsFolder = Path.Combine("requests", "unmatched");
private readonly string _rootFolder;
/// <summary>
/// Default implementation for a handler to interact with the local file system to read and write static mapping files.
/// Initializes a new instance of the <see cref="LocalFileSystemHandler"/> class.
/// </summary>
public class LocalFileSystemHandler : IFileSystemHandler
public LocalFileSystemHandler() : this(Directory.GetCurrentDirectory())
{
private static readonly string AdminMappingsFolder = Path.Combine("__admin", "mappings");
private static readonly string UnmatchedRequestsFolder = Path.Combine("requests", "unmatched");
}
private readonly string _rootFolder;
/// <summary>
/// Initializes a new instance of the <see cref="LocalFileSystemHandler"/> class.
/// </summary>
/// <param name="rootFolder">The root folder.</param>
public LocalFileSystemHandler(string rootFolder)
{
_rootFolder = rootFolder;
}
/// <summary>
/// Initializes a new instance of the <see cref="LocalFileSystemHandler"/> class.
/// </summary>
public LocalFileSystemHandler() : this(Directory.GetCurrentDirectory())
/// <inheritdoc cref="IFileSystemHandler.FolderExists"/>
public virtual bool FolderExists(string path)
{
Guard.NotNullOrEmpty(path);
return Directory.Exists(path);
}
/// <inheritdoc cref="IFileSystemHandler.CreateFolder"/>
public virtual void CreateFolder(string path)
{
Guard.NotNullOrEmpty(path);
Directory.CreateDirectory(path);
}
/// <inheritdoc cref="IFileSystemHandler.EnumerateFiles"/>
public virtual IEnumerable<string> EnumerateFiles(string path, bool includeSubdirectories)
{
Guard.NotNullOrEmpty(path);
return includeSubdirectories ? Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories) : Directory.EnumerateFiles(path);
}
/// <inheritdoc cref="IFileSystemHandler.GetMappingFolder"/>
public virtual string GetMappingFolder()
{
return Path.Combine(_rootFolder, AdminMappingsFolder);
}
/// <inheritdoc cref="IFileSystemHandler.ReadMappingFile"/>
public virtual string ReadMappingFile(string path)
{
Guard.NotNullOrEmpty(path);
return File.ReadAllText(path);
}
/// <inheritdoc cref="IFileSystemHandler.WriteMappingFile(string, string)"/>
public virtual void WriteMappingFile(string path, string text)
{
Guard.NotNullOrEmpty(path, nameof(path));
Guard.NotNull(text, nameof(text));
File.WriteAllText(path, text);
}
/// <inheritdoc cref="IFileSystemHandler.ReadResponseBodyAsFile"/>
public virtual byte[] ReadResponseBodyAsFile(string path)
{
Guard.NotNullOrEmpty(path);
path = PathUtils.CleanPath(path);
// If the file exists at the given path relative to the MappingsFolder, then return that.
// Else the path will just be as-is.
return File.ReadAllBytes(File.Exists(PathUtils.Combine(GetMappingFolder(), path)) ? PathUtils.Combine(GetMappingFolder(), path) : path);
}
/// <inheritdoc cref="IFileSystemHandler.ReadResponseBodyAsString"/>
public virtual string ReadResponseBodyAsString(string path)
{
Guard.NotNullOrEmpty(path);
path = PathUtils.CleanPath(path);
// In case the path is a filename, the path will be adjusted to the MappingFolder.
// Else the path will just be as-is.
return File.ReadAllText(File.Exists(PathUtils.Combine(GetMappingFolder(), path)) ? PathUtils.Combine(GetMappingFolder(), path) : path);
}
/// <inheritdoc cref="IFileSystemHandler.FileExists"/>
public virtual bool FileExists(string filename)
{
Guard.NotNullOrEmpty(filename);
return File.Exists(AdjustPathForMappingFolder(filename));
}
/// <inheritdoc />
public virtual void WriteFile(string filename, byte[] bytes)
{
Guard.NotNullOrEmpty(filename);
Guard.NotNull(bytes);
File.WriteAllBytes(AdjustPathForMappingFolder(filename), bytes);
}
/// <inheritdoc />
public virtual void WriteFile(string folder, string filename, byte[] bytes)
{
Guard.NotNullOrEmpty(folder);
Guard.NotNullOrEmpty(filename);
Guard.NotNull(bytes);
File.WriteAllBytes(PathUtils.Combine(folder, filename), bytes);
}
/// <inheritdoc cref="IFileSystemHandler.DeleteFile"/>
public virtual void DeleteFile(string filename)
{
Guard.NotNullOrEmpty(filename);
File.Delete(AdjustPathForMappingFolder(filename));
}
/// <inheritdoc cref="IFileSystemHandler.ReadFile"/>
public virtual byte[] ReadFile(string filename)
{
Guard.NotNullOrEmpty(filename);
return File.ReadAllBytes(AdjustPathForMappingFolder(filename));
}
/// <inheritdoc cref="IFileSystemHandler.ReadFileAsString"/>
public virtual string ReadFileAsString(string filename)
{
return File.ReadAllText(AdjustPathForMappingFolder(Guard.NotNullOrEmpty(filename, nameof(filename))));
}
/// <inheritdoc cref="IFileSystemHandler.GetUnmatchedRequestsFolder"/>
public virtual string GetUnmatchedRequestsFolder()
{
return Path.Combine(_rootFolder, UnmatchedRequestsFolder);
}
/// <inheritdoc cref="IFileSystemHandler.WriteUnmatchedRequest"/>
public virtual void WriteUnmatchedRequest(string filename, string text)
{
Guard.NotNullOrEmpty(filename);
Guard.NotNull(text);
var folder = GetUnmatchedRequestsFolder();
if (!FolderExists(folder))
{
CreateFolder(folder);
}
/// <summary>
/// Initializes a new instance of the <see cref="LocalFileSystemHandler"/> class.
/// </summary>
/// <param name="rootFolder">The root folder.</param>
public LocalFileSystemHandler(string rootFolder)
{
_rootFolder = rootFolder;
}
File.WriteAllText(Path.Combine(folder, filename), text);
}
/// <inheritdoc cref="IFileSystemHandler.FolderExists"/>
public virtual bool FolderExists(string path)
{
Guard.NotNullOrEmpty(path, nameof(path));
return Directory.Exists(path);
}
/// <inheritdoc cref="IFileSystemHandler.CreateFolder"/>
public virtual void CreateFolder(string path)
{
Guard.NotNullOrEmpty(path, nameof(path));
Directory.CreateDirectory(path);
}
/// <inheritdoc cref="IFileSystemHandler.EnumerateFiles"/>
public virtual IEnumerable<string> EnumerateFiles(string path, bool includeSubdirectories)
{
Guard.NotNullOrEmpty(path, nameof(path));
return includeSubdirectories ? Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories) : Directory.EnumerateFiles(path);
}
/// <inheritdoc cref="IFileSystemHandler.GetMappingFolder"/>
public virtual string GetMappingFolder()
{
return Path.Combine(_rootFolder, AdminMappingsFolder);
}
/// <inheritdoc cref="IFileSystemHandler.ReadMappingFile"/>
public virtual string ReadMappingFile(string path)
{
Guard.NotNullOrEmpty(path, nameof(path));
return File.ReadAllText(path);
}
/// <inheritdoc cref="IFileSystemHandler.WriteMappingFile(string, string)"/>
public virtual void WriteMappingFile(string path, string text)
{
Guard.NotNullOrEmpty(path, nameof(path));
Guard.NotNull(text, nameof(text));
File.WriteAllText(path, text);
}
/// <inheritdoc cref="IFileSystemHandler.ReadResponseBodyAsFile"/>
public virtual byte[] ReadResponseBodyAsFile(string path)
{
Guard.NotNullOrEmpty(path, nameof(path));
path = PathUtils.CleanPath(path);
// If the file exists at the given path relative to the MappingsFolder, then return that.
// Else the path will just be as-is.
return File.ReadAllBytes(File.Exists(PathUtils.Combine(GetMappingFolder(), path)) ? PathUtils.Combine(GetMappingFolder(), path) : path);
}
/// <inheritdoc cref="IFileSystemHandler.ReadResponseBodyAsString"/>
public virtual string ReadResponseBodyAsString(string path)
{
Guard.NotNullOrEmpty(path, nameof(path));
path = PathUtils.CleanPath(path);
// In case the path is a filename, the path will be adjusted to the MappingFolder.
// Else the path will just be as-is.
return File.ReadAllText(File.Exists(PathUtils.Combine(GetMappingFolder(), path)) ? PathUtils.Combine(GetMappingFolder(), path) : path);
}
/// <inheritdoc cref="IFileSystemHandler.FileExists"/>
public virtual bool FileExists(string filename)
{
Guard.NotNullOrEmpty(filename, nameof(filename));
return File.Exists(AdjustPathForMappingFolder(filename));
}
/// <inheritdoc />
public virtual void WriteFile(string filename, byte[] bytes)
{
Guard.NotNullOrEmpty(filename, nameof(filename));
Guard.NotNull(bytes, nameof(bytes));
File.WriteAllBytes(AdjustPathForMappingFolder(filename), bytes);
}
/// <inheritdoc />
public virtual void WriteFile(string folder, string filename, byte[] bytes)
{
Guard.NotNullOrEmpty(folder);
Guard.NotNullOrEmpty(filename);
Guard.NotNull(bytes);
File.WriteAllBytes(PathUtils.Combine(folder, filename), bytes);
}
/// <inheritdoc cref="IFileSystemHandler.DeleteFile"/>
public virtual void DeleteFile(string filename)
{
Guard.NotNullOrEmpty(filename, nameof(filename));
File.Delete(AdjustPathForMappingFolder(filename));
}
/// <inheritdoc cref="IFileSystemHandler.ReadFile"/>
public virtual byte[] ReadFile(string filename)
{
Guard.NotNullOrEmpty(filename, nameof(filename));
return File.ReadAllBytes(AdjustPathForMappingFolder(filename));
}
/// <inheritdoc cref="IFileSystemHandler.ReadFileAsString"/>
public virtual string ReadFileAsString(string filename)
{
return File.ReadAllText(AdjustPathForMappingFolder(Guard.NotNullOrEmpty(filename, nameof(filename))));
}
/// <inheritdoc cref="IFileSystemHandler.GetUnmatchedRequestsFolder"/>
public virtual string GetUnmatchedRequestsFolder()
{
return Path.Combine(_rootFolder, UnmatchedRequestsFolder);
}
/// <inheritdoc cref="IFileSystemHandler.WriteUnmatchedRequest"/>
public virtual void WriteUnmatchedRequest(string filename, string text)
{
Guard.NotNullOrEmpty(filename, nameof(filename));
Guard.NotNull(text, nameof(text));
var folder = GetUnmatchedRequestsFolder();
if (!FolderExists(folder))
{
CreateFolder(folder);
}
File.WriteAllText(Path.Combine(folder, filename), text);
}
/// <summary>
/// Adjusts the path to the MappingFolder.
/// </summary>
/// <param name="filename">The path.</param>
/// <returns>Adjusted path</returns>
private string AdjustPathForMappingFolder(string filename)
{
return Path.Combine(GetMappingFolder(), filename);
}
/// <summary>
/// Adjusts the path to the MappingFolder.
/// </summary>
/// <param name="filename">The path.</param>
/// <returns>Adjusted path</returns>
private string AdjustPathForMappingFolder(string filename)
{
return Path.Combine(GetMappingFolder(), filename);
}
}

View File

@@ -28,7 +28,7 @@ internal static class HttpRequestMessageHelper
switch (requestMessage.BodyData?.DetectedBodyType)
{
case BodyType.Bytes:
httpRequestMessage.Content = ByteArrayContentHelper.Create(requestMessage.BodyData.BodyAsBytes, contentType);
httpRequestMessage.Content = ByteArrayContentHelper.Create(requestMessage.BodyData.BodyAsBytes!, contentType);
break;
case BodyType.Json:
@@ -36,7 +36,8 @@ internal static class HttpRequestMessageHelper
break;
case BodyType.String:
httpRequestMessage.Content = StringContentHelper.Create(requestMessage.BodyData.BodyAsString, contentType);
case BodyType.FormUrlEncoded:
httpRequestMessage.Content = StringContentHelper.Create(requestMessage.BodyData.BodyAsString!, contentType);
break;
}

View File

@@ -15,7 +15,8 @@ internal static class HttpResponseMessageHelper
Uri requiredUri,
Uri originalUri,
bool deserializeJson,
bool decompressGzipAndDeflate)
bool decompressGzipAndDeflate,
bool deserializeFormUrlEncoded)
{
var responseMessage = new ResponseMessage { StatusCode = (int)httpResponseMessage.StatusCode };
@@ -44,7 +45,8 @@ internal static class HttpResponseMessageHelper
ContentType = contentTypeHeader?.FirstOrDefault(),
DeserializeJson = deserializeJson,
ContentEncoding = contentEncodingHeader?.FirstOrDefault(),
DecompressGZipAndDeflate = decompressGzipAndDeflate
DecompressGZipAndDeflate = decompressGzipAndDeflate,
DeserializeFormUrlEncoded = deserializeFormUrlEncoded
};
responseMessage.BodyData = await BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false);
}
@@ -55,7 +57,7 @@ internal static class HttpResponseMessageHelper
// If Location header contains absolute redirect URL, and base URL is one that we proxy to,
// we need to replace it to original one.
if (string.Equals(header.Key, HttpKnownHeaderNames.Location, StringComparison.OrdinalIgnoreCase)
&& Uri.TryCreate(header.Value.First(), UriKind.Absolute, out Uri absoluteLocationUri)
&& Uri.TryCreate(header.Value.First(), UriKind.Absolute, out var absoluteLocationUri)
&& string.Equals(absoluteLocationUri.Host, requiredUri.Host, StringComparison.OrdinalIgnoreCase))
{
var replacedLocationUri = new Uri(originalUri, absoluteLocationUri.PathAndQuery);

View File

@@ -121,7 +121,7 @@ public interface IMapping
/// <summary>
/// Use Fire and Forget for the defined webhook(s). [Optional]
/// </summary>
bool? UseWebhooksFireAndForget { get; set; }
bool? UseWebhooksFireAndForget { get; }
/// <summary>
/// Data Object which can be used when WithTransformer is used.
@@ -130,7 +130,7 @@ public interface IMapping
/// lookup data "1"
/// </example>
/// </summary>
object? Data { get; set; }
object? Data { get; }
/// <summary>
/// ProvideResponseAsync

View File

@@ -60,7 +60,7 @@ internal static class JObjectExtensions
{
if (src == null)
{
return new object?[0];
return EmptyArray<object?>.Value;
}
return ConvertJTokenArray(src, options);

View File

@@ -67,13 +67,13 @@ public class Mapping : IMapping
public IWebhook[]? Webhooks { get; }
/// <inheritdoc />
public bool? UseWebhooksFireAndForget { get; set; }
public bool? UseWebhooksFireAndForget { get; }
/// <inheritdoc />
public ITimeSettings? TimeSettings { get; }
/// <inheritdoc />
public object? Data { get; set; }
public object? Data { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Mapping"/> class.

View File

@@ -1,6 +1,5 @@
using System.Net.Http.Headers;
using AnyOfTypes;
using JetBrains.Annotations;
using WireMock.Models;
namespace WireMock.Matchers;

View File

@@ -1,3 +1,4 @@
using System;
using System.Linq;
using AnyOfTypes;
using WireMock.Models;
@@ -62,7 +63,7 @@ public class NotNullOrEmptyMatcher : IObjectMatcher, IStringMatcher
/// <inheritdoc cref="IStringMatcher.GetPatterns"/>
public AnyOf<string, StringPattern>[] GetPatterns()
{
return new AnyOf<string, StringPattern>[0];
return EmptyArray<AnyOf<string, StringPattern>>.Value;
}
/// <inheritdoc />

View File

@@ -1,8 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AnyOfTypes;
using Stef.Validation;
using WireMock.Models;
using WireMock.Types;
using WireMock.Util;
@@ -33,6 +32,11 @@ public class RequestMessageBodyMatcher : IRequestMatcher
/// </summary>
public Func<IBodyData?, bool>? BodyDataFunc { get; }
/// <summary>
/// The body data function for FormUrlEncoded
/// </summary>
public Func<IDictionary<string, string>?, bool>? FormUrlEncodedFunc { get; }
/// <summary>
/// The matchers.
/// </summary>
@@ -109,6 +113,15 @@ public class RequestMessageBodyMatcher : IRequestMatcher
BodyDataFunc = Guard.NotNull(func);
}
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageBodyMatcher"/> class.
/// </summary>
/// <param name="func">The function.</param>
public RequestMessageBodyMatcher(Func<IDictionary<string, string>?, bool> func)
{
FormUrlEncodedFunc = Guard.NotNull(func);
}
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageBodyMatcher"/> class.
/// </summary>
@@ -144,6 +157,7 @@ public class RequestMessageBodyMatcher : IRequestMatcher
{
case BodyType.Json:
case BodyType.String:
case BodyType.FormUrlEncoded:
return notNullOrEmptyMatcher.IsMatch(requestMessage.BodyData.BodyAsString);
case BodyType.Bytes:
@@ -158,7 +172,7 @@ public class RequestMessageBodyMatcher : IRequestMatcher
{
// If the body is a byte array, try to match.
var detectedBodyType = requestMessage.BodyData?.DetectedBodyType;
if (detectedBodyType is BodyType.Bytes or BodyType.String)
if (detectedBodyType is BodyType.Bytes or BodyType.String or BodyType.FormUrlEncoded)
{
return exactObjectMatcher.IsMatch(requestMessage.BodyData?.BodyAsBytes);
}
@@ -184,7 +198,7 @@ public class RequestMessageBodyMatcher : IRequestMatcher
if (matcher is IStringMatcher stringMatcher)
{
// If the body is a Json or a String, use the BodyAsString to match on.
if (requestMessage?.BodyData?.DetectedBodyType == BodyType.Json || requestMessage?.BodyData?.DetectedBodyType == BodyType.String)
if (requestMessage?.BodyData?.DetectedBodyType is BodyType.Json or BodyType.String or BodyType.FormUrlEncoded)
{
return stringMatcher.IsMatch(requestMessage.BodyData.BodyAsString);
}
@@ -206,6 +220,11 @@ public class RequestMessageBodyMatcher : IRequestMatcher
return MatchScores.ToScore(Func(requestMessage.BodyData?.BodyAsString));
}
if (FormUrlEncodedFunc != null)
{
return MatchScores.ToScore(FormUrlEncodedFunc(requestMessage.BodyData?.BodyAsFormUrlEncoded));
}
if (JsonFunc != null)
{
return MatchScores.ToScore(JsonFunc(requestMessage.BodyData?.BodyAsJson));

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text;
using WireMock.Types;
@@ -14,6 +15,9 @@ public class BodyData : IBodyData
/// <inheritdoc />
public string? BodyAsString { get; set; }
/// <inheritdoc />
public IDictionary<string, string>? BodyAsFormUrlEncoded { get; set; }
/// <inheritdoc cref="IBodyData.BodyAsJson" />
public object? BodyAsJson { get; set; }

View File

@@ -1,19 +1,18 @@
using System;
namespace WireMock.Models
namespace WireMock.Models;
/// <summary>
/// TimeSettingsModel: Start, End and TTL
/// </summary>
public class TimeSettings : ITimeSettings
{
/// <summary>
/// TimeSettingsModel: Start, End and TTL
/// </summary>
public class TimeSettings : ITimeSettings
{
/// <inheritdoc />
public DateTime? Start { get; set; }
/// <inheritdoc />
public DateTime? Start { get; set; }
/// <inheritdoc />
public DateTime? End { get; set; }
/// <inheritdoc />
public DateTime? End { get; set; }
/// <inheritdoc />
public int? TTL { get; set; }
}
/// <inheritdoc />
public int? TTL { get; set; }
}

View File

@@ -1,51 +1,47 @@
using System;
using System;
using Stef.Validation;
namespace WireMock.Models
namespace WireMock.Models;
/// <summary>
/// UrlDetails
/// </summary>
public class UrlDetails
{
/// <summary>
/// UrlDetails
/// Gets the url (relative).
/// </summary>
public class UrlDetails
public Uri Url { get; }
/// <summary>
/// Gets the AbsoluteUrl.
/// </summary>
public Uri AbsoluteUrl { get; }
/// <summary>
/// Initializes a new instance of the <see cref="UrlDetails"/> class.
/// </summary>
/// <param name="url">The URL.</param>
public UrlDetails(string url) : this(new Uri(url))
{
/// <summary>
/// Gets the url (relative).
/// </summary>
public Uri Url { get; }
}
/// <summary>
/// Gets the AbsoluteUrl.
/// </summary>
public Uri AbsoluteUrl { get; }
/// <summary>
/// Initializes a new instance of the <see cref="UrlDetails"/> class.
/// </summary>
/// <param name="url">The URL.</param>
public UrlDetails(Uri url) : this(url, url)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UrlDetails"/> class.
/// </summary>
/// <param name="url">The URL.</param>
public UrlDetails(string url) : this(new Uri(url))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UrlDetails"/> class.
/// </summary>
/// <param name="url">The URL.</param>
public UrlDetails(Uri url) : this(url, url)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UrlDetails"/> class.
/// </summary>
/// <param name="absoluteUrl">The absolute URL.</param>
/// <param name="url">The URL (relative).</param>
public UrlDetails(Uri absoluteUrl, Uri url)
{
Guard.NotNull(absoluteUrl, nameof(absoluteUrl));
Guard.NotNull(url, nameof(url));
AbsoluteUrl = absoluteUrl;
Url = url;
}
/// <summary>
/// Initializes a new instance of the <see cref="UrlDetails"/> class.
/// </summary>
/// <param name="absoluteUrl">The absolute URL.</param>
/// <param name="url">The URL (relative).</param>
public UrlDetails(Uri absoluteUrl, Uri url)
{
AbsoluteUrl = Guard.NotNull(absoluteUrl);
Url = Guard.NotNull(url);
}
}

View File

@@ -130,6 +130,7 @@ namespace WireMock.Owin.Mappers
switch (responseMessage.BodyData?.DetectedBodyType)
{
case BodyType.String:
case BodyType.FormUrlEncoded:
return (responseMessage.BodyData.Encoding ?? _utf8NoBom).GetBytes(responseMessage.BodyData.BodyAsString!);
case BodyType.Json:

View File

@@ -45,8 +45,16 @@ internal class ProxyHelper
// Create ResponseMessage
bool deserializeJson = !_settings.DisableJsonBodyParsing.GetValueOrDefault(false);
bool decompressGzipAndDeflate = !_settings.DisableRequestBodyDecompressing.GetValueOrDefault(false);
bool deserializeFormUrlEncoded = !_settings.DisableDeserializeFormUrlEncoded.GetValueOrDefault(false);
var responseMessage = await HttpResponseMessageHelper.CreateAsync(httpResponseMessage, requiredUri, originalUri, deserializeJson, decompressGzipAndDeflate).ConfigureAwait(false);
var responseMessage = await HttpResponseMessageHelper.CreateAsync(
httpResponseMessage,
requiredUri,
originalUri,
deserializeJson,
decompressGzipAndDeflate,
deserializeFormUrlEncoded
).ConfigureAwait(false);
IMapping? newMapping = null;
@@ -56,11 +64,11 @@ internal class ProxyHelper
if (saveMappingSettings != null)
{
save &= Check(saveMappingSettings.StatusCodePattern,
() => HttpStatusRangeParser.IsMatch(saveMappingSettings.StatusCodePattern, responseMessage.StatusCode)
() => saveMappingSettings.StatusCodePattern != null && HttpStatusRangeParser.IsMatch(saveMappingSettings.StatusCodePattern, responseMessage.StatusCode)
);
save &= Check(saveMappingSettings.HttpMethods,
() => saveMappingSettings.HttpMethods.Value.Contains(requestMessage.Method, StringComparer.OrdinalIgnoreCase)
() => saveMappingSettings.HttpMethods != null && saveMappingSettings.HttpMethods.Value.Contains(requestMessage.Method, StringComparer.OrdinalIgnoreCase)
);
}

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using JsonConverter.Abstractions;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Util;
@@ -45,35 +47,60 @@ public interface IBodyRequestBuilder : IRequestMatcher
/// WithBody: Body as object
/// </summary>
/// <param name="body">The body.</param>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="matchBehaviour">The match behaviour [default is AcceptOnMatch].</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(object body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBody : Body as a string response based on a object (which will be converted to a JSON string using NewtonSoft.Json).
/// </summary>
/// <param name="body">The body.</param>
/// <param name="matchBehaviour">The match behaviour [default is AcceptOnMatch].</param>
/// <returns>A <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsJson(object body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBody : Body as a string response based on a object (which will be converted to a JSON string using the <see cref="IJsonConverter"/>).
/// </summary>
/// <param name="body">The body.</param>
/// <param name="converter">The JsonConverter.</param>
/// <param name="options">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <param name="matchBehaviour">The match behaviour [default is AcceptOnMatch].</param>
/// <returns>A <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsJson(object body, IJsonConverter converter, JsonConverterOptions? options = null, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBody: func (string)
/// </summary>
/// <param name="func">The function.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<string, bool> func);
IRequestBuilder WithBody(Func<string?, bool> func);
/// <summary>
/// WithBody: func (byte[])
/// </summary>
/// <param name="func">The function.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<byte[], bool> func);
IRequestBuilder WithBody(Func<byte[]?, bool> func);
/// <summary>
/// WithBody: func (json object)
/// </summary>
/// <param name="func">The function.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<object, bool> func);
IRequestBuilder WithBody(Func<object?, bool> func);
/// <summary>
/// WithBody: func (BodyData object)
/// </summary>
/// <param name="func">The function.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<IBodyData, bool> func);
IRequestBuilder WithBody(Func<IBodyData?, bool> func);
/// <summary>
/// WithBody: Body as form-urlencoded values.
/// </summary>
/// <param name="func">The form-urlencoded values.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<IDictionary<string, string>?, bool> func);
}

View File

@@ -1,36 +1,57 @@
// This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License.
// For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root.
using System;
using System.Collections.Generic;
using JsonConverter.Abstractions;
using Newtonsoft.Json;
using Stef.Validation;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Util;
using Stef.Validation;
namespace WireMock.RequestBuilders;
public partial class Request
{
/// <inheritdoc cref="IBodyRequestBuilder.WithBody(string, MatchBehaviour)"/>
/// <inheritdoc />
public IRequestBuilder WithBody(string body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageBodyMatcher(matchBehaviour, body));
return this;
}
/// <inheritdoc cref="IBodyRequestBuilder.WithBody(byte[], MatchBehaviour)"/>
/// <inheritdoc />
public IRequestBuilder WithBody(byte[] body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageBodyMatcher(matchBehaviour, body));
return this;
}
/// <inheritdoc cref="IBodyRequestBuilder.WithBody(object, MatchBehaviour)"/>
/// <inheritdoc />
public IRequestBuilder WithBody(object body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageBodyMatcher(matchBehaviour, body));
return this;
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsJson(object body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
var bodyAsJsonString = JsonConvert.SerializeObject(body);
_requestMatchers.Add(new RequestMessageBodyMatcher(matchBehaviour, bodyAsJsonString));
return this;
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsJson(object body, IJsonConverter converter, JsonConverterOptions? options = null, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
Guard.NotNull(converter);
var bodyAsJsonString = converter.Serialize(body, options);
_requestMatchers.Add(new RequestMessageBodyMatcher(matchBehaviour, bodyAsJsonString));
return this;
}
/// <inheritdoc />
public IRequestBuilder WithBody(IMatcher matcher)
{
@@ -46,39 +67,46 @@ public partial class Request
return this;
}
/// <inheritdoc cref="IBodyRequestBuilder.WithBody(Func{string, bool})"/>
public IRequestBuilder WithBody(Func<string, bool> func)
/// <inheritdoc />
public IRequestBuilder WithBody(Func<string?, bool> func)
{
Guard.NotNull(func, nameof(func));
Guard.NotNull(func);
_requestMatchers.Add(new RequestMessageBodyMatcher(func));
return this;
}
/// <inheritdoc cref="IBodyRequestBuilder.WithBody(Func{byte[], bool})"/>
public IRequestBuilder WithBody(Func<byte[], bool> func)
/// <inheritdoc />
public IRequestBuilder WithBody(Func<byte[]?, bool> func)
{
Guard.NotNull(func, nameof(func));
Guard.NotNull(func);
_requestMatchers.Add(new RequestMessageBodyMatcher(func));
return this;
}
/// <inheritdoc cref="IBodyRequestBuilder.WithBody(Func{object, bool})"/>
public IRequestBuilder WithBody(Func<object, bool> func)
/// <inheritdoc />
public IRequestBuilder WithBody(Func<object?, bool> func)
{
Guard.NotNull(func, nameof(func));
Guard.NotNull(func);
_requestMatchers.Add(new RequestMessageBodyMatcher(func));
return this;
}
/// <inheritdoc cref="IBodyRequestBuilder.WithBody(Func{IBodyData, bool})"/>
public IRequestBuilder WithBody(Func<IBodyData, bool> func)
/// <inheritdoc />
public IRequestBuilder WithBody(Func<IBodyData?, bool> func)
{
Guard.NotNull(func, nameof(func));
Guard.NotNull(func);
_requestMatchers.Add(new RequestMessageBodyMatcher(func));
return this;
}
/// <inheritdoc />
public IRequestBuilder WithBody(Func<IDictionary<string, string>?, bool> func)
{
_requestMatchers.Add(new RequestMessageBodyMatcher(Guard.NotNull(func)));
return this;
}
}

View File

@@ -92,7 +92,7 @@ public interface IBodyResponseBuilder : IFaultResponseBuilder
/// </summary>
/// <param name="body">The body.</param>
/// <param name="converter">The JsonConverter.</param>
/// <param name="options">The IJsonConverterOption [optional].</param>
/// <param name="options">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithBody(object body, IJsonConverter converter, JsonConverterOptions? options = null);
@@ -102,7 +102,7 @@ public interface IBodyResponseBuilder : IFaultResponseBuilder
/// <param name="body">The body.</param>
/// <param name="encoding">The body encoding, can be <c>null</c>.</param>
/// <param name="converter">The JsonConverter.</param>
/// <param name="options">The IJsonConverterOption [optional].</param>
/// <param name="options">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithBody(object body, Encoding? encoding, IJsonConverter converter, JsonConverterOptions? options = null);
}

View File

@@ -27,7 +27,7 @@ public interface IProxyResponseBuilder : IStatusCodeResponseBuilder
/// WithProxy using <see cref="X509Certificate2"/>.
/// </summary>
/// <param name="proxyUrl">The proxy url.</param>
/// <param name="certificate"">The X509Certificate2.</param>
/// <param name="certificate">The X509Certificate2.</param>
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithProxy(string proxyUrl, X509Certificate2 certificate);
}

View File

@@ -44,6 +44,7 @@ internal class LogEntryMapper
switch (logEntry.RequestMessage.BodyData.DetectedBodyType)
{
case BodyType.String:
case BodyType.FormUrlEncoded:
logRequestModel.Body = logEntry.RequestMessage.BodyData.BodyAsString;
break;
@@ -120,6 +121,7 @@ internal class LogEntryMapper
switch (logEntry.ResponseMessage.BodyData!.DetectedBodyType)
{
case BodyType.String:
case BodyType.FormUrlEncoded:
if (!string.IsNullOrEmpty(logEntry.ResponseMessage.BodyData.IsFuncUsed) && _options.DoNotSaveDynamicResponseInLogEntry == true)
{
logResponseModel.Body = logEntry.ResponseMessage.BodyData.IsFuncUsed;
@@ -142,6 +144,9 @@ internal class LogEntryMapper
logResponseModel.BodyAsFile = logEntry.ResponseMessage.BodyData.BodyAsFile;
logResponseModel.BodyAsFileIsCached = logEntry.ResponseMessage.BodyData.BodyAsFileIsCached;
break;
default:
break;
}
}

View File

@@ -131,6 +131,7 @@ internal class MappingConverter
switch (response.ResponseMessage.BodyData.DetectedBodyType)
{
case BodyType.String:
case BodyType.FormUrlEncoded:
sb.AppendLine($" .WithBody(\"{response.ResponseMessage.BodyData.BodyAsString}\")");
break;
}
@@ -140,7 +141,7 @@ internal class MappingConverter
{
sb.AppendLine($" .WithDelay({response.Delay.Value.TotalMilliseconds})");
}
else if (response.MinimumDelayMilliseconds > 0 && response.MaximumDelayMilliseconds > 0)
else if (response is { MinimumDelayMilliseconds: > 0, MaximumDelayMilliseconds: > 0 })
{
sb.AppendLine($" .WithRandomDelay({response.MinimumDelayMilliseconds}, {response.MaximumDelayMilliseconds})");
}
@@ -325,6 +326,7 @@ internal class MappingConverter
switch (response.ResponseMessage.BodyData?.DetectedBodyType)
{
case BodyType.String:
case BodyType.FormUrlEncoded:
mappingModel.Response.Body = response.ResponseMessage.BodyData.BodyAsString;
break;

View File

@@ -31,6 +31,7 @@ internal class ProxyMappingConverter
var useDefinedRequestMatchers = proxyAndRecordSettings.UseDefinedRequestMatchers;
var excludedHeaders = new List<string>(proxyAndRecordSettings.ExcludedHeaders ?? new string[] { }) { "Cookie" };
var excludedCookies = proxyAndRecordSettings.ExcludedCookies ?? new string[0];
var excludedParams = proxyAndRecordSettings.ExcludedParams ?? new string[0];
var request = (Request?)mapping?.RequestMatcher;
var clientIPMatcher = request?.GetRequestMessageMatcher<RequestMessageClientIPMatcher>();
@@ -74,12 +75,21 @@ internal class ProxyMappingConverter
{
foreach (var paramMatcher in paramMatchers)
{
newRequest.WithParam(paramMatcher.Key, paramMatcher.MatchBehaviour, paramMatcher.Matchers!.ToArray());
if (!excludedParams.Contains(paramMatcher.Key, StringComparer.OrdinalIgnoreCase))
{
newRequest.WithParam(paramMatcher.Key, paramMatcher.MatchBehaviour, paramMatcher.Matchers!.ToArray());
}
}
}
else
{
requestMessage.Query?.Loop((key, value) => newRequest.WithParam(key, false, value.ToArray()));
requestMessage.Query?.Loop((key, value) =>
{
if (!excludedParams.Contains(key, StringComparer.OrdinalIgnoreCase))
{
newRequest.WithParam(key, false, value.ToArray());
}
});
}
// Cookies
@@ -141,6 +151,7 @@ internal class ProxyMappingConverter
break;
case BodyType.String:
case BodyType.FormUrlEncoded:
newRequest.WithBody(new ExactMatcher(MatchBehaviour.AcceptOnMatch, true, throwExceptionWhenMatcherFails, MatchOperator.Or, requestMessage.BodyData.BodyAsString!));
break;

View File

@@ -54,10 +54,12 @@ internal static class SwaggerMapper
{
operation.Parameters.Add(openApiParameter);
}
foreach (var openApiParameter in MapRequestHeaders(mapping.Request.Headers))
{
operation.Parameters.Add(openApiParameter);
}
foreach (var openApiParameter in MapRequestCookies(mapping.Request.Cookies))
{
operation.Parameters.Add(openApiParameter);
@@ -94,7 +96,7 @@ internal static class SwaggerMapper
return openApiDocument.ToJson(SchemaType.OpenApi3, Formatting.Indented);
}
private static IEnumerable<OpenApiParameter> MapRequestQueryParameters(IList<ParamModel>? queryParameters)
private static IReadOnlyList<OpenApiParameter> MapRequestQueryParameters(IList<ParamModel>? queryParameters)
{
if (queryParameters == null)
{
@@ -146,7 +148,7 @@ internal static class SwaggerMapper
.ToList();
}
private static IEnumerable<OpenApiParameter> MapRequestCookies(IList<CookieModel>? cookies)
private static IReadOnlyList<OpenApiParameter> MapRequestCookies(IList<CookieModel>? cookies)
{
if (cookies == null)
{

View File

@@ -97,6 +97,7 @@ internal static class WebhookMapper
switch (webhook.Request.BodyData.DetectedBodyType)
{
case BodyType.String:
case BodyType.FormUrlEncoded:
model.Request.Body = webhook.Request.BodyData.BodyAsString;
break;

View File

@@ -37,6 +37,8 @@ public partial class WireMockServer
private const string AdminRequests = "/__admin/requests";
private const string AdminSettings = "/__admin/settings";
private const string AdminScenarios = "/__admin/scenarios";
private const string AdminOpenApi = "/__admin/openapi";
private const string QueryParamReloadStaticMappings = "reloadStaticMappings";
private static readonly Guid ProxyMappingGuid = new("e59914fd-782e-428e-91c1-4810ffb86567");
@@ -113,6 +115,10 @@ public partial class WireMockServer
Given(Request.Create().WithPath(_adminFilesFilenamePathMatcher).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(FileGet));
Given(Request.Create().WithPath(_adminFilesFilenamePathMatcher).UsingHead()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(FileHead));
Given(Request.Create().WithPath(_adminFilesFilenamePathMatcher).UsingDelete()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(FileDelete));
// __admin/openapi
Given(Request.Create().WithPath($"{AdminOpenApi}/convert").UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(OpenApiConvertToMappings));
Given(Request.Create().WithPath($"{AdminOpenApi}/save").UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(OpenApiSaveToMappings));
}
#endregion
@@ -217,10 +223,17 @@ public partial class WireMockServer
var model = new SettingsModel
{
AllowBodyForAllHttpMethods = _settings.AllowBodyForAllHttpMethods,
AllowOnlyDefinedHttpStatusCodeInResponse = _settings.AllowOnlyDefinedHttpStatusCodeInResponse,
AllowPartialMapping = _settings.AllowPartialMapping,
DisableDeserializeFormUrlEncoded = _settings.DisableDeserializeFormUrlEncoded,
DisableJsonBodyParsing = _settings.DisableJsonBodyParsing,
DisableRequestBodyDecompressing = _settings.DisableRequestBodyDecompressing,
DoNotSaveDynamicResponseInLogEntry = _settings.DoNotSaveDynamicResponseInLogEntry,
GlobalProcessingDelay = (int?)_options.RequestProcessingDelay?.TotalMilliseconds,
HandleRequestsSynchronously = _settings.HandleRequestsSynchronously,
HostingScheme = _settings.HostingScheme,
MaxRequestLogCount = _settings.MaxRequestLogCount,
QueryParameterMultipleValueSupport = _settings.QueryParameterMultipleValueSupport,
ReadStaticMappings = _settings.ReadStaticMappings,
RequestLogExpirationDuration = _settings.RequestLogExpirationDuration,
SaveUnmatchedRequests = _settings.SaveUnmatchedRequests,
@@ -228,14 +241,11 @@ public partial class WireMockServer
UseRegexExtended = _settings.UseRegexExtended,
WatchStaticMappings = _settings.WatchStaticMappings,
WatchStaticMappingsInSubdirectories = _settings.WatchStaticMappingsInSubdirectories,
HostingScheme = _settings.HostingScheme,
DoNotSaveDynamicResponseInLogEntry = _settings.DoNotSaveDynamicResponseInLogEntry,
QueryParameterMultipleValueSupport = _settings.QueryParameterMultipleValueSupport,
#if USE_ASPNETCORE
CorsPolicyOptions = _settings.CorsPolicyOptions?.ToString(),
AcceptAnyClientCertificate = _settings.AcceptAnyClientCertificate,
ClientCertificateMode = _settings.ClientCertificateMode,
AcceptAnyClientCertificate = _settings.AcceptAnyClientCertificate
CorsPolicyOptions = _settings.CorsPolicyOptions?.ToString()
#endif
};
@@ -250,10 +260,16 @@ public partial class WireMockServer
// _settings
_settings.AllowBodyForAllHttpMethods = settings.AllowBodyForAllHttpMethods;
_settings.AllowOnlyDefinedHttpStatusCodeInResponse = settings.AllowOnlyDefinedHttpStatusCodeInResponse;
_settings.AllowPartialMapping = settings.AllowPartialMapping;
_settings.DisableDeserializeFormUrlEncoded = settings.DisableDeserializeFormUrlEncoded;
_settings.DisableJsonBodyParsing = settings.DisableJsonBodyParsing;
_settings.DisableRequestBodyDecompressing = settings.DisableRequestBodyDecompressing;
_settings.DoNotSaveDynamicResponseInLogEntry = settings.DoNotSaveDynamicResponseInLogEntry;
_settings.HandleRequestsSynchronously = settings.HandleRequestsSynchronously;
_settings.MaxRequestLogCount = settings.MaxRequestLogCount;
_settings.ProxyAndRecordSettings = TinyMapperUtils.Instance.Map(settings.ProxyAndRecordSettings);
_settings.QueryParameterMultipleValueSupport = settings.QueryParameterMultipleValueSupport;
_settings.ReadStaticMappings = settings.ReadStaticMappings;
_settings.RequestLogExpirationDuration = settings.RequestLogExpirationDuration;
_settings.SaveUnmatchedRequests = settings.SaveUnmatchedRequests;
@@ -261,8 +277,6 @@ public partial class WireMockServer
_settings.UseRegexExtended = settings.UseRegexExtended;
_settings.WatchStaticMappings = settings.WatchStaticMappings;
_settings.WatchStaticMappingsInSubdirectories = settings.WatchStaticMappingsInSubdirectories;
_settings.DoNotSaveDynamicResponseInLogEntry = settings.DoNotSaveDynamicResponseInLogEntry;
_settings.QueryParameterMultipleValueSupport = settings.QueryParameterMultipleValueSupport;
InitSettings(_settings);
@@ -729,7 +743,7 @@ public partial class WireMockServer
return encodingModel != null ? Encoding.GetEncoding(encodingModel.CodePage) : null;
}
private static ResponseMessage ToJson<T>(T result, bool keepNullValues = false)
private static ResponseMessage ToJson<T>(T result, bool keepNullValues = false, object? statusCode = null)
{
return new ResponseMessage
{
@@ -738,7 +752,7 @@ public partial class WireMockServer
DetectedBodyType = BodyType.String,
BodyAsString = JsonConvert.SerializeObject(result, keepNullValues ? JsonSerializationConstants.JsonSerializerSettingsIncludeNullValues : JsonSerializationConstants.JsonSerializerSettingsDefault)
},
StatusCode = (int)HttpStatusCode.OK,
StatusCode = statusCode ?? (int)HttpStatusCode.OK,
Headers = new Dictionary<string, WireMockList<string>> { { HttpKnownHeaderNames.ContentType, new WireMockList<string>(WireMockConstants.ContentTypeJson) } }
};
}
@@ -759,14 +773,18 @@ public partial class WireMockServer
private static T DeserializeObject<T>(IRequestMessage requestMessage) where T : new()
{
return requestMessage.BodyData?.DetectedBodyType switch
switch (requestMessage.BodyData?.DetectedBodyType)
{
BodyType.String => JsonUtils.DeserializeObject<T>(requestMessage.BodyData.BodyAsString!),
case BodyType.String:
case BodyType.FormUrlEncoded:
return JsonUtils.DeserializeObject<T>(requestMessage.BodyData.BodyAsString!);
BodyType.Json when requestMessage.BodyData?.BodyAsJson != null => ((JObject)requestMessage.BodyData.BodyAsJson).ToObject<T>()!,
case BodyType.Json when requestMessage.BodyData?.BodyAsJson != null:
return ((JObject)requestMessage.BodyData.BodyAsJson).ToObject<T>()!;
_ => throw new NotSupportedException()
};
default:
throw new NotSupportedException();
}
}
private static T[] DeserializeRequestMessageToArray<T>(IRequestMessage requestMessage)

View File

@@ -65,7 +65,7 @@ namespace WireMock.Server
}
};
if (BytesEncodingUtils.TryGetEncoding(bytes, out Encoding encoding) && FileBodyIsString.Select(x => x.Equals(encoding)).Any())
if (BytesEncodingUtils.TryGetEncoding(bytes, out var encoding) && FileBodyIsString.Select(x => x.Equals(encoding)).Any())
{
response.BodyData.DetectedBodyType = BodyType.String;
response.BodyData.BodyAsString = encoding.GetString(bytes);

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Stef.Validation;
using WireMock.Admin.Mappings;
@@ -14,7 +15,7 @@ namespace WireMock.Server;
public partial class WireMockServer
{
private void ConvertMappingsAndRegisterAsRespondProvider(MappingModel[] mappingModels, string? path = null)
private void ConvertMappingsAndRegisterAsRespondProvider(IReadOnlyList<MappingModel> mappingModels, string? path = null)
{
var duplicateGuids = mappingModels
.Where(m => m.Guid != null)
@@ -46,7 +47,7 @@ public partial class WireMockServer
}
var respondProvider = Given(requestBuilder, mappingModel.SaveToFile == true);
if (guid != null)
{
respondProvider = respondProvider.WithGuid(guid.Value);
@@ -107,7 +108,10 @@ public partial class WireMockServer
respondProvider = respondProvider.WithWebhook(webhooks);
}
respondProvider.WithWebhookFireAndForget(mappingModel.UseWebhooksFireAndForget ?? false);
if (mappingModel.UseWebhooksFireAndForget == true)
{
respondProvider.WithWebhookFireAndForget(mappingModel.UseWebhooksFireAndForget.Value);
}
var responseBuilder = InitResponseBuilder(mappingModel.Response);
respondProvider.RespondWith(responseBuilder);
@@ -205,11 +209,11 @@ public partial class WireMockServer
{
foreach (var cookieModel in requestModel.Cookies.Where(c => c.Matchers != null))
{
requestBuilder = requestBuilder.WithCookie(
cookieModel.Name,
cookieModel.IgnoreCase == true,
cookieModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch,
cookieModel.Matchers!.Select(_matcherMapper.Map).OfType<IStringMatcher>().ToArray());
requestBuilder = requestBuilder.WithCookie(
cookieModel.Name,
cookieModel.IgnoreCase == true,
cookieModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch,
cookieModel.Matchers!.Select(_matcherMapper.Map).OfType<IStringMatcher>().ToArray());
}
}

View File

@@ -0,0 +1,54 @@
#if OPENAPIPARSER
using System;
using System.Linq;
using System.Net;
using WireMock.Net.OpenApiParser;
#endif
namespace WireMock.Server;
public partial class WireMockServer
{
private IResponseMessage OpenApiConvertToMappings(IRequestMessage requestMessage)
{
#if OPENAPIPARSER
try
{
var mappingModels = new WireMockOpenApiParser().FromText(requestMessage.Body, out var diagnostic);
return diagnostic.Errors.Any() ? ToJson(diagnostic, false, HttpStatusCode.BadRequest) : ToJson(mappingModels);
}
catch (Exception e)
{
_settings.Logger.Error("HttpStatusCode set to {0} {1}", HttpStatusCode.BadRequest, e);
return ResponseMessageBuilder.Create(e.Message, HttpStatusCode.BadRequest);
}
#else
return ResponseMessageBuilder.Create("Not supported for .NETStandard 1.3 and .NET 4.5.2 or lower.", 400);
#endif
}
private IResponseMessage OpenApiSaveToMappings(IRequestMessage requestMessage)
{
#if OPENAPIPARSER
try
{
var mappingModels = new WireMockOpenApiParser().FromText(requestMessage.Body, out var diagnostic);
if (diagnostic.Errors.Any())
{
return ToJson(diagnostic, false, HttpStatusCode.BadRequest);
}
ConvertMappingsAndRegisterAsRespondProvider(mappingModels);
return ResponseMessageBuilder.Create("OpenApi document converted to Mappings", HttpStatusCode.Created);
}
catch (Exception e)
{
_settings.Logger.Error("HttpStatusCode set to {0} {1}", HttpStatusCode.BadRequest, e);
return ResponseMessageBuilder.Create(e.Message, HttpStatusCode.BadRequest);
}
#else
return ResponseMessageBuilder.Create("Not supported for .NETStandard 1.3 and .NET 4.5.2 or lower.", 400);
#endif
}
}

View File

@@ -57,6 +57,12 @@ public class ProxyAndRecordSettings : HttpClientSettings
[PublicAPI]
public string[]? ExcludedHeaders { get; set; }
/// <summary>
/// Defines a list of params which will be excluded from the saved mappings.
/// </summary>
[PublicAPI]
public string[]? ExcludedParams { get; set; }
/// <summary>
/// Defines a list of cookies which will be excluded from the saved mappings.
/// </summary>

View File

@@ -6,7 +6,7 @@ public class ProxySaveMappingSetting<T>
{
public MatchBehaviour MatchBehaviour { get; } = MatchBehaviour.AcceptOnMatch;
public T Value { get; private set; }
public T Value { get; }
public ProxySaveMappingSetting(T value, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
@@ -14,7 +14,7 @@ public class ProxySaveMappingSetting<T>
MatchBehaviour = matchBehaviour;
}
public static implicit operator ProxySaveMappingSetting<T>(T value) => new ProxySaveMappingSetting<T>(value);
public static implicit operator ProxySaveMappingSetting<T>(T value) => new(value);
public static implicit operator T(ProxySaveMappingSetting<T> @this) => @this.Value;
}

View File

@@ -32,7 +32,7 @@ internal class SimpleCommandLineParser
}
else if (string.IsNullOrEmpty(currentName))
{
Arguments[arg] = new string[0];
Arguments[arg] = EmptyArray<string>.Value;
}
else
{

View File

@@ -182,13 +182,13 @@ public class WireMockServerSettings
public bool? AllowCSharpCodeMatcher { get; set; }
/// <summary>
/// Allow a Body for all HTTP Methods. (default set to false).
/// Allow a Body for all HTTP Methods. (default set to <c>false</c>).
/// </summary>
[PublicAPI]
public bool? AllowBodyForAllHttpMethods { get; set; }
/// <summary>
/// Allow only a HttpStatus Code in the response which is defined. (default set to false).
/// Allow only a HttpStatus Code in the response which is defined. (default set to <c>false</c>).
/// - false : also null, 0, empty or invalid HttpStatus codes are allowed.
/// - true : only codes defined in <see cref="System.Net.HttpStatusCode"/> are allowed.
/// </summary>
@@ -196,25 +196,31 @@ public class WireMockServerSettings
public bool? AllowOnlyDefinedHttpStatusCodeInResponse { get; set; }
/// <summary>
/// Set to true to disable Json deserialization when processing requests. (default set to false).
/// Set to true to disable Json deserialization when processing requests. (default set to <c>false</c>).
/// </summary>
[PublicAPI]
public bool? DisableJsonBodyParsing { get; set; }
/// <summary>
/// Disable support for GZip and Deflate request body decompression. (default set to false).
/// Disable support for GZip and Deflate request body decompression. (default set to <c>false</c>).
/// </summary>
[PublicAPI]
public bool? DisableRequestBodyDecompressing { get; set; }
/// <summary>
/// Handle all requests synchronously. (default set to false).
/// Set to true to disable FormUrlEncoded deserializing when processing requests. (default set to <c>false</c>).
/// </summary>
[PublicAPI]
public bool? DisableDeserializeFormUrlEncoded { get; set; }
/// <summary>
/// Handle all requests synchronously. (default set to <c>false</c>).
/// </summary>
[PublicAPI]
public bool? HandleRequestsSynchronously { get; set; }
/// <summary>
/// Throw an exception when the <see cref="IMatcher"/> fails because of invalid input. (default set to false).
/// Throw an exception when the <see cref="IMatcher"/> fails because of invalid input. (default set to <c>false</c>).
/// </summary>
[PublicAPI]
public bool? ThrowExceptionWhenMatcherFails { get; set; }
@@ -255,19 +261,19 @@ public class WireMockServerSettings
public WebhookSettings? WebhookSettings { get; set; }
/// <summary>
/// Use the <see cref="RegexExtended"/> instead of the default <see cref="Regex"/> (default set to true).
/// Use the <see cref="RegexExtended"/> instead of the default <see cref="Regex"/> (default set to <c>true</c>).
/// </summary>
[PublicAPI]
public bool? UseRegexExtended { get; set; } = true;
/// <summary>
/// Save unmatched requests to a file using the <see cref="IFileSystemHandler"/> (default set to false).
/// Save unmatched requests to a file using the <see cref="IFileSystemHandler"/> (default set to <c>false</c>).
/// </summary>
[PublicAPI]
public bool? SaveUnmatchedRequests { get; set; }
/// <summary>
/// Don't save the response-string in the LogEntry when WithBody(Func{IRequestMessage, string}) or WithBody(Func{IRequestMessage, Task{string}}) is used. (default set to false).
/// Don't save the response-string in the LogEntry when WithBody(Func{IRequestMessage, string}) or WithBody(Func{IRequestMessage, Task{string}}) is used. (default set to <c>false</c>).
/// </summary>
[PublicAPI]
public bool? DoNotSaveDynamicResponseInLogEntry { get; set; }

View File

@@ -4,7 +4,6 @@ using System.Linq;
using JetBrains.Annotations;
using Stef.Validation;
using WireMock.Logging;
using WireMock.Matchers;
using WireMock.Types;
using WireMock.Util;
@@ -47,6 +46,8 @@ public static class WireMockServerSettingsParser
AllowOnlyDefinedHttpStatusCodeInResponse = parser.GetBoolValue("AllowOnlyDefinedHttpStatusCodeInResponse"),
AllowPartialMapping = parser.GetBoolValue("AllowPartialMapping"),
DisableJsonBodyParsing = parser.GetBoolValue("DisableJsonBodyParsing"),
DisableRequestBodyDecompressing = parser.GetBoolValue(nameof(WireMockServerSettings.DisableRequestBodyDecompressing)),
DisableDeserializeFormUrlEncoded = parser.GetBoolValue(nameof(WireMockServerSettings.DisableDeserializeFormUrlEncoded)),
HandleRequestsSynchronously = parser.GetBoolValue("HandleRequestsSynchronously"),
MaxRequestLogCount = parser.GetIntValue("MaxRequestLogCount"),
ReadStaticMappings = parser.GetBoolValue("ReadStaticMappings"),

View File

@@ -85,7 +85,7 @@ internal class Transformer : ITransformer
{
responseMessage.BodyData = TransformBodyData(transformerContext, options, model, original.BodyData, useTransformerForBodyAsFile);
if (original.BodyData.DetectedBodyType == BodyType.String)
if (original.BodyData.DetectedBodyType is BodyType.String or BodyType.FormUrlEncoded)
{
responseMessage.BodyOriginal = original.BodyData.BodyAsString;
}
@@ -123,13 +123,21 @@ internal class Transformer : ITransformer
private IBodyData? TransformBodyData(ITransformerContext transformerContext, ReplaceNodeOptions options, TransformModel model, IBodyData original, bool useTransformerForBodyAsFile)
{
return original.DetectedBodyType switch
switch (original.DetectedBodyType)
{
BodyType.Json => TransformBodyAsJson(transformerContext, options, model, original),
BodyType.File => TransformBodyAsFile(transformerContext, model, original, useTransformerForBodyAsFile),
BodyType.String => TransformBodyAsString(transformerContext, model, original),
_ => null
};
case BodyType.Json:
return TransformBodyAsJson(transformerContext, options, model, original);
case BodyType.File:
return TransformBodyAsFile(transformerContext, model, original, useTransformerForBodyAsFile);
case BodyType.String:
case BodyType.FormUrlEncoded:
return TransformBodyAsString(transformerContext, model, original);
default:
return null;
}
}
private static IDictionary<string, WireMockList<string>> TransformHeaders(ITransformerContext transformerContext, TransformModel model, IDictionary<string, WireMockList<string>>? original)

View File

@@ -49,12 +49,14 @@ internal static class BodyParser
new WildcardMatcher("application/vnd.*+json", true)
};
private static readonly IStringMatcher FormUrlEncodedMatcher = new WildcardMatcher("application/x-www-form-urlencoded", true);
private static readonly IStringMatcher[] TextContentTypeMatchers =
{
new WildcardMatcher("text/*", true),
new RegexMatcher("^application\\/(java|type)script$", true),
new WildcardMatcher("application/*xml", true),
new WildcardMatcher("application/x-www-form-urlencoded", true)
FormUrlEncodedMatcher
};
public static bool ShouldParseBody(string? httpMethod, bool allowBodyForAllHttpMethods)
@@ -69,7 +71,7 @@ internal static class BodyParser
return true;
}
if (BodyAllowedForMethods.TryGetValue(httpMethod!.ToUpper(), out bool allowed))
if (BodyAllowedForMethods.TryGetValue(httpMethod!.ToUpper(), out var allowed))
{
return allowed;
}
@@ -88,6 +90,11 @@ internal static class BodyParser
return BodyType.Bytes;
}
if (MatchScores.IsPerfect(FormUrlEncodedMatcher.IsMatch(contentType.MediaType)))
{
return BodyType.FormUrlEncoded;
}
if (TextContentTypeMatchers.Any(matcher => MatchScores.IsPerfect(matcher.IsMatch(contentType.MediaType))))
{
return BodyType.String;
@@ -133,13 +140,30 @@ internal static class BodyParser
return data;
}
// Try to get the body as String or Json
// Try to get the body as String, FormUrlEncoded or Json
try
{
data.BodyAsString = DefaultEncoding.GetString(data.BodyAsBytes);
data.Encoding = DefaultEncoding;
data.DetectedBodyType = BodyType.String;
// If string is not null or empty, try to deserialize the string to a IDictionary<string, string>
if (settings.DeserializeFormUrlEncoded &&
data.DetectedBodyTypeFromContentType == BodyType.FormUrlEncoded &&
QueryStringParser.TryParse(data.BodyAsString, false, out var nameValueCollection)
)
{
try
{
data.BodyAsFormUrlEncoded = nameValueCollection;
data.DetectedBodyType = BodyType.FormUrlEncoded;
}
catch
{
// Deserialize FormUrlEncoded failed, just ignore.
}
}
// If string is not null or empty, try to deserialize the string to a JObject
if (settings.DeserializeJson && !string.IsNullOrEmpty(data.BodyAsString))
{

View File

@@ -13,4 +13,6 @@ internal class BodyParserSettings
public bool DecompressGZipAndDeflate { get; set; } = true;
public bool DeserializeJson { get; set; } = true;
public bool DeserializeFormUrlEncoded { get; set; } = true;
}

View File

@@ -17,7 +17,7 @@ namespace WireMock.Util;
/// http://www.unicode.org/versions/corrigendum1.html
/// http://www.ietf.org/rfc/rfc2279.txt
/// </summary>
public static class BytesEncodingUtils
internal static class BytesEncodingUtils
{
/// <summary>
/// Tries the get the Encoding from an array of bytes.
@@ -108,7 +108,7 @@ public static class BytesEncodingUtils
return true;
}
if (ch >= 0xc2 && ch <= 0xdf)
if (ch is >= 0xc2 and <= 0xdf)
{
if (position >= length - 2)
{
@@ -145,7 +145,7 @@ public static class BytesEncodingUtils
return true;
}
if (ch >= 0xe1 && ch <= 0xef)
if (ch is >= 0xe1 and <= 0xef)
{
if (position >= length - 3)
{
@@ -204,7 +204,7 @@ public static class BytesEncodingUtils
return true;
}
if (ch >= 0xf1 && ch <= 0xf3)
if (ch is >= 0xf1 and <= 0xf3)
{
if (position >= length - 4)
{

View File

@@ -1,86 +1,85 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace WireMock.Util
namespace WireMock.Util;
/// <summary>
/// A special Collection that overrides methods of <see cref="ObservableCollection{T}"/> to make them thread safe.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
/// <inheritdoc cref="ObservableCollection{T}" />
internal class ConcurrentObservableCollection<T> : ObservableCollection<T>
{
private readonly object _lockObject = new object();
/// <summary>
/// Initializes a new instance of the <see cref="T:WireMock.Util.ConcurrentObservableCollection`1" /> class.
/// </summary>
public ConcurrentObservableCollection() { }
/// <summary>
/// A special Collection that overrides methods of <see cref="ObservableCollection{T}"/> to make them thread safe.
/// Initializes a new instance of the <see cref="ConcurrentObservableCollection{T}"/> class that contains elements copied from the specified list.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
/// <inheritdoc cref="ObservableCollection{T}" />
internal class ConcurrentObservableCollection<T> : ObservableCollection<T>
/// <param name="list">The list from which the elements are copied.</param>
public ConcurrentObservableCollection(List<T> list) : base(list) { }
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentObservableCollection{T}"/> class that contains elements copied from the specified collection.
/// </summary>
/// <param name="collection">The collection from which the elements are copied.</param>
public ConcurrentObservableCollection(IEnumerable<T> collection) : base(collection) { }
/// <inheritdoc cref="ObservableCollection{T}.ClearItems"/>
protected override void ClearItems()
{
private readonly object _lockObject = new object();
/// <summary>
/// Initializes a new instance of the <see cref="T:WireMock.Util.ConcurrentObservableCollection`1" /> class.
/// </summary>
public ConcurrentObservableCollection() { }
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentObservableCollection{T}"/> class that contains elements copied from the specified list.
/// </summary>
/// <param name="list">The list from which the elements are copied.</param>
public ConcurrentObservableCollection(List<T> list) : base(list) { }
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentObservableCollection{T}"/> class that contains elements copied from the specified collection.
/// </summary>
/// <param name="collection">The collection from which the elements are copied.</param>
public ConcurrentObservableCollection(IEnumerable<T> collection) : base(collection) { }
/// <inheritdoc cref="ObservableCollection{T}.ClearItems"/>
protected override void ClearItems()
lock (_lockObject)
{
lock (_lockObject)
{
base.ClearItems();
}
base.ClearItems();
}
}
/// <inheritdoc cref="ObservableCollection{T}.RemoveItem"/>
protected override void RemoveItem(int index)
/// <inheritdoc cref="ObservableCollection{T}.RemoveItem"/>
protected override void RemoveItem(int index)
{
lock (_lockObject)
{
lock (_lockObject)
{
base.RemoveItem(index);
}
base.RemoveItem(index);
}
}
/// <inheritdoc cref="ObservableCollection{T}.InsertItem"/>
protected override void InsertItem(int index, T item)
/// <inheritdoc cref="ObservableCollection{T}.InsertItem"/>
protected override void InsertItem(int index, T item)
{
lock (_lockObject)
{
lock (_lockObject)
{
base.InsertItem(index, item);
}
base.InsertItem(index, item);
}
}
/// <inheritdoc cref="ObservableCollection{T}.SetItem"/>
protected override void SetItem(int index, T item)
/// <inheritdoc cref="ObservableCollection{T}.SetItem"/>
protected override void SetItem(int index, T item)
{
lock (_lockObject)
{
lock (_lockObject)
{
base.SetItem(index, item);
}
base.SetItem(index, item);
}
}
/// <inheritdoc cref="ObservableCollection{T}.MoveItem"/>
protected override void MoveItem(int oldIndex, int newIndex)
/// <inheritdoc cref="ObservableCollection{T}.MoveItem"/>
protected override void MoveItem(int oldIndex, int newIndex)
{
lock (_lockObject)
{
lock (_lockObject)
{
base.MoveItem(oldIndex, newIndex);
}
base.MoveItem(oldIndex, newIndex);
}
}
public List<T> ToList()
public List<T> ToList()
{
lock (_lockObject)
{
lock (_lockObject)
{
return Items.ToList();
}
return Items.ToList();
}
}
}

View File

@@ -4,251 +4,250 @@ using System.IO;
using JetBrains.Annotations;
using Stef.Validation;
namespace WireMock.Util
namespace WireMock.Util;
/// <summary>
/// An EnhancedFileSystemWatcher, which can be used to suppress duplicate events that fire on a single change to the file.
/// </summary>
/// <seealso cref="FileSystemWatcher" />
public class EnhancedFileSystemWatcher : FileSystemWatcher
{
#region Private Members
// Default Watch Interval in Milliseconds
private const int DefaultWatchInterval = 100;
// This Dictionary keeps the track of when an event occurred last for a particular file
private ConcurrentDictionary<string, DateTime> _lastFileEvent = new();
// Watch Interval in Milliseconds
private int _interval;
// Timespan created when interval is set
private TimeSpan _recentTimeSpan;
#endregion
#region Public Properties
/// <summary>
/// An EnhancedFileSystemWatcher, which can be used to suppress duplicate events that fire on a single change to the file.
/// Interval, in milliseconds, within which events are considered "recent".
/// </summary>
/// <seealso cref="FileSystemWatcher" />
public class EnhancedFileSystemWatcher : FileSystemWatcher
[PublicAPI]
public int Interval
{
#region Private Members
// Default Watch Interval in Milliseconds
private const int DefaultWatchInterval = 100;
// This Dictionary keeps the track of when an event occurred last for a particular file
private ConcurrentDictionary<string, DateTime> _lastFileEvent = new();
// Watch Interval in Milliseconds
private int _interval;
// Timespan created when interval is set
private TimeSpan _recentTimeSpan;
#endregion
#region Public Properties
/// <summary>
/// Interval, in milliseconds, within which events are considered "recent".
/// </summary>
[PublicAPI]
public int Interval
get => _interval;
set
{
get => _interval;
set
{
_interval = value;
_interval = value;
// Set timespan based on the value passed
_recentTimeSpan = new TimeSpan(0, 0, 0, 0, value);
}
// Set timespan based on the value passed
_recentTimeSpan = new TimeSpan(0, 0, 0, 0, value);
}
/// <summary>
/// Allows user to set whether to filter recent events.
/// If this is set a false, this class behaves like System.IO.FileSystemWatcher class.
/// </summary>
[PublicAPI]
public bool FilterRecentEvents { get; set; }
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="EnhancedFileSystemWatcher"/> class.
/// </summary>
/// <param name="interval">The interval.</param>
public EnhancedFileSystemWatcher(int interval = DefaultWatchInterval)
{
Guard.Condition(interval, i => i >= 0);
InitializeMembers(interval);
}
/// <summary>
/// Initializes a new instance of the <see cref="EnhancedFileSystemWatcher"/> class.
/// </summary>
/// <param name="path">The directory to monitor, in standard or Universal Naming Convention (UNC) notation.</param>
/// <param name="interval">The interval.</param>
public EnhancedFileSystemWatcher(string path, int interval = DefaultWatchInterval) : base(path)
{
Guard.NotNullOrEmpty(path);
Guard.Condition(interval, i => i >= 0);
InitializeMembers(interval);
}
/// <summary>
/// Initializes a new instance of the <see cref="EnhancedFileSystemWatcher"/> class.
/// </summary>
/// <param name="path">The directory to monitor, in standard or Universal Naming Convention (UNC) notation.</param>
/// <param name="filter">The type of files to watch. For example, "*.txt" watches for changes to all text files.</param>
/// <param name="interval">The interval.</param>
public EnhancedFileSystemWatcher(string path, string filter, int interval = DefaultWatchInterval) : base(path, filter)
{
Guard.NotNullOrEmpty(path);
Guard.NotNullOrEmpty(filter);
Guard.Condition(interval, i => i >= 0);
InitializeMembers(interval);
}
#endregion
#region Events
// These events hide the events from the base class.
// We want to raise these events appropriately and we do not want the
// users of this class subscribing to these events of the base class accidentally
/// <summary>
/// Occurs when a file or directory in the specified <see cref="P:System.IO.FileSystemWatcher.Path" /> is changed.
/// </summary>
public new event FileSystemEventHandler? Changed;
/// <summary>
/// Occurs when a file or directory in the specified <see cref="P:System.IO.FileSystemWatcher.Path" /> is created.
/// </summary>
public new event FileSystemEventHandler? Created;
/// <summary>
/// Occurs when a file or directory in the specified <see cref="P:System.IO.FileSystemWatcher.Path" /> is deleted.
/// </summary>
public new event FileSystemEventHandler? Deleted;
/// <summary>
/// Occurs when a file or directory in the specified <see cref="P:System.IO.FileSystemWatcher.Path" /> is renamed.
/// </summary>
public new event RenamedEventHandler? Renamed;
#endregion
#region Protected Methods to raise the Events for this class
/// <summary>
/// Raises the <see cref="E:System.IO.FileSystemWatcher.Changed" /> event.
/// </summary>
/// <param name="e">A <see cref="T:System.IO.FileSystemEventArgs" /> that contains the event data.</param>
protected new virtual void OnChanged(FileSystemEventArgs e)
{
Changed?.Invoke(this, e);
}
/// <summary>
/// Raises the <see cref="E:System.IO.FileSystemWatcher.Created" /> event.
/// </summary>
/// <param name="e">A <see cref="T:System.IO.FileSystemEventArgs" /> that contains the event data.</param>
protected new virtual void OnCreated(FileSystemEventArgs e)
{
Created?.Invoke(this, e);
}
/// <summary>
/// Raises the <see cref="E:System.IO.FileSystemWatcher.Deleted" /> event.
/// </summary>
/// <param name="e">A <see cref="T:System.IO.FileSystemEventArgs" /> that contains the event data.</param>
protected new virtual void OnDeleted(FileSystemEventArgs e)
{
Deleted?.Invoke(this, e);
}
/// <summary>
/// Raises the <see cref="E:System.IO.FileSystemWatcher.Renamed" /> event.
/// </summary>
/// <param name="e">A <see cref="T:System.IO.RenamedEventArgs" /> that contains the event data.</param>
protected new virtual void OnRenamed(RenamedEventArgs e)
{
Renamed?.Invoke(this, e);
}
#endregion
#region Private Methods
/// <summary>
/// This Method Initializes the private members.
/// Interval is set to its default value of 100 millisecond.
/// FilterRecentEvents is set to true, _lastFileEvent dictionary is initialized.
/// We subscribe to the base class events.
/// </summary>
private void InitializeMembers(int interval = 100)
{
Interval = interval;
FilterRecentEvents = true;
_lastFileEvent = new ConcurrentDictionary<string, DateTime>();
base.Created += OnCreated;
base.Changed += OnChanged;
base.Deleted += OnDeleted;
base.Renamed += OnRenamed;
}
/// <summary>
/// This method searches the dictionary to find out when the last event occurred
/// for a particular file. If that event occurred within the specified timespan
/// it returns true, else false
/// </summary>
/// <param name="fileName">The filename to be checked</param>
/// <returns>True if an event has occurred within the specified interval, False otherwise</returns>
private bool HasAnotherFileEventOccurredRecently(string fileName)
{
// Check dictionary only if user wants to filter recent events otherwise return value stays false.
if (!FilterRecentEvents)
{
return false;
}
bool retVal = false;
if (_lastFileEvent.ContainsKey(fileName))
{
// If dictionary contains the filename, check how much time has elapsed
// since the last event occurred. If the timespan is less that the
// specified interval, set return value to true
// and store current datetime in dictionary for this file
DateTime lastEventTime = _lastFileEvent[fileName];
DateTime currentTime = DateTime.Now;
TimeSpan timeSinceLastEvent = currentTime - lastEventTime;
retVal = timeSinceLastEvent < _recentTimeSpan;
_lastFileEvent[fileName] = currentTime;
}
else
{
// If dictionary does not contain the filename,
// no event has occurred in past for this file, so set return value to false
// and append filename along with current datetime to the dictionary
_lastFileEvent.TryAdd(fileName, DateTime.Now);
}
return retVal;
}
#region FileSystemWatcher EventHandlers
// Base class Event Handlers. Check if an event has occurred recently and call method
// to raise appropriate event only if no recent event is detected
private void OnChanged(object sender, FileSystemEventArgs e)
{
if (!HasAnotherFileEventOccurredRecently(e.FullPath))
{
OnChanged(e);
}
}
private void OnCreated(object sender, FileSystemEventArgs e)
{
if (!HasAnotherFileEventOccurredRecently(e.FullPath))
{
OnCreated(e);
}
}
private void OnDeleted(object sender, FileSystemEventArgs e)
{
if (!HasAnotherFileEventOccurredRecently(e.FullPath))
{
OnDeleted(e);
}
}
private void OnRenamed(object sender, RenamedEventArgs e)
{
if (!HasAnotherFileEventOccurredRecently(e.OldFullPath))
{
OnRenamed(e);
}
}
#endregion
#endregion
}
/// <summary>
/// Allows user to set whether to filter recent events.
/// If this is set a false, this class behaves like System.IO.FileSystemWatcher class.
/// </summary>
[PublicAPI]
public bool FilterRecentEvents { get; set; }
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="EnhancedFileSystemWatcher"/> class.
/// </summary>
/// <param name="interval">The interval.</param>
public EnhancedFileSystemWatcher(int interval = DefaultWatchInterval)
{
Guard.Condition(interval, i => i >= 0);
InitializeMembers(interval);
}
/// <summary>
/// Initializes a new instance of the <see cref="EnhancedFileSystemWatcher"/> class.
/// </summary>
/// <param name="path">The directory to monitor, in standard or Universal Naming Convention (UNC) notation.</param>
/// <param name="interval">The interval.</param>
public EnhancedFileSystemWatcher(string path, int interval = DefaultWatchInterval) : base(path)
{
Guard.NotNullOrEmpty(path);
Guard.Condition(interval, i => i >= 0);
InitializeMembers(interval);
}
/// <summary>
/// Initializes a new instance of the <see cref="EnhancedFileSystemWatcher"/> class.
/// </summary>
/// <param name="path">The directory to monitor, in standard or Universal Naming Convention (UNC) notation.</param>
/// <param name="filter">The type of files to watch. For example, "*.txt" watches for changes to all text files.</param>
/// <param name="interval">The interval.</param>
public EnhancedFileSystemWatcher(string path, string filter, int interval = DefaultWatchInterval) : base(path, filter)
{
Guard.NotNullOrEmpty(path);
Guard.NotNullOrEmpty(filter);
Guard.Condition(interval, i => i >= 0);
InitializeMembers(interval);
}
#endregion
#region Events
// These events hide the events from the base class.
// We want to raise these events appropriately and we do not want the
// users of this class subscribing to these events of the base class accidentally
/// <summary>
/// Occurs when a file or directory in the specified <see cref="P:System.IO.FileSystemWatcher.Path" /> is changed.
/// </summary>
public new event FileSystemEventHandler? Changed;
/// <summary>
/// Occurs when a file or directory in the specified <see cref="P:System.IO.FileSystemWatcher.Path" /> is created.
/// </summary>
public new event FileSystemEventHandler? Created;
/// <summary>
/// Occurs when a file or directory in the specified <see cref="P:System.IO.FileSystemWatcher.Path" /> is deleted.
/// </summary>
public new event FileSystemEventHandler? Deleted;
/// <summary>
/// Occurs when a file or directory in the specified <see cref="P:System.IO.FileSystemWatcher.Path" /> is renamed.
/// </summary>
public new event RenamedEventHandler? Renamed;
#endregion
#region Protected Methods to raise the Events for this class
/// <summary>
/// Raises the <see cref="E:System.IO.FileSystemWatcher.Changed" /> event.
/// </summary>
/// <param name="e">A <see cref="T:System.IO.FileSystemEventArgs" /> that contains the event data.</param>
protected new virtual void OnChanged(FileSystemEventArgs e)
{
Changed?.Invoke(this, e);
}
/// <summary>
/// Raises the <see cref="E:System.IO.FileSystemWatcher.Created" /> event.
/// </summary>
/// <param name="e">A <see cref="T:System.IO.FileSystemEventArgs" /> that contains the event data.</param>
protected new virtual void OnCreated(FileSystemEventArgs e)
{
Created?.Invoke(this, e);
}
/// <summary>
/// Raises the <see cref="E:System.IO.FileSystemWatcher.Deleted" /> event.
/// </summary>
/// <param name="e">A <see cref="T:System.IO.FileSystemEventArgs" /> that contains the event data.</param>
protected new virtual void OnDeleted(FileSystemEventArgs e)
{
Deleted?.Invoke(this, e);
}
/// <summary>
/// Raises the <see cref="E:System.IO.FileSystemWatcher.Renamed" /> event.
/// </summary>
/// <param name="e">A <see cref="T:System.IO.RenamedEventArgs" /> that contains the event data.</param>
protected new virtual void OnRenamed(RenamedEventArgs e)
{
Renamed?.Invoke(this, e);
}
#endregion
#region Private Methods
/// <summary>
/// This Method Initializes the private members.
/// Interval is set to its default value of 100 millisecond.
/// FilterRecentEvents is set to true, _lastFileEvent dictionary is initialized.
/// We subscribe to the base class events.
/// </summary>
private void InitializeMembers(int interval = 100)
{
Interval = interval;
FilterRecentEvents = true;
_lastFileEvent = new ConcurrentDictionary<string, DateTime>();
base.Created += OnCreated;
base.Changed += OnChanged;
base.Deleted += OnDeleted;
base.Renamed += OnRenamed;
}
/// <summary>
/// This method searches the dictionary to find out when the last event occurred
/// for a particular file. If that event occurred within the specified timespan
/// it returns true, else false
/// </summary>
/// <param name="fileName">The filename to be checked</param>
/// <returns>True if an event has occurred within the specified interval, False otherwise</returns>
private bool HasAnotherFileEventOccurredRecently(string fileName)
{
// Check dictionary only if user wants to filter recent events otherwise return value stays false.
if (!FilterRecentEvents)
{
return false;
}
bool retVal = false;
if (_lastFileEvent.ContainsKey(fileName))
{
// If dictionary contains the filename, check how much time has elapsed
// since the last event occurred. If the timespan is less that the
// specified interval, set return value to true
// and store current datetime in dictionary for this file
DateTime lastEventTime = _lastFileEvent[fileName];
DateTime currentTime = DateTime.Now;
TimeSpan timeSinceLastEvent = currentTime - lastEventTime;
retVal = timeSinceLastEvent < _recentTimeSpan;
_lastFileEvent[fileName] = currentTime;
}
else
{
// If dictionary does not contain the filename,
// no event has occurred in past for this file, so set return value to false
// and append filename along with current datetime to the dictionary
_lastFileEvent.TryAdd(fileName, DateTime.Now);
}
return retVal;
}
#region FileSystemWatcher EventHandlers
// Base class Event Handlers. Check if an event has occurred recently and call method
// to raise appropriate event only if no recent event is detected
private void OnChanged(object sender, FileSystemEventArgs e)
{
if (!HasAnotherFileEventOccurredRecently(e.FullPath))
{
OnChanged(e);
}
}
private void OnCreated(object sender, FileSystemEventArgs e)
{
if (!HasAnotherFileEventOccurredRecently(e.FullPath))
{
OnCreated(e);
}
}
private void OnDeleted(object sender, FileSystemEventArgs e)
{
if (!HasAnotherFileEventOccurredRecently(e.FullPath))
{
OnDeleted(e);
}
}
private void OnRenamed(object sender, RenamedEventArgs e)
{
if (!HasAnotherFileEventOccurredRecently(e.OldFullPath))
{
OnRenamed(e);
}
}
#endregion
#endregion
}

View File

@@ -16,18 +16,14 @@ internal static class HttpStatusRangeParser
/// <param name="pattern">The pattern. (Can be null, in that case it's allowed.)</param>
/// <param name="httpStatusCode">The value.</param>
/// <exception cref="ArgumentException"><paramref name="pattern"/> is invalid.</exception>
public static bool IsMatch(string pattern, object? httpStatusCode)
public static bool IsMatch(string? pattern, object? httpStatusCode)
{
switch (httpStatusCode)
return httpStatusCode switch
{
case int statusCodeAsInteger:
return IsMatch(pattern, statusCodeAsInteger);
case string statusCodeAsString:
return IsMatch(pattern, int.Parse(statusCodeAsString));
}
return false;
int statusCodeAsInteger => IsMatch(pattern, statusCodeAsInteger),
string statusCodeAsString => IsMatch(pattern, int.Parse(statusCodeAsString)),
_ => false
};
}
/// <summary>

View File

@@ -9,7 +9,7 @@ namespace WireMock.Util;
/// <summary>
/// Port Utility class
/// </summary>
public static class PortUtils
internal static class PortUtils
{
private static readonly Regex UrlDetailsRegex = new(@"^((?<proto>\w+)://)(?<host>[^/]+?):(?<port>\d+)\/?$", RegexOptions.Compiled);

View File

@@ -1,7 +1,8 @@
using System;
using System.Net;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using WireMock.Types;
namespace WireMock.Util;
@@ -13,6 +14,31 @@ internal static class QueryStringParser
{
private static readonly Dictionary<string, WireMockList<string>> Empty = new();
public static bool TryParse(string? queryString, bool caseIgnore, [NotNullWhen(true)] out IDictionary<string, string>? nameValueCollection)
{
if (queryString is null)
{
nameValueCollection = default;
return false;
}
var parts = queryString!
.Split(new[] { "&" }, StringSplitOptions.RemoveEmptyEntries)
.Select(parameter => parameter.Split('='))
.Distinct();
nameValueCollection = caseIgnore ? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) : new Dictionary<string, string>();
foreach (var part in parts)
{
if (part.Length == 2)
{
nameValueCollection.Add(part[0], WebUtility.UrlDecode(part[1]));
}
}
return true;
}
public static IDictionary<string, WireMockList<string>> Parse(string? queryString, QueryParameterMultipleValueSupport? support = null)
{
if (string.IsNullOrEmpty(queryString))

View File

@@ -25,7 +25,7 @@ internal static class RegexUtils
return namedGroupsDictionary;
}
public static (bool IsValid, bool Result) MatchRegex(string pattern, string input, bool useRegexExtended = true)
public static (bool IsValid, bool Result) MatchRegex(string? pattern, string input, bool useRegexExtended = true)
{
if (string.IsNullOrEmpty(pattern))
{
@@ -36,14 +36,12 @@ internal static class RegexUtils
{
if (useRegexExtended)
{
var r = new RegexExtended(pattern, RegexOptions.None, RegexTimeOut);
return (true, r.IsMatch(input));
}
else
{
var r = new Regex(pattern, RegexOptions.None, RegexTimeOut);
return (true, r.IsMatch(input));
var regexExtended = new RegexExtended(pattern, RegexOptions.None, RegexTimeOut);
return (true, regexExtended.IsMatch(input));
}
var regex = new Regex(pattern, RegexOptions.None, RegexTimeOut);
return (true, regex.IsMatch(input));
}
catch
{

View File

@@ -1,119 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace WireMock.Util;
/// <summary>
/// Code based on https://stackoverflow.com/questions/40507909/convert-jobject-to-anonymous-object
/// </summary>
internal static class TypeBuilderUtils
{
private static readonly ConcurrentDictionary<IDictionary<string, Type>, Type> Types = new();
private static readonly ModuleBuilder ModuleBuilder = AssemblyBuilder
.DefineDynamicAssembly(new AssemblyName("WireMock.Net.Reflection"), AssemblyBuilderAccess.Run)
.DefineDynamicModule("WireMock.Net.Reflection.Module");
public static Type BuildType(IDictionary<string, Type> properties, string? name = null)
{
var keyExists = Types.Keys.FirstOrDefault(k => Compare(k, properties));
if (keyExists != null)
{
return Types[keyExists];
}
var typeBuilder = GetTypeBuilder(name ?? Guid.NewGuid().ToString());
foreach (var property in properties)
{
CreateGetSetMethods(typeBuilder, property.Key, property.Value);
}
var type = typeBuilder.CreateTypeInfo()!.AsType();
Types.TryAdd(properties, type);
return type;
}
/// <summary>
/// https://stackoverflow.com/questions/3804367/testing-for-equality-between-dictionaries-in-c-sharp
/// </summary>
private static bool Compare<TKey, TValue>(IDictionary<TKey, TValue> dict1, IDictionary<TKey, TValue> dict2)
where TKey : notnull
{
if (dict1 == dict2)
{
return true;
}
if (dict1.Count != dict2.Count)
{
return false;
}
var valueComparer = EqualityComparer<TValue>.Default;
foreach (var kvp in dict1)
{
if (!dict2.TryGetValue(kvp.Key, out var value2))
{
return false;
}
if (!valueComparer.Equals(kvp.Value, value2))
{
return false;
}
}
return true;
}
private static TypeBuilder GetTypeBuilder(string name)
{
return ModuleBuilder.DefineType(name,
TypeAttributes.Public |
TypeAttributes.Class |
TypeAttributes.AutoClass |
TypeAttributes.AnsiClass |
TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout,
null);
}
private static void CreateGetSetMethods(TypeBuilder typeBuilder, string propertyName, Type propertyType)
{
var fieldBuilder = typeBuilder.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);
var propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
var getPropertyMethodBuilder = typeBuilder.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
var getIl = getPropertyMethodBuilder.GetILGenerator();
getIl.Emit(OpCodes.Ldarg_0);
getIl.Emit(OpCodes.Ldfld, fieldBuilder);
getIl.Emit(OpCodes.Ret);
var setPropertyMethodBuilder = typeBuilder.DefineMethod("set_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new[] { propertyType });
var setIl = setPropertyMethodBuilder.GetILGenerator();
var modifyProperty = setIl.DefineLabel();
var exitSet = setIl.DefineLabel();
setIl.MarkLabel(modifyProperty);
setIl.Emit(OpCodes.Ldarg_0);
setIl.Emit(OpCodes.Ldarg_1);
setIl.Emit(OpCodes.Stfld, fieldBuilder);
setIl.Emit(OpCodes.Nop);
setIl.MarkLabel(exitSet);
setIl.Emit(OpCodes.Ret);
propertyBuilder.SetGetMethod(getPropertyMethodBuilder);
propertyBuilder.SetSetMethod(setPropertyMethodBuilder);
}
}

View File

@@ -1,11 +0,0 @@
using System;
namespace WireMock.Validation
{
/// <summary>
/// To fix 'xxx' is null on at least one execution path. See also https://rules.sonarsource.com/csharp/RSPEC-3900.
/// </summary>
internal class ValidatedNotNullAttribute : Attribute
{
}
}

View File

@@ -35,15 +35,19 @@
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard1.3' or '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1'">
<DefineConstants>NETSTANDARD;USE_ASPNETCORE</DefineConstants>
<DefineConstants>$(DefineConstants);NETSTANDARD;USE_ASPNETCORE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1' or '$(TargetFramework)' == 'netcoreapp2.2' or '$(TargetFramework)' == 'netcoreapp3.1' or '$(TargetFramework)' == 'net5.0' or '$(TargetFramework)' == 'net6.0'or '$(TargetFramework)' == 'net7.0' ">
<DefineConstants>USE_ASPNETCORE</DefineConstants>
<DefineConstants>$(DefineConstants);USE_ASPNETCORE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net461'">
<DefineConstants>USE_ASPNETCORE;NET46</DefineConstants>
<DefineConstants>$(DefineConstants);USE_ASPNETCORE;NET46</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452'">
<DefineConstants>$(DefineConstants);OPENAPIPARSER</DefineConstants>
</PropertyGroup>
<ItemGroup>
@@ -176,17 +180,21 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Handlebars.Net.Helpers" Version="2.3.15-preview-01" />
<PackageReference Include="Handlebars.Net.Helpers.DynamicLinq" Version="2.3.15-preview-01" />
<PackageReference Include="Handlebars.Net.Helpers.Humanizer" Version="2.3.15-preview-01" />
<PackageReference Include="Handlebars.Net.Helpers.Json" Version="2.3.15-preview-01" />
<PackageReference Include="Handlebars.Net.Helpers.Random" Version="2.3.15-preview-01" />
<PackageReference Include="Handlebars.Net.Helpers.XPath" Version="2.3.15-preview-01" />
<PackageReference Include="Handlebars.Net.Helpers.Xeger" Version="2.3.15-preview-01" />
<PackageReference Include="Handlebars.Net.Helpers" Version="2.3.15" />
<PackageReference Include="Handlebars.Net.Helpers.DynamicLinq" Version="2.3.15" />
<PackageReference Include="Handlebars.Net.Helpers.Humanizer" Version="2.3.15" />
<PackageReference Include="Handlebars.Net.Helpers.Json" Version="2.3.15" />
<PackageReference Include="Handlebars.Net.Helpers.Random" Version="2.3.15" />
<PackageReference Include="Handlebars.Net.Helpers.XPath" Version="2.3.15" />
<PackageReference Include="Handlebars.Net.Helpers.Xeger" Version="2.3.15" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WireMock.Net.Abstractions\WireMock.Net.Abstractions.csproj" />
<ProjectReference Include="..\WireMock.Org.Abstractions\WireMock.Org.Abstractions.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452'">
<ProjectReference Include="..\WireMock.Net.OpenApiParser\WireMock.Net.OpenApiParser.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,3 +1,4 @@
using System;
using Newtonsoft.Json.Linq;
using NFluent;
using WireMock.Matchers;
@@ -37,7 +38,7 @@ public class JmesPathMatcherTests
public void JmesPathMatcher_IsMatch_ByteArray()
{
// Assign
var bytes = new byte[0];
var bytes = EmptyArray<byte>.Value;
var matcher = new JmesPathMatcher("");
// Act

View File

@@ -111,7 +111,7 @@ public class JsonMatcherTests
public void JsonMatcher_IsMatch_ByteArray()
{
// Assign
var bytes = new byte[0];
var bytes = EmptyArray<byte>.Value;
var matcher = new JsonMatcher("");
// Act

View File

@@ -89,7 +89,7 @@ public class JsonPartialMatcherTests
public void JsonPartialMatcher_IsMatch_ByteArray()
{
// Assign
var bytes = new byte[0];
var bytes = EmptyArray<byte>.Value;
var matcher = new JsonPartialMatcher("");
// Act

View File

@@ -89,7 +89,7 @@ public class JsonPartialWildcardMatcherTests
public void JsonPartialWildcardMatcher_IsMatch_ByteArray()
{
// Assign
var bytes = new byte[0];
var bytes = EmptyArray<byte>.Value;
var matcher = new JsonPartialWildcardMatcher("");
// Act

View File

@@ -1,3 +1,4 @@
using System;
using Newtonsoft.Json.Linq;
using NFluent;
using WireMock.Matchers;
@@ -37,7 +38,7 @@ public class JsonPathMatcherTests
public void JsonPathMatcher_IsMatch_ByteArray()
{
// Assign
var bytes = new byte[0];
var bytes = EmptyArray<byte>.Value;
var matcher = new JsonPathMatcher("");
// Act

View File

@@ -0,0 +1,840 @@
{
"openapi": "3.0.2",
"info": {
"title": "Swagger Petstore - OpenAPI 3.0",
"description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) ",
"termsOfService": "http://swagger.io/terms/",
"contact": { "email": "apiteam@swagger.io" },
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0.4"
},
"externalDocs": {
"description": "Find out more about Swagger",
"url": "http://swagger.io"
},
"servers": [ { "url": "/api/v3" } ],
"tags": [
{
"name": "pet",
"description": "Everything about your Pets",
"externalDocs": {
"description": "Find out more",
"url": "http://swagger.io"
}
},
{
"name": "store",
"description": "Operations about user"
},
{
"name": "user",
"description": "Access to Petstore orders",
"externalDocs": {
"description": "Find out more about our store",
"url": "http://swagger.io"
}
}
],
"paths": {
"/pet": {
"put": {
"tags": [ "pet" ],
"summary": "Update an existing pet",
"description": "Update an existing pet by Id",
"operationId": "updatePet",
"requestBody": {
"description": "Update an existent pet in the store",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Pet" } },
"application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } },
"application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Pet" } }
},
"required": true
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } },
"application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }
}
},
"400": { "description": "Invalid ID supplied" },
"404": { "description": "Pet not found" },
"405": { "description": "Validation exception" }
},
"security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ]
},
"post": {
"tags": [ "pet" ],
"summary": "Add a new pet to the store",
"description": "Add a new pet to the store",
"operationId": "addPet",
"requestBody": {
"description": "Create a new pet in the store",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Pet" } },
"application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } },
"application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Pet" } }
},
"required": true
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } },
"application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }
}
},
"405": { "description": "Invalid input" }
},
"security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ]
}
},
"/pet/findByStatus": {
"get": {
"tags": [ "pet" ],
"summary": "Finds Pets by status",
"description": "Multiple status values can be provided with comma separated strings",
"operationId": "findPetsByStatus",
"parameters": [
{
"name": "status",
"in": "query",
"description": "Status values that need to be considered for filter",
"required": false,
"explode": true,
"schema": {
"type": "string",
"default": "available",
"enum": [ "available", "pending", "sold" ]
}
}
],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/xml": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/Pet" }
}
},
"application/json": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/Pet" }
}
}
}
},
"400": { "description": "Invalid status value" }
},
"security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ]
}
},
"/pet/findByTags": {
"get": {
"tags": [ "pet" ],
"summary": "Finds Pets by tags",
"description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
"operationId": "findPetsByTags",
"parameters": [
{
"name": "tags",
"in": "query",
"description": "Tags to filter by",
"required": false,
"explode": true,
"schema": {
"type": "array",
"items": { "type": "string" }
}
}
],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/xml": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/Pet" }
}
},
"application/json": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/Pet" }
}
}
}
},
"400": { "description": "Invalid tag value" }
},
"security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ]
}
},
"/pet/{petId}": {
"get": {
"tags": [ "pet" ],
"summary": "Find pet by ID",
"description": "Returns a single pet",
"operationId": "getPetById",
"parameters": [
{
"name": "petId",
"in": "path",
"description": "ID of pet to return",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } },
"application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }
}
},
"400": { "description": "Invalid ID supplied" },
"404": { "description": "Pet not found" }
},
"security": [
{ "api_key": [] },
{ "petstore_auth": [ "write:pets", "read:pets" ] }
]
},
"post": {
"tags": [ "pet" ],
"summary": "Updates a pet in the store with form data",
"description": "",
"operationId": "updatePetWithForm",
"parameters": [
{
"name": "petId",
"in": "path",
"description": "ID of pet that needs to be updated",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "name",
"in": "query",
"description": "Name of pet that needs to be updated",
"schema": { "type": "string" }
},
{
"name": "status",
"in": "query",
"description": "Status of pet that needs to be updated",
"schema": { "type": "string" }
}
],
"responses": { "405": { "description": "Invalid input" } },
"security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ]
},
"delete": {
"tags": [ "pet" ],
"summary": "Deletes a pet",
"description": "",
"operationId": "deletePet",
"parameters": [
{
"name": "api_key",
"in": "header",
"description": "",
"required": false,
"schema": { "type": "string" }
},
{
"name": "petId",
"in": "path",
"description": "Pet id to delete",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": { "400": { "description": "Invalid pet value" } },
"security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ]
}
},
"/pet/{petId}/uploadImage": {
"post": {
"tags": [ "pet" ],
"summary": "uploads an image",
"description": "",
"operationId": "uploadFile",
"parameters": [
{
"name": "petId",
"in": "path",
"description": "ID of pet to update",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "additionalMetadata",
"in": "query",
"description": "Additional Metadata",
"required": false,
"schema": { "type": "string" }
}
],
"requestBody": {
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"responses": {
"200": {
"description": "successful operation",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } }
}
},
"security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ]
}
},
"/store/inventory": {
"get": {
"tags": [ "store" ],
"summary": "Returns pet inventories by status",
"description": "Returns a map of status codes to quantities",
"operationId": "getInventory",
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int32"
}
}
}
}
}
},
"security": [ { "api_key": [] } ]
}
},
"/store/order": {
"post": {
"tags": [ "store" ],
"summary": "Place an order for a pet",
"description": "Place a new order in the store",
"operationId": "placeOrder",
"requestBody": {
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Order" } },
"application/xml": { "schema": { "$ref": "#/components/schemas/Order" } },
"application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/Order" } }
}
},
"responses": {
"200": {
"description": "successful operation",
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } }
},
"405": { "description": "Invalid input" }
}
}
},
"/store/order/{orderId}": {
"get": {
"tags": [ "store" ],
"summary": "Find purchase order by ID",
"description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions",
"operationId": "getOrderById",
"parameters": [
{
"name": "orderId",
"in": "path",
"description": "ID of order that needs to be fetched",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/xml": { "schema": { "$ref": "#/components/schemas/Order" } },
"application/json": { "schema": { "$ref": "#/components/schemas/Order" } }
}
},
"400": { "description": "Invalid ID supplied" },
"404": { "description": "Order not found" }
}
},
"delete": {
"tags": [ "store" ],
"summary": "Delete purchase order by ID",
"description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors",
"operationId": "deleteOrder",
"parameters": [
{
"name": "orderId",
"in": "path",
"description": "ID of the order that needs to be deleted",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"400": { "description": "Invalid ID supplied" },
"404": { "description": "Order not found" }
}
}
},
"/user": {
"post": {
"tags": [ "user" ],
"summary": "Create user",
"description": "This can only be done by the logged in user.",
"operationId": "createUser",
"requestBody": {
"description": "Created user object",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/User" } },
"application/xml": { "schema": { "$ref": "#/components/schemas/User" } },
"application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/User" } }
}
},
"responses": {
"default": {
"description": "successful operation",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/User" } },
"application/xml": { "schema": { "$ref": "#/components/schemas/User" } }
}
}
}
}
},
"/user/createWithList": {
"post": {
"tags": [ "user" ],
"summary": "Creates list of users with given input array",
"description": "Creates list of users with given input array",
"operationId": "createUsersWithListInput",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/User" }
}
}
}
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/xml": { "schema": { "$ref": "#/components/schemas/User" } },
"application/json": { "schema": { "$ref": "#/components/schemas/User" } }
}
},
"default": { "description": "successful operation" }
}
}
},
"/user/login": {
"get": {
"tags": [ "user" ],
"summary": "Logs user into the system",
"description": "",
"operationId": "loginUser",
"parameters": [
{
"name": "username",
"in": "query",
"description": "The user name for login",
"required": false,
"schema": { "type": "string" }
},
{
"name": "password",
"in": "query",
"description": "The password for login in clear text",
"required": false,
"schema": { "type": "string" }
}
],
"responses": {
"200": {
"description": "successful operation",
"headers": {
"X-Rate-Limit": {
"description": "calls per hour allowed by the user",
"schema": {
"type": "integer",
"format": "int32"
}
},
"X-Expires-After": {
"description": "date in UTC when toekn expires",
"schema": {
"type": "string",
"format": "date-time"
}
}
},
"content": {
"application/xml": { "schema": { "type": "string" } },
"application/json": { "schema": { "type": "string" } }
}
},
"400": { "description": "Invalid username/password supplied" }
}
}
},
"/user/logout": {
"get": {
"tags": [ "user" ],
"summary": "Logs out current logged in user session",
"description": "",
"operationId": "logoutUser",
"parameters": [],
"responses": { "default": { "description": "successful operation" } }
}
},
"/user/{username}": {
"get": {
"tags": [ "user" ],
"summary": "Get user by user name",
"description": "",
"operationId": "getUserByName",
"parameters": [
{
"name": "username",
"in": "path",
"description": "The name that needs to be fetched. Use user1 for testing. ",
"required": true,
"schema": { "type": "string" }
}
],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/xml": { "schema": { "$ref": "#/components/schemas/User" } },
"application/json": { "schema": { "$ref": "#/components/schemas/User" } }
}
},
"400": { "description": "Invalid username supplied" },
"404": { "description": "User not found" }
}
},
"put": {
"tags": [ "user" ],
"summary": "Update user",
"description": "This can only be done by the logged in user.",
"operationId": "updateUser",
"parameters": [
{
"name": "username",
"in": "path",
"description": "name that need to be deleted",
"required": true,
"schema": { "type": "string" }
}
],
"requestBody": {
"description": "Update an existent user in the store",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/User" } },
"application/xml": { "schema": { "$ref": "#/components/schemas/User" } },
"application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/User" } }
}
},
"responses": { "default": { "description": "successful operation" } }
},
"delete": {
"tags": [ "user" ],
"summary": "Delete user",
"description": "This can only be done by the logged in user.",
"operationId": "deleteUser",
"parameters": [
{
"name": "username",
"in": "path",
"description": "The name that needs to be deleted",
"required": true,
"schema": { "type": "string" }
}
],
"responses": {
"400": { "description": "Invalid username supplied" },
"404": { "description": "User not found" }
}
}
}
},
"components": {
"schemas": {
"Order": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64",
"example": 10
},
"petId": {
"type": "integer",
"format": "int64",
"example": 198772
},
"quantity": {
"type": "integer",
"format": "int32",
"example": 7
},
"shipDate": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string",
"description": "Order Status",
"example": "approved",
"enum": [ "placed", "approved", "delivered" ]
},
"complete": { "type": "boolean" }
},
"xml": { "name": "order" }
},
"Customer": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64",
"example": 100000
},
"username": {
"type": "string",
"example": "fehguy"
},
"address": {
"type": "array",
"xml": {
"name": "addresses",
"wrapped": true
},
"items": { "$ref": "#/components/schemas/Address" }
}
},
"xml": { "name": "customer" }
},
"Address": {
"type": "object",
"properties": {
"street": {
"type": "string",
"example": "437 Lytton"
},
"city": {
"type": "string",
"example": "Palo Alto"
},
"state": {
"type": "string",
"example": "CA"
},
"zip": {
"type": "string",
"example": "94301"
}
},
"xml": { "name": "address" }
},
"Category": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64",
"example": 1
},
"name": {
"type": "string",
"example": "Dogs"
}
},
"xml": { "name": "category" }
},
"User": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64",
"example": 10
},
"username": {
"type": "string",
"example": "theUser"
},
"firstName": {
"type": "string",
"example": "John"
},
"lastName": {
"type": "string",
"example": "James"
},
"email": {
"type": "string",
"example": "john@email.com"
},
"password": {
"type": "string",
"example": "12345"
},
"phone": {
"type": "string",
"example": "12345"
},
"userStatus": {
"type": "integer",
"description": "User Status",
"format": "int32",
"example": 1
}
},
"xml": { "name": "user" }
},
"Tag": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": { "type": "string" }
},
"xml": { "name": "tag" }
},
"Pet": {
"required": [ "name", "photoUrls" ],
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64",
"example": 10
},
"name": {
"type": "string",
"example": "doggie"
},
"category": { "$ref": "#/components/schemas/Category" },
"photoUrls": {
"type": "array",
"xml": { "wrapped": true },
"items": {
"type": "string",
"xml": { "name": "photoUrl" }
}
},
"tags": {
"type": "array",
"xml": { "wrapped": true },
"items": { "$ref": "#/components/schemas/Tag" }
},
"status": {
"type": "string",
"description": "pet status in the store",
"enum": [ "available", "pending", "sold" ]
}
},
"xml": { "name": "pet" }
},
"ApiResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"type": { "type": "string" },
"message": { "type": "string" }
},
"xml": { "name": "##default" }
}
},
"requestBodies": {
"Pet": {
"description": "Pet object that needs to be added to the store",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Pet" } },
"application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }
}
},
"UserArray": {
"description": "List of user object",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/User" }
}
}
}
}
},
"securitySchemes": {
"petstore_auth": {
"type": "oauth2",
"flows": {
"implicit": {
"authorizationUrl": "https://petstore.swagger.io/oauth/authorize",
"scopes": {
"write:pets": "modify pets in your account",
"read:pets": "read your pets"
}
}
}
},
"api_key": {
"type": "apiKey",
"name": "api_key",
"in": "header"
}
}
}
}

View File

@@ -0,0 +1,730 @@
swagger: '2.0'
info:
description: 'This is a sample server Petstore server. Copied from https://github.com/swagger-api/swagger-codegen/blob/master/modules/swagger-codegen/src/test/resources/2_0/petstore.yaml.'
version: 1.0.0
title: Swagger Petstore
termsOfService: 'http://swagger.io/terms/'
contact:
email: apiteam@swagger.io
license:
name: Apache-2.0
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
host: petstore.swagger.io
basePath: /v2
tags:
- name: pet
description: Everything about your Pets
externalDocs:
description: Find out more
url: 'http://swagger.io'
- name: store
description: Access to Petstore orders
- name: user
description: Operations about user
externalDocs:
description: Find out more about our store
url: 'http://swagger.io'
schemes:
- http
paths:
/pet:
post:
tags:
- pet
summary: Add a new pet to the store
description: ''
operationId: addPet
consumes:
- application/json
- application/xml
produces:
- application/xml
- application/json
parameters:
- in: body
name: body
description: Pet object that needs to be added to the store
required: true
schema:
$ref: '#/definitions/Pet'
responses:
'405':
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
put:
tags:
- pet
summary: Update an existing pet
description: ''
operationId: updatePet
consumes:
- application/json
- application/xml
produces:
- application/xml
- application/json
parameters:
- in: body
name: body
description: Pet object that needs to be added to the store
required: true
schema:
$ref: '#/definitions/Pet'
responses:
'400':
description: Invalid ID supplied
'404':
description: Pet not found
'405':
description: Validation exception
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
/pet/findByStatus:
get:
tags:
- pet
summary: Finds Pets by status
description: Multiple status values can be provided with comma separated strings
operationId: findPetsByStatus
produces:
- application/xml
- application/json
parameters:
- name: status
in: query
description: Status values that need to be considered for filter
required: true
type: array
items:
type: string
enum:
- available
- pending
- sold
default: available
collectionFormat: csv
responses:
'200':
description: successful operation
schema:
type: array
items:
$ref: '#/definitions/Pet'
'400':
description: Invalid status value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
/pet/findByTags:
get:
tags:
- pet
summary: Finds Pets by tags
description: 'Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.'
operationId: findPetsByTags
produces:
- application/xml
- application/json
parameters:
- name: tags
in: query
description: Tags to filter by
required: true
type: array
items:
type: string
collectionFormat: csv
responses:
'200':
description: successful operation
schema:
type: array
items:
$ref: '#/definitions/Pet'
'400':
description: Invalid tag value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
deprecated: true
'/pet/{petId}':
get:
tags:
- pet
summary: Find pet by ID
description: Returns a single pet
operationId: getPetById
produces:
- application/xml
- application/json
parameters:
- name: petId
in: path
description: ID of pet to return
required: true
type: integer
format: int64
responses:
'200':
description: successful operation
schema:
$ref: '#/definitions/Pet'
'400':
description: Invalid ID supplied
'404':
description: Pet not found
security:
- api_key: []
post:
tags:
- pet
summary: Updates a pet in the store with form data
description: ''
operationId: updatePetWithForm
consumes:
- application/x-www-form-urlencoded
produces:
- application/xml
- application/json
parameters:
- name: petId
in: path
description: ID of pet that needs to be updated
required: true
type: integer
format: int64
- name: name
in: formData
description: Updated name of the pet
required: false
type: string
- name: status
in: formData
description: Updated status of the pet
required: false
type: string
responses:
'405':
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
delete:
tags:
- pet
summary: Deletes a pet
description: ''
operationId: deletePet
produces:
- application/xml
- application/json
parameters:
- name: api_key
in: header
required: false
type: string
- name: petId
in: path
description: Pet id to delete
required: true
type: integer
format: int64
responses:
'400':
description: Invalid pet value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
'/pet/{petId}/uploadImage':
post:
tags:
- pet
summary: uploads an image
description: ''
operationId: uploadFile
consumes:
- multipart/form-data
produces:
- application/json
parameters:
- name: petId
in: path
description: ID of pet to update
required: true
type: integer
format: int64
- name: additionalMetadata
in: formData
description: Additional data to pass to server
required: false
type: string
- name: file
in: formData
description: file to upload
required: false
type: file
responses:
'200':
description: successful operation
schema:
$ref: '#/definitions/ApiResponse'
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
/store/inventory:
get:
tags:
- store
summary: Returns pet inventories by status
description: Returns a map of status codes to quantities
operationId: getInventory
produces:
- application/json
parameters: []
responses:
'200':
description: successful operation
schema:
type: object
additionalProperties:
type: integer
format: int32
security:
- api_key: []
/store/order:
post:
tags:
- store
summary: Place an order for a pet
description: ''
operationId: placeOrder
produces:
- application/xml
- application/json
parameters:
- in: body
name: body
description: order placed for purchasing the pet
required: true
schema:
$ref: '#/definitions/Order'
responses:
'200':
description: successful operation
schema:
$ref: '#/definitions/Order'
'400':
description: Invalid Order
'/store/order/{orderId}':
get:
tags:
- store
summary: Find purchase order by ID
description: 'For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions'
operationId: getOrderById
produces:
- application/xml
- application/json
parameters:
- name: orderId
in: path
description: ID of pet that needs to be fetched
required: true
type: integer
maximum: 5
minimum: 1
format: int64
responses:
'200':
description: successful operation
schema:
$ref: '#/definitions/Order'
'400':
description: Invalid ID supplied
'404':
description: Order not found
delete:
tags:
- store
summary: Delete purchase order by ID
description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
operationId: deleteOrder
produces:
- application/xml
- application/json
parameters:
- name: orderId
in: path
description: ID of the order that needs to be deleted
required: true
type: string
responses:
'400':
description: Invalid ID supplied
'404':
description: Order not found
/user:
post:
tags:
- user
summary: Create user
description: This can only be done by the logged in user.
operationId: createUser
produces:
- application/xml
- application/json
parameters:
- in: body
name: body
description: Created user object
required: true
schema:
$ref: '#/definitions/User'
responses:
default:
description: successful operation
/user/createWithArray:
post:
tags:
- user
summary: Creates list of users with given input array
description: ''
operationId: createUsersWithArrayInput
produces:
- application/xml
- application/json
parameters:
- in: body
name: body
description: List of user object
required: true
schema:
type: array
items:
$ref: '#/definitions/User'
responses:
default:
description: successful operation
/user/createWithList:
post:
tags:
- user
summary: Creates list of users with given input array
description: ''
operationId: createUsersWithListInput
produces:
- application/xml
- application/json
parameters:
- in: body
name: body
description: List of user object
required: true
schema:
type: array
items:
$ref: '#/definitions/User'
responses:
default:
description: successful operation
/user/login:
get:
tags:
- user
summary: Logs user into the system
description: ''
operationId: loginUser
produces:
- application/xml
- application/json
parameters:
- name: username
in: query
description: The user name for login
required: true
type: string
- name: password
in: query
description: The password for login in clear text
required: true
type: string
responses:
'200':
description: successful operation
schema:
type: string
headers:
X-Rate-Limit:
type: integer
format: int32
description: calls per hour allowed by the user
X-Expires-After:
type: string
format: date-time
description: date in UTC when toekn expires
'400':
description: Invalid username/password supplied
/user/logout:
get:
tags:
- user
summary: Logs out current logged in user session
description: ''
operationId: logoutUser
produces:
- application/xml
- application/json
parameters: []
responses:
default:
description: successful operation
'/user/{username}':
get:
tags:
- user
summary: Get user by user name
description: ''
operationId: getUserByName
produces:
- application/xml
- application/json
parameters:
- name: username
in: path
description: 'The name that needs to be fetched. Use user1 for testing.'
required: true
type: string
responses:
'200':
description: successful operation
schema:
$ref: '#/definitions/User'
'400':
description: Invalid username supplied
'404':
description: User not found
put:
tags:
- user
summary: Updated user
description: This can only be done by the logged in user.
operationId: updateUser
produces:
- application/xml
- application/json
parameters:
- name: username
in: path
description: name that need to be deleted
required: true
type: string
- in: body
name: body
description: Updated user object
required: true
schema:
$ref: '#/definitions/User'
responses:
'400':
description: Invalid user supplied
'404':
description: User not found
delete:
tags:
- user
summary: Delete user
description: This can only be done by the logged in user.
operationId: deleteUser
produces:
- application/xml
- application/json
parameters:
- name: username
in: path
description: The name that needs to be deleted
required: true
type: string
responses:
'400':
description: Invalid username supplied
'404':
description: User not found
securityDefinitions:
petstore_auth:
type: oauth2
authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog'
flow: implicit
scopes:
'write:pets': modify pets in your account
'read:pets': read your pets
api_key:
type: apiKey
name: api_key
in: header
definitions:
Order:
title: Pet Order
description: An order for a pets from the pet store
type: object
properties:
id:
type: integer
format: int64
petId:
type: integer
format: int64
quantity:
type: integer
format: int32
shipDate:
type: string
format: date-time
status:
type: string
description: Order Status
enum:
- placed
- approved
- delivered
complete:
type: boolean
default: false
xml:
name: Order
Category:
title: Pet category
description: A category for a pet
type: object
properties:
id:
type: integer
format: int64
name:
type: string
xml:
name: Category
User:
title: a User
description: A User who is purchasing from the pet store
type: object
properties:
id:
type: integer
format: int64
username:
type: string
firstName:
type: string
lastName:
type: string
email:
type: string
password:
type: string
phone:
type: string
userStatus:
type: integer
format: int32
description: User Status
xml:
name: User
Tag:
title: Pet Tag
description: A tag for a pet
type: object
properties:
id:
type: integer
format: int64
name:
type: string
xml:
name: Tag
Pet:
title: a Pet
description: A pet for sale in the pet store
type: object
required:
- name
- photoUrls
properties:
id:
type: integer
format: int64
category:
$ref: '#/definitions/Category'
name:
type: string
example: doggie
photoUrls:
type: array
xml:
name: photoUrl
wrapped: true
items:
type: string
tags:
type: array
xml:
name: tag
wrapped: true
items:
$ref: '#/definitions/Tag'
status:
type: string
description: pet status in the store
enum:
- available
- pending
- sold
xml:
name: Pet
ApiResponse:
title: An uploaded response
description: Describes the result of uploading an image resource
type: object
properties:
code:
type: integer
format: int32
type:
type: string
message:
type: string
#issue: https://github.com/swagger-api/swagger-codegen/issues/7980
Amount:
type: object
description: >
some description
properties:
value:
format: double
type: number
minimum: 0.01
maximum: 1000000000000000
description: >
some description
currency:
$ref: '#/definitions/Currency'
required:
- value
- currency
Currency:
type: string
pattern: '^[A-Z]{3,3}$'
description: >
some description
externalDocs:
description: Find out more about Swagger
url: 'http://swagger.io'

View File

@@ -431,7 +431,7 @@ public class RequestMessageBodyMatcherTests
// JSON match +++
{json, new RequestMessageBodyMatcher((object? o) => ((dynamic) o!).a == "b"), true},
{json, new RequestMessageBodyMatcher((string? s) => s == json), true},
{json, new RequestMessageBodyMatcher((byte[]? b) => b.SequenceEqual(Encoding.UTF8.GetBytes(json))), true},
{json, new RequestMessageBodyMatcher((byte[]? b) => b?.SequenceEqual(Encoding.UTF8.GetBytes(json)) == true), true},
// JSON no match ---
{json, new RequestMessageBodyMatcher((object? o) => false), false},
@@ -442,7 +442,7 @@ public class RequestMessageBodyMatcherTests
// string match +++
{str, new RequestMessageBodyMatcher((object? o) => o == null), true},
{str, new RequestMessageBodyMatcher((string? s) => s == str), true},
{str, new RequestMessageBodyMatcher((byte[]? b) => b.SequenceEqual(Encoding.UTF8.GetBytes(str))), true},
{str, new RequestMessageBodyMatcher((byte[]? b) => b?.SequenceEqual(Encoding.UTF8.GetBytes(str)) == true), true},
// string no match ---
{str, new RequestMessageBodyMatcher((object? o) => false), false},
@@ -453,13 +453,13 @@ public class RequestMessageBodyMatcherTests
// binary match +++
{bytes, new RequestMessageBodyMatcher((object? o) => o == null), true},
{bytes, new RequestMessageBodyMatcher((string? s) => s == null), true},
{bytes, new RequestMessageBodyMatcher((byte[]? b) => b.SequenceEqual(bytes)), true},
{bytes, new RequestMessageBodyMatcher((byte[]? b) => b?.SequenceEqual(bytes) == true), true},
// binary no match ---
{bytes, new RequestMessageBodyMatcher((object? o) => false), false},
{bytes, new RequestMessageBodyMatcher((string? s) => false), false},
{bytes, new RequestMessageBodyMatcher((byte[]? b) => false), false},
{bytes, new RequestMessageBodyMatcher(), false },
{bytes, new RequestMessageBodyMatcher(), false }
};
}
}

View File

@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;
using FluentAssertions;
using JsonConverter.Abstractions;
using Moq;
using Newtonsoft.Json;
using NFluent;
using WireMock.Matchers;
@@ -55,11 +58,31 @@ namespace WireMock.Net.Tests
Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
}
[Fact]
public void Request_WithBody_FuncFormUrlEncoded()
{
// Assign
var requestBuilder = Request.Create().UsingAnyMethod().WithBody((IDictionary<string, string>? values) => values != null);
// Act
var body = new BodyData
{
BodyAsFormUrlEncoded = new Dictionary<string, string>(),
DetectedBodyTypeFromContentType = BodyType.FormUrlEncoded,
DetectedBodyType = BodyType.FormUrlEncoded
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
// Assert
var requestMatchResult = new RequestMatchResult();
Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
}
[Fact]
public void Request_WithBody_FuncBodyData()
{
// Assign
var requestBuilder = Request.Create().UsingAnyMethod().WithBody((IBodyData b) => b != null);
var requestBuilder = Request.Create().UsingAnyMethod().WithBody((IBodyData? b) => b != null);
// Act
var body = new BodyData
@@ -78,7 +101,7 @@ namespace WireMock.Net.Tests
public void Request_WithBody_FuncByteArray()
{
// Assign
var requestBuilder = Request.Create().UsingAnyMethod().WithBody((byte[] b) => b != null);
var requestBuilder = Request.Create().UsingAnyMethod().WithBody((byte[]? b) => b != null);
// Act
var body = new BodyData
@@ -113,7 +136,7 @@ namespace WireMock.Net.Tests
}
[Fact]
public void Request_BodyAsString_Using_WildcardMatcher()
public void Request_WithBody_BodyDataAsString_Using_WildcardMatcher()
{
// Arrange
var spec = Request.Create().WithPath("/foo").UsingAnyMethod().WithBody(new WildcardMatcher("H*o*"));
@@ -132,7 +155,7 @@ namespace WireMock.Net.Tests
}
[Fact]
public void Request_BodyAsJson_Using_WildcardMatcher()
public void Request_WithBody_BodyDataAsJson_Using_WildcardMatcher()
{
// Arrange
var spec = Request.Create().WithPath("/foo").UsingAnyMethod().WithBody(new WildcardMatcher("*Hello*"));
@@ -328,6 +351,56 @@ namespace WireMock.Net.Tests
Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
}
[Fact]
public void Request_WithBodyAsJson_UsingObject()
{
// Assign
object body = new
{
Test = "abc"
};
var requestBuilder = Request.Create().UsingAnyMethod().WithBodyAsJson(body);
var bodyData = new BodyData
{
BodyAsString = JsonConvert.SerializeObject(body),
DetectedBodyType = BodyType.String
};
// Act
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, bodyData);
// Assert
var requestMatchResult = new RequestMatchResult();
Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
}
[Fact]
public void Request_WithBodyAsJson_WithIJsonConverter_UsingObject()
{
// Assign
var jsonConverterMock = new Mock<IJsonConverter>();
jsonConverterMock.Setup(j => j.Serialize(It.IsAny<object>(), It.IsAny<JsonConverterOptions>())).Returns("test");
object body = new
{
Any = "key"
};
var requestBuilder = Request.Create().UsingAnyMethod().WithBodyAsJson(body, jsonConverterMock.Object);
var bodyData = new BodyData
{
BodyAsString = "test",
DetectedBodyType = BodyType.String
};
// Act
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, bodyData);
// Assert
var requestMatchResult = new RequestMatchResult();
Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
}
[Theory]
[InlineData(new byte[] { 1 }, BodyType.Bytes)]
[InlineData(new byte[] { 48, 49, 50 }, BodyType.Bytes)]

View File

@@ -0,0 +1,62 @@
using System;
using System.Text;
using FluentAssertions;
using RandomDataGenerator.FieldOptions;
using RandomDataGenerator.Randomizers;
using WireMock.Util;
using Xunit;
namespace WireMock.Net.Tests.Util;
public class CompressionUtilsTests
{
[Theory]
[InlineData("gzip")]
[InlineData("deflate")]
public void CompressDecompress_ValidInput_ReturnsOriginalData(string contentEncoding)
{
// Arrange
byte[] data = Encoding.UTF8.GetBytes("Test data for compression");
// Act
byte[] compressedData = CompressionUtils.Compress(contentEncoding, data);
byte[] decompressedData = CompressionUtils.Decompress(contentEncoding, compressedData);
// Assert
decompressedData.Should().BeEquivalentTo(data);
}
[Theory]
[InlineData("gzip")]
[InlineData("deflate")]
public void Compress_ValidInput_ReturnsCompressedData(string contentEncoding)
{
// Arrange
var text = RandomizerFactory.GetRandomizer(new FieldOptionsTextRegex { Pattern = "[0-9A-Z]{1000}" }).Generate()!;
byte[] data = Encoding.UTF8.GetBytes(text);
// Act
byte[] compressedData = CompressionUtils.Compress(contentEncoding, data);
// Assert
compressedData.Length.Should().BeLessThan(data.Length);
}
[Fact]
public void CompressDecompress_InvalidContentEncoding_ThrowsNotSupportedException()
{
// Arrange
byte[] data = Encoding.UTF8.GetBytes("Test data for compression");
string contentEncoding = "invalid";
// Act
Action compressAction = () => CompressionUtils.Compress(contentEncoding, data);
Action decompressAction = () => CompressionUtils.Decompress(contentEncoding, data);
// Assert
compressAction.Should().Throw<NotSupportedException>()
.WithMessage($"ContentEncoding '{contentEncoding}' is not supported.");
decompressAction.Should().Throw<NotSupportedException>()
.WithMessage($"ContentEncoding '{contentEncoding}' is not supported.");
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.Globalization;
using FluentAssertions;
using WireMock.Util;
using Xunit;
namespace WireMock.Net.Tests.Util;
public class CultureInfoUtilsTests
{
[Theory]
[InlineData(null, typeof(CultureInfo))]
[InlineData("en-US", typeof(CultureInfo))]
[InlineData("fr-FR", typeof(CultureInfo))]
[InlineData("es-ES", typeof(CultureInfo))]
public void Parse_ValidInputs_ReturnsExpectedCultureInfo(string? value, Type expectedType)
{
// Act
var result = CultureInfoUtils.Parse(value);
// Assert
result.Should().BeOfType(expectedType);
}
[Theory]
[InlineData("InvalidCulture")]
[InlineData("123456")]
public void Parse_InvalidInputs_ReturnsCurrentCulture(string? value)
{
// Arrange
var expectedCulture = CultureInfo.CurrentCulture;
// Act
var result = CultureInfoUtils.Parse(value);
// Assert
result.Should().Be(expectedCulture);
}
#if !NETSTANDARD1_3
[Fact]
public void Parse_IntegerInput_ReturnsExpectedCultureInfo()
{
// Arrange
string value = "1033"; // en-US culture identifier
var expectedCulture = new CultureInfo(1033);
// Act
var result = CultureInfoUtils.Parse(value);
// Assert
result.Should().Be(expectedCulture);
}
#endif
[Fact]
public void Parse_CurrentCultureInput_ReturnsCurrentCulture()
{
// Arrange
string value = nameof(CultureInfo.CurrentCulture);
var expectedCulture = CultureInfo.CurrentCulture;
// Act
var result = CultureInfoUtils.Parse(value);
// Assert
result.Should().Be(expectedCulture);
}
[Fact]
public void Parse_InvariantCultureInput_ReturnsInvariantCulture()
{
// Arrange
string value = nameof(CultureInfo.InvariantCulture);
var expectedCulture = CultureInfo.InvariantCulture;
// Act
var result = CultureInfoUtils.Parse(value);
// Assert
result.Should().Be(expectedCulture);
}
}

View File

@@ -1,5 +1,5 @@
using FluentAssertions;
using System.Collections.Generic;
using FluentAssertions;
using WireMock.Types;
using WireMock.Util;
using Xunit;
@@ -8,6 +8,51 @@ namespace WireMock.Net.Tests.Util;
public class QueryStringParserTests
{
public static IEnumerable<object?[]> QueryStringTestData => new List<object?[]>
{
new object?[] { null, false, false, null },
new object?[] { string.Empty, false, true, new Dictionary<string, string>() },
new object?[] { "test", false, true, new Dictionary<string, string>() },
new object?[] { "&", false, true, new Dictionary<string, string>() },
new object?[] { "&&", false, true, new Dictionary<string, string>() },
new object?[] { "a=", false, true, new Dictionary<string, string> { { "a", "" } } },
new object?[] { "&a", false, true, new Dictionary<string, string>() },
new object?[] { "&a=", false, true, new Dictionary<string, string> { { "a", "" } } },
new object?[] { "&key1=value1", false, true, new Dictionary<string, string> { { "key1", "value1" } } },
new object?[] { "key1=value1", false, true, new Dictionary<string, string> { { "key1", "value1" } } },
new object?[] { "key1=value1&key2=value2", false, true, new Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } } },
new object?[] { "key1=value1&key2=value2&", false, true, new Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } } },
new object?[] { "key1=value1&&key2=value2", false, true, new Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } } },
new object?[] { "&key1=value1&key2=value2&&", false, true, new Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } } },
};
[Theory]
[MemberData(nameof(QueryStringTestData))]
public void TryParse_Should_Parse_QueryString(string queryString, bool caseIgnore, bool expectedResult, IDictionary<string, string> expectedOutput)
{
// Act
var result = QueryStringParser.TryParse(queryString, caseIgnore, out var actual);
// Assert
result.Should().Be(expectedResult);
actual.Should().BeEquivalentTo(expectedOutput);
}
[Fact]
public void TryParse_Should_Parse_QueryStringWithUrlEncodedValues()
{
// Arrange
var key = "x";
var value = "rNaCP7hv8UOmS%2FJcujdvLw%3D%3D";
// Act
var result = QueryStringParser.TryParse($"{key}={value}", true, out var actual);
// Assert
result.Should().BeTrue();
actual.Should().BeEquivalentTo(new Dictionary<string, string> { { "x", "rNaCP7hv8UOmS/JcujdvLw==" } });
}
[Fact]
public void Parse_WithNullString()
{

View File

@@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using FluentAssertions;
using WireMock.Util;
using Xunit;
namespace WireMock.Net.Tests.Util;
public class RegexUtilsTests
{
[Fact]
public void GetNamedGroups_ValidRegexWithNamedGroups_ReturnsNamedGroupsDictionary()
{
// Arrange
var pattern = @"^(?<street>\w+)\s(?<number>\d+)$";
var input = "MainStreet 123";
var regex = new Regex(pattern);
// Act
var namedGroupsDictionary = RegexUtils.GetNamedGroups(regex, input);
// Assert
namedGroupsDictionary.Should().NotBeEmpty()
.And.Contain(new KeyValuePair<string, string>("street", "MainStreet"))
.And.Contain(new KeyValuePair<string, string>("number", "123"));
}
[Theory]
[InlineData("", "test", false, false)]
[InlineData(null, "test", false, false)]
[InlineData(".*", "test", true, true)]
[InlineData("invalid[", "test", false, false)]
public void MatchRegex_WithVariousPatterns_ReturnsExpectedResults(
string? pattern, string input, bool expectedIsValid, bool expectedResult)
{
// Act
var (isValidResult, matchResult) = RegexUtils.MatchRegex(pattern, input);
// Assert
isValidResult.Should().Be(expectedIsValid);
matchResult.Should().Be(expectedResult);
}
[Theory]
[InlineData("", "test", false, false)]
[InlineData(null, "test", false, false)]
[InlineData(".*", "test", true, true)]
[InlineData("invalid[", "test", false, false)]
public void MatchRegex_WithVariousPatternsAndExtendedRegex_ReturnsExpectedResults(
string? pattern, string input, bool expectedIsValid, bool expectedResult)
{
// Act
var (isValidResult, matchResult) = RegexUtils.MatchRegex(pattern, input, useRegexExtended: true);
// Assert
isValidResult.Should().Be(expectedIsValid);
matchResult.Should().Be(expectedResult);
}
}

View File

@@ -64,7 +64,7 @@
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.3.1-preview-02" />
<!--<PackageReference Include="System.Linq.Dynamic.Core" Version="1.3.1" />-->
<PackageReference Include="System.Threading" Version="4.3.0" />
<PackageReference Include="RestEase" Version="1.5.7" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
@@ -103,6 +103,12 @@
</ItemGroup>
<ItemGroup>
<None Update="OpenApiParser\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="OpenApiParser\*.yml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="responsebody.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

View File

@@ -1,5 +1,6 @@
#if !(NET452 || NET461 || NETCOREAPP3_1)
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
@@ -760,5 +761,81 @@ public class WireMockAdminApiTests
server.Stop();
}
[Fact]
public async Task IWireMockAdminApi_OpenApiConvert_Yml()
{
// Arrange
var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore.yml"));
var server = WireMockServer.StartWithAdminInterface();
var api = RestClient.For<IWireMockAdminApi>(server.Url);
// Act
var mappings = await api.OpenApiConvertAsync(openApiDocument).ConfigureAwait(false);
// Assert
server.MappingModels.Should().BeEmpty();
mappings.Should().HaveCount(20);
server.Stop();
}
[Fact]
public async Task IWireMockAdminApi_OpenApiConvert_Json()
{
// Arrange
var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore-openapi3.json"));
var server = WireMockServer.StartWithAdminInterface();
var api = RestClient.For<IWireMockAdminApi>(server.Url);
// Act
var mappings = await api.OpenApiConvertAsync(openApiDocument).ConfigureAwait(false);
// Assert
server.MappingModels.Should().BeEmpty();
mappings.Should().HaveCount(19);
server.Stop();
}
[Fact]
public async Task IWireMockAdminApi_OpenApiSave_Json()
{
// Arrange
var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore-openapi3.json"));
var server = WireMockServer.StartWithAdminInterface();
var api = RestClient.For<IWireMockAdminApi>(server.Url);
// Act
var statusModel = await api.OpenApiSaveAsync(openApiDocument).ConfigureAwait(false);
// Assert
statusModel.Status.Should().Be("OpenApi document converted to Mappings");
server.MappingModels.Should().HaveCount(19);
server.Stop();
}
[Fact]
public async Task IWireMockAdminApi_OpenApiSave_Yml()
{
// Arrange
var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore.yml"));
var server = WireMockServer.StartWithAdminInterface();
var api = RestClient.For<IWireMockAdminApi>(server.Url);
// Act
var mappings = await api.OpenApiConvertAsync(openApiDocument).ConfigureAwait(false);
// Assert
server.MappingModels.Should().BeEmpty();
mappings.Should().HaveCount(20);
server.Stop();
}
}
#endif

View File

@@ -117,7 +117,7 @@ public class WireMockServerProxyTests
}
// Assert
server.Mappings.Should().HaveCount(32);
server.Mappings.Should().HaveCount(34);
}
[Fact]
@@ -528,6 +528,48 @@ public class WireMockServerProxyTests
Check.That(matchers).Contains("GoodCookie");
}
[Fact]
public async Task WireMockServer_Proxy_Should_exclude_ExcludedParams_in_mapping()
{
// Assign
string path = $"/prx_{Guid.NewGuid()}";
var serverForProxyForwarding = WireMockServer.Start();
serverForProxyForwarding
.Given(Request.Create().WithPath(path))
.RespondWith(Response.Create());
var settings = new WireMockServerSettings
{
ProxyAndRecordSettings = new ProxyAndRecordSettings
{
Url = serverForProxyForwarding.Urls[0],
SaveMapping = true,
SaveMappingToFile = false,
ExcludedParams = new[] { "timestamp" }
}
};
var server = WireMockServer.Start(settings);
var defaultMapping = server.Mappings.First();
var param01 = "?timestamp=2023-03-23";
var param02 = "&name=person";
// Act
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri($"{server.Urls[0]}{path}{param01}{param02}"),
Content = new StringContent("stringContent"),
};
await new HttpClient().SendAsync(requestMessage).ConfigureAwait(false);
// Assert
var mapping = server.Mappings.FirstOrDefault(m => m.Guid != defaultMapping.Guid);
Check.That(mapping).IsNotNull();
var matchers = ((Request)mapping.RequestMatcher).GetRequestMessageMatchers<RequestMessageParamMatcher>().Select(m => m.Key).ToList();
Check.That(matchers).Not.Contains("timestamp");
Check.That(matchers).Contains("name");
}
[Fact]
public async Task WireMockServer_Proxy_Should_preserve_content_header_in_proxied_request_with_empty_content()
{

View File

@@ -81,7 +81,7 @@ public class WireMockServerSettingsTests
// Assert
server.Mappings.Should().NotBeNull();
server.Mappings.Should().HaveCount(30);
server.Mappings.Should().HaveCount(32);
server.Mappings.All(m => m.Priority == WireMockConstants.AdminPriority).Should().BeTrue();
}
@@ -100,9 +100,9 @@ public class WireMockServerSettingsTests
// Assert
server.Mappings.Should().NotBeNull();
server.Mappings.Should().HaveCount(31);
server.Mappings.Should().HaveCount(33);
server.Mappings.Count(m => m.Priority == WireMockConstants.AdminPriority).Should().Be(30);
server.Mappings.Count(m => m.Priority == WireMockConstants.AdminPriority).Should().Be(32);
server.Mappings.Count(m => m.Priority == WireMockConstants.ProxyPriority).Should().Be(1);
}

View File

@@ -1,7 +1,9 @@
#if !NET452
//using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
//using System.Net.Http.Json;
using System.Threading.Tasks;
using FluentAssertions;
using WireMock.Matchers;
@@ -10,61 +12,120 @@ using WireMock.ResponseBuilders;
using WireMock.Server;
using Xunit;
namespace WireMock.Net.Tests
namespace WireMock.Net.Tests;
public partial class WireMockServerTests
{
public partial class WireMockServerTests
public class DummyClass
{
public class DummyClass
{
public string Hi { get; set; }
}
public string? Hi { get; set; }
}
[Fact]
public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_WildcardMatcher_ShouldMatch()
[Fact]
public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_WildcardMatcher_ShouldMatch()
{
// Arrange
var server = WireMockServer.Start();
server.Given(
Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*"))
)
.RespondWith(
Response.Create().WithStatusCode(200)
);
var jsonObject = new DummyClass
{
// Arrange
var server = WireMockServer.Start();
server.Given(
Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*"))
Hi = "Hello World!"
};
// Act
var response = await new HttpClient().PostAsJsonAsync("http://localhost:" + server.Ports[0] + "/foo", jsonObject).ConfigureAwait(false);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
server.Stop();
}
[Fact]
public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_WildcardMatcher_ShouldMatch()
{
// Arrange
var server = WireMockServer.Start();
server.Given(
Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*"))
)
.RespondWith(
Response.Create().WithStatusCode(200)
);
// Act
var response = await new HttpClient().PostAsync("http://localhost:" + server.Ports[0] + "/foo", new StringContent("{ Hi = \"Hello World\" }")).ConfigureAwait(false);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
server.Stop();
}
[Fact]
public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFunc()
{
// Arrange
var server = WireMockServer.Start();
server.Given(
Request.Create()
.UsingPost()
.WithPath("/foo")
.WithBody(values => values != null && values["key1"] == "value1")
)
.RespondWith(
Response.Create()
);
// Act
var content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("key1", "value1") });
var response = await new HttpClient()
.PostAsync($"{server.Url}/foo", content)
.ConfigureAwait(false);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
server.Stop();
}
[Fact]
public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithExactMatcher()
{
// Arrange
var server = WireMockServer.Start();
server.Given(
Request.Create()
.UsingPost()
.WithPath("/foo")
.WithHeader("Content-Type", "application/x-www-form-urlencoded")
.WithBody(new ExactMatcher("name=John+Doe&email=johndoe%40example.com")
)
.RespondWith(
Response.Create().WithStatusCode(200)
);
)
.RespondWith(
Response.Create()
);
var jsonObject = new DummyClass
{
Hi = "Hello World!"
};
// Act
var response = await new HttpClient().PostAsJsonAsync("http://localhost:" + server.Ports[0] + "/foo", jsonObject).ConfigureAwait(false);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
server.Stop();
}
[Fact]
public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_WildcardMatcher_ShouldMatch()
// Act
var content = new FormUrlEncodedContent(new[]
{
// Arrange
var server = WireMockServer.Start();
server.Given(
Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*"))
)
.RespondWith(
Response.Create().WithStatusCode(200)
);
new KeyValuePair<string, string>("name", "John Doe"),
new KeyValuePair<string, string>("email", "johndoe@example.com")
});
var response = await new HttpClient()
.PostAsync($"{server.Url}/foo", content)
.ConfigureAwait(false);
// Act
var response = await new HttpClient().PostAsync("http://localhost:" + server.Ports[0] + "/foo", new StringContent("{ Hi = \"Hello World\" }")).ConfigureAwait(false);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
server.Stop();
}
server.Stop();
}
}
#endif