diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..07c6fa08 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (WireMock.Net.StandAlone.NETCoreApp)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build_WireMock.Net.StandAlone.NETCoreApp", + "program": "${workspaceRoot}/examples/WireMock.Net.StandAlone.NETCoreApp/bin/Debug/netcoreapp2.0/WireMock.Net.StandAlone.NETCoreApp.dll", + "args": [], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "console": "internalConsole" + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..ae6d8b7a --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "taskName": "build_WireMock.Net.StandAlone.NETCoreApp", + "command": "dotnet build ${workspaceRoot}/examples/WireMock.Net.StandAlone.NETCoreApp/WireMock.Net.StandAlone.NETCoreApp.csproj -f netcoreapp2.0 ", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/examples/WireMock.Net.Client/Program.cs b/examples/WireMock.Net.Client/Program.cs index 09dbaedb..6bce8c55 100644 --- a/examples/WireMock.Net.Client/Program.cs +++ b/examples/WireMock.Net.Client/Program.cs @@ -12,7 +12,7 @@ namespace WireMock.Net.Client static void Main(string[] args) { // Create an implementation of the IFluentMockServerAdmin and pass in the base URL for the API. - var api = RestClient.For("http://localhost:9090"); + var api = RestClient.For("http://localhost:9091"); // Set BASIC Auth var value = Convert.ToBase64String(Encoding.ASCII.GetBytes("a:b")); diff --git a/examples/WireMock.Net.Console.Record.NETCoreApp/WireMock.Net.Console.Record.NETCoreApp.csproj b/examples/WireMock.Net.Console.Record.NETCoreApp/WireMock.Net.Console.Record.NETCoreApp.csproj index ce5ea4e7..61a866e7 100644 --- a/examples/WireMock.Net.Console.Record.NETCoreApp/WireMock.Net.Console.Record.NETCoreApp.csproj +++ b/examples/WireMock.Net.Console.Record.NETCoreApp/WireMock.Net.Console.Record.NETCoreApp.csproj @@ -12,8 +12,7 @@ - - + \ No newline at end of file diff --git a/examples/WireMock.Net.ConsoleApplication/MainApp.cs b/examples/WireMock.Net.ConsoleApplication/MainApp.cs index 4001ae4a..4d4c7d97 100644 --- a/examples/WireMock.Net.ConsoleApplication/MainApp.cs +++ b/examples/WireMock.Net.ConsoleApplication/MainApp.cs @@ -13,8 +13,8 @@ namespace WireMock.Net.ConsoleApplication { public static void Run() { - string url1 = "http://localhost:9090/"; - string url2 = "http://localhost:9091/"; + string url1 = "http://localhost:9091/"; + string url2 = "http://localhost:9092/"; string url3 = "https://localhost:9443/"; var server = FluentMockServer.Start(new FluentMockServerSettings diff --git a/examples/WireMock.Net.StandAlone.NETCoreApp/Program.cs b/examples/WireMock.Net.StandAlone.NETCoreApp/Program.cs index c53df7b9..5871bf0a 100644 --- a/examples/WireMock.Net.StandAlone.NETCoreApp/Program.cs +++ b/examples/WireMock.Net.StandAlone.NETCoreApp/Program.cs @@ -1,15 +1,42 @@ using System; +using System.Threading; +using WireMock.Server; namespace WireMock.Net.StandAlone.NETCoreApp { class Program { + private static int sleepTime = 30000; + private static FluentMockServer server; + static void Main(string[] args) { - StandAloneApp.Start(args); + server = StandAloneApp.Start(args); - Console.WriteLine("Press any key to stop the server"); - Console.ReadKey(); + Console.WriteLine($"{DateTime.UtcNow} Press Ctrl+C to shut down"); + + System.Console.CancelKeyPress += (s,e) => + { + Stop("CancelKeyPress"); + }; + + System.Runtime.Loader.AssemblyLoadContext.Default.Unloading += ctx => + { + Stop("AssemblyLoadContext.Default.Unloading"); + }; + + while(true) + { + Console.WriteLine($"{DateTime.UtcNow} WireMock.Net server running"); + Thread.Sleep(sleepTime); + } + } + + private static void Stop(string why) + { + Console.WriteLine($"{DateTime.UtcNow} WireMock.Net server stopping because '{why}'"); + server.Stop(); + Console.WriteLine($"{DateTime.UtcNow} WireMock.Net server stopped"); } } } \ No newline at end of file diff --git a/examples/WireMock.Net.StandAlone.NETCoreApp/Properties/launchSettings.json b/examples/WireMock.Net.StandAlone.NETCoreApp/Properties/launchSettings.json index c52dce78..72fecc44 100644 --- a/examples/WireMock.Net.StandAlone.NETCoreApp/Properties/launchSettings.json +++ b/examples/WireMock.Net.StandAlone.NETCoreApp/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "WireMock.Net.StandAlone.NETCoreApp": { "commandName": "Project", - "commandLineArgs": "--Urls http://*:9090" + "commandLineArgs": "--Urls http://*:9091" } } } \ No newline at end of file diff --git a/examples/WireMock.Net.StandAlone.NETCoreApp/WireMock.Net.StandAlone.NETCoreApp.csproj b/examples/WireMock.Net.StandAlone.NETCoreApp/WireMock.Net.StandAlone.NETCoreApp.csproj index a3d52cc2..24dd3352 100644 --- a/examples/WireMock.Net.StandAlone.NETCoreApp/WireMock.Net.StandAlone.NETCoreApp.csproj +++ b/examples/WireMock.Net.StandAlone.NETCoreApp/WireMock.Net.StandAlone.NETCoreApp.csproj @@ -2,8 +2,8 @@ Exe - netcoreapp1.0 - 1.0.1 + netcoreapp1.0;netcoreapp2.0 + ../../WireMock.Net-Logo.ico diff --git a/src/WireMock.Net.StandAlone/StandAloneApp.cs b/src/WireMock.Net.StandAlone/StandAloneApp.cs index 9ce3c969..a8482873 100644 --- a/src/WireMock.Net.StandAlone/StandAloneApp.cs +++ b/src/WireMock.Net.StandAlone/StandAloneApp.cs @@ -43,6 +43,12 @@ namespace WireMock.Net.StandAlone [ValueArgument(typeof(string), "AdminPassword", Description = "The password needed for __admin access.", Optional = true)] public string AdminPassword { get; set; } + + [ValueArgument(typeof(int?), "RequestLogExpirationDuration", Description = "The RequestLog expiration in hours (optional).", Optional = true)] + public int? RequestLogExpirationDuration { get; set; } + + [ValueArgument(typeof(int?), "MaxRequestLogCount", Description = "The MaxRequestLog count (optional).", Optional = true)] + public int? MaxRequestLogCount { get; set; } } /// @@ -76,7 +82,7 @@ namespace WireMock.Net.StandAlone if (!options.Urls.Any()) { - options.Urls.Add("http://localhost:9090/"); + options.Urls.Add("http://localhost:9091/"); } var settings = new FluentMockServerSettings @@ -86,7 +92,9 @@ namespace WireMock.Net.StandAlone ReadStaticMappings = options.ReadStaticMappings, AllowPartialMapping = options.AllowPartialMapping, AdminUsername = options.AdminUsername, - AdminPassword = options.AdminPassword + AdminPassword = options.AdminPassword, + RequestLogExpirationDuration = options.RequestLogExpirationDuration, + MaxRequestLogCount = options.MaxRequestLogCount }; if (!string.IsNullOrEmpty(options.ProxyURL)) diff --git a/src/WireMock.Net.StandAlone/WireMock.Net.StandAlone.csproj b/src/WireMock.Net.StandAlone/WireMock.Net.StandAlone.csproj index 8dfc987d..1cb729db 100644 --- a/src/WireMock.Net.StandAlone/WireMock.Net.StandAlone.csproj +++ b/src/WireMock.Net.StandAlone/WireMock.Net.StandAlone.csproj @@ -18,7 +18,7 @@ git https://github.com/WireMock-Net/WireMock.Net True - full + portable ../../WireMock.Net-Logo.ico WireMock.Net.StandAlone diff --git a/src/WireMock.Net/Admin/Settings/SettingsModel.cs b/src/WireMock.Net/Admin/Settings/SettingsModel.cs index 7e20ef54..1a5a77d4 100644 --- a/src/WireMock.Net/Admin/Settings/SettingsModel.cs +++ b/src/WireMock.Net/Admin/Settings/SettingsModel.cs @@ -14,5 +14,15 @@ /// Gets or sets if partial mapping is allowed. /// public bool? AllowPartialMapping { get; set; } + + /// + /// Gets or sets the RequestLog expiration in hours + /// + public int? RequestLogExpirationDuration { get; set; } + + /// + /// Gets or sets the MaxRequestLog count. + /// + public int? MaxRequestLogCount { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Owin/WireMockMiddleware.cs b/src/WireMock.Net/Owin/WireMockMiddleware.cs index 8d0d0488..529082d6 100644 --- a/src/WireMock.Net/Owin/WireMockMiddleware.cs +++ b/src/WireMock.Net/Owin/WireMockMiddleware.cs @@ -1,143 +1,157 @@ -using System; -using System.Collections; -using System.Threading.Tasks; -using WireMock.Logging; -using WireMock.Matchers.Request; -using System.Linq; -#if !NETSTANDARD -using Microsoft.Owin; -#else -using Microsoft.AspNetCore.Http; -#endif - -namespace WireMock.Owin -{ -#if !NETSTANDARD - internal class WireMockMiddleware : OwinMiddleware -#else - internal class WireMockMiddleware -#endif - { - private static readonly Task CompletedTask = Task.FromResult(false); - private readonly WireMockMiddlewareOptions _options; - - private readonly OwinRequestMapper _requestMapper = new OwinRequestMapper(); - private readonly OwinResponseMapper _responseMapper = new OwinResponseMapper(); - -#if !NETSTANDARD - public WireMockMiddleware(OwinMiddleware next, WireMockMiddlewareOptions options) : base(next) - { - _options = options; - } -#else - public WireMockMiddleware(RequestDelegate next, WireMockMiddlewareOptions options) - { - _options = options; - } -#endif - -#if !NETSTANDARD - public override async Task Invoke(IOwinContext ctx) -#else - public async Task Invoke(HttpContext ctx) -#endif - { - var request = await _requestMapper.MapAsync(ctx.Request); - - ResponseMessage response = null; - Mapping targetMapping = null; - RequestMatchResult requestMatchResult = null; - try - { - var mappings = _options.Mappings - .Select(m => new - { - Mapping = m, - MatchResult = m.IsRequestHandled(request) - }) - .ToList(); - - if (_options.AllowPartialMapping) - { - var partialMappings = mappings - .Where(pm => pm.Mapping.IsAdminInterface && pm.MatchResult.IsPerfectMatch || !pm.Mapping.IsAdminInterface) - .OrderBy(m => m.MatchResult) - .ThenBy(m => m.Mapping.Priority) - .ToList(); - - var bestPartialMatch = partialMappings.FirstOrDefault(pm => pm.MatchResult.AverageTotalScore > 0.0); - - targetMapping = bestPartialMatch?.Mapping; - requestMatchResult = bestPartialMatch?.MatchResult; - } - else - { - var perfectMatch = mappings - .OrderBy(m => m.Mapping.Priority) - .FirstOrDefault(m => m.MatchResult.IsPerfectMatch); - - targetMapping = perfectMatch?.Mapping; - requestMatchResult = perfectMatch?.MatchResult; - } - - if (targetMapping == null) - { - response = new ResponseMessage { StatusCode = 404, Body = "No matching mapping found" }; - return; - } - - if (targetMapping.IsAdminInterface && _options.AuthorizationMatcher != null) - { - string authorization; - bool present = request.Headers.TryGetValue("Authorization", out authorization); - if (!present || _options.AuthorizationMatcher.IsMatch(authorization) < 1.0) - { - response = new ResponseMessage { StatusCode = 401 }; - return; - } - } - - if (!targetMapping.IsAdminInterface && _options.RequestProcessingDelay > TimeSpan.Zero) - { - await Task.Delay(_options.RequestProcessingDelay.Value); - } - - response = await targetMapping.ResponseToAsync(request); - } - catch (Exception ex) - { - response = new ResponseMessage { StatusCode = 500, Body = ex.ToString() }; - } - finally - { - var log = new LogEntry - { - Guid = Guid.NewGuid(), - RequestMessage = request, - ResponseMessage = response, - MappingGuid = targetMapping?.Guid, - MappingTitle = targetMapping?.Title, - RequestMatchResult = requestMatchResult - }; - - LogRequest(log); - - await _responseMapper.MapAsync(response, ctx.Response); - } - - await CompletedTask; - } - - /// - /// The log request. - /// - /// The request. - private void LogRequest(LogEntry entry) - { - lock (((ICollection)_options.LogEntries).SyncRoot) - { - _options.LogEntries.Add(entry); - } - } - } +using System; +using System.Collections; +using System.Threading.Tasks; +using WireMock.Logging; +using WireMock.Matchers.Request; +using System.Linq; +#if !NETSTANDARD +using Microsoft.Owin; +#else +using Microsoft.AspNetCore.Http; +#endif + +namespace WireMock.Owin +{ +#if !NETSTANDARD + internal class WireMockMiddleware : OwinMiddleware +#else + internal class WireMockMiddleware +#endif + { + private static readonly Task CompletedTask = Task.FromResult(false); + private readonly WireMockMiddlewareOptions _options; + + private readonly OwinRequestMapper _requestMapper = new OwinRequestMapper(); + private readonly OwinResponseMapper _responseMapper = new OwinResponseMapper(); + +#if !NETSTANDARD + public WireMockMiddleware(OwinMiddleware next, WireMockMiddlewareOptions options) : base(next) + { + _options = options; + } +#else + public WireMockMiddleware(RequestDelegate next, WireMockMiddlewareOptions options) + { + _options = options; + } +#endif + +#if !NETSTANDARD + public override async Task Invoke(IOwinContext ctx) +#else + public async Task Invoke(HttpContext ctx) +#endif + { + var request = await _requestMapper.MapAsync(ctx.Request); + + bool logRequest = false; + ResponseMessage response = null; + Mapping targetMapping = null; + RequestMatchResult requestMatchResult = null; + try + { + var mappings = _options.Mappings + .Select(m => new + { + Mapping = m, + MatchResult = m.IsRequestHandled(request) + }) + .ToList(); + + if (_options.AllowPartialMapping) + { + var partialMappings = mappings + .Where(pm => pm.Mapping.IsAdminInterface && pm.MatchResult.IsPerfectMatch || !pm.Mapping.IsAdminInterface) + .OrderBy(m => m.MatchResult) + .ThenBy(m => m.Mapping.Priority) + .ToList(); + + var bestPartialMatch = partialMappings.FirstOrDefault(pm => pm.MatchResult.AverageTotalScore > 0.0); + + targetMapping = bestPartialMatch?.Mapping; + requestMatchResult = bestPartialMatch?.MatchResult; + } + else + { + var perfectMatch = mappings + .OrderBy(m => m.Mapping.Priority) + .FirstOrDefault(m => m.MatchResult.IsPerfectMatch); + + targetMapping = perfectMatch?.Mapping; + requestMatchResult = perfectMatch?.MatchResult; + } + + if (targetMapping == null) + { + logRequest = true; + response = new ResponseMessage { StatusCode = 404, Body = "No matching mapping found" }; + return; + } + + logRequest = !targetMapping.IsAdminInterface; + + if (targetMapping.IsAdminInterface && _options.AuthorizationMatcher != null) + { + string authorization; + bool present = request.Headers.TryGetValue("Authorization", out authorization); + if (!present || _options.AuthorizationMatcher.IsMatch(authorization) < 1.0) + { + response = new ResponseMessage { StatusCode = 401 }; + return; + } + } + + if (!targetMapping.IsAdminInterface && _options.RequestProcessingDelay > TimeSpan.Zero) + { + await Task.Delay(_options.RequestProcessingDelay.Value); + } + + response = await targetMapping.ResponseToAsync(request); + } + catch (Exception ex) + { + response = new ResponseMessage { StatusCode = 500, Body = ex.ToString() }; + } + finally + { + var log = new LogEntry + { + Guid = Guid.NewGuid(), + RequestMessage = request, + ResponseMessage = response, + MappingGuid = targetMapping?.Guid, + MappingTitle = targetMapping?.Title, + RequestMatchResult = requestMatchResult + }; + + LogRequest(log, logRequest); + + await _responseMapper.MapAsync(response, ctx.Response); + } + + await CompletedTask; + } + + private void LogRequest(LogEntry entry, bool addRequest) + { + lock (((ICollection)_options.LogEntries).SyncRoot) + { + if (addRequest) + { + _options.LogEntries.Add(entry); + } + + if (_options.MaxRequestLogCount != null) + { + _options.LogEntries = _options.LogEntries.Skip(_options.LogEntries.Count - _options.MaxRequestLogCount.Value).ToList(); + } + + if (_options.RequestLogExpirationDuration != null) + { + var checkTime = DateTime.Now.AddHours(-_options.RequestLogExpirationDuration.Value); + _options.LogEntries = _options.LogEntries.Where(le => le.RequestMessage.DateTime > checkTime).ToList(); + } + } + } + } } \ No newline at end of file diff --git a/src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs b/src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs index 070e7d6a..f6cd3a17 100644 --- a/src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs +++ b/src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs @@ -16,6 +16,10 @@ namespace WireMock.Owin public IList Mappings { get; set; } public IList LogEntries { get; set; } + + public int? RequestLogExpirationDuration { get; set; } + + public int? MaxRequestLogCount { get; set; } public WireMockMiddlewareOptions() { diff --git a/src/WireMock.Net/Server/FluentMockServer.Admin.cs b/src/WireMock.Net/Server/FluentMockServer.Admin.cs index 9f945375..ad2efc3f 100644 --- a/src/WireMock.Net/Server/FluentMockServer.Admin.cs +++ b/src/WireMock.Net/Server/FluentMockServer.Admin.cs @@ -161,6 +161,8 @@ namespace WireMock.Server var model = new SettingsModel { AllowPartialMapping = _options.AllowPartialMapping, + MaxRequestLogCount = _options.MaxRequestLogCount, + RequestLogExpirationDuration = _options.RequestLogExpirationDuration, GlobalProcessingDelay = (int?)_options.RequestProcessingDelay?.TotalMilliseconds }; @@ -174,6 +176,10 @@ namespace WireMock.Server if (settings.AllowPartialMapping != null) _options.AllowPartialMapping = settings.AllowPartialMapping.Value; + _options.MaxRequestLogCount = settings.MaxRequestLogCount; + + _options.RequestLogExpirationDuration = settings.RequestLogExpirationDuration; + if (settings.GlobalProcessingDelay != null) _options.RequestProcessingDelay = TimeSpan.FromMilliseconds(settings.GlobalProcessingDelay.Value); diff --git a/src/WireMock.Net/Server/FluentMockServer.cs b/src/WireMock.Net/Server/FluentMockServer.cs index 88239447..7e953987 100644 --- a/src/WireMock.Net/Server/FluentMockServer.cs +++ b/src/WireMock.Net/Server/FluentMockServer.cs @@ -1,347 +1,381 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using JetBrains.Annotations; -using WireMock.Http; -using WireMock.Matchers; -using WireMock.Matchers.Request; -using WireMock.RequestBuilders; -using WireMock.Settings; -using WireMock.Validation; -using WireMock.Owin; - -namespace WireMock.Server -{ - /// - /// The fluent mock server. - /// - public partial class FluentMockServer : IDisposable - { - private readonly IOwinSelfHost _httpServer; - private readonly object _syncRoot = new object(); - private readonly WireMockMiddlewareOptions _options = new WireMockMiddlewareOptions(); - - /// - /// Gets the ports. - /// - /// - /// The ports. - /// - [PublicAPI] - public List Ports { get; } - - /// - /// Gets the urls. - /// - [PublicAPI] - public string[] Urls { get; } - - /// - /// Gets the mappings. - /// - [PublicAPI] - public IEnumerable Mappings - { - get - { - lock (((ICollection)_options.Mappings).SyncRoot) - { - return new ReadOnlyCollection(_options.Mappings); - } - } - } - - #region Start/Stop - /// - /// Starts the specified settings. - /// - /// The FluentMockServerSettings. - /// The . - [PublicAPI] - public static FluentMockServer Start(FluentMockServerSettings settings) - { - Check.NotNull(settings, nameof(settings)); - - return new FluentMockServer(settings); - } - - /// - /// Start this FluentMockServer. - /// - /// The port. - /// The SSL support. - /// The . - [PublicAPI] - public static FluentMockServer Start([CanBeNull] int? port = 0, bool ssl = false) - { - return new FluentMockServer(new FluentMockServerSettings - { - Port = port, - UseSSL = ssl - }); - } - - /// - /// Start this FluentMockServer. - /// - /// The urls to listen on. - /// The . - [PublicAPI] - public static FluentMockServer Start(params string[] urls) - { - Check.NotEmpty(urls, nameof(urls)); - - return new FluentMockServer(new FluentMockServerSettings - { - Urls = urls - }); - } - - /// - /// Start this FluentMockServer with the admin interface. - /// - /// The port. - /// The SSL support. - /// The . - [PublicAPI] - public static FluentMockServer StartWithAdminInterface(int? port = 0, bool ssl = false) - { - return new FluentMockServer(new FluentMockServerSettings - { - Port = port, - UseSSL = ssl, - StartAdminInterface = true - }); - } - - /// - /// Start this FluentMockServer with the admin interface. - /// - /// The urls. - /// The . - [PublicAPI] - public static FluentMockServer StartWithAdminInterface(params string[] urls) - { - Check.NotEmpty(urls, nameof(urls)); - - return new FluentMockServer(new FluentMockServerSettings - { - Urls = urls, - StartAdminInterface = true - }); - } - - /// - /// Start this FluentMockServer with the admin interface and read static mappings. - /// - /// The urls. - /// The . - [PublicAPI] - public static FluentMockServer StartWithAdminInterfaceAndReadStaticMappings(params string[] urls) - { - Check.NotEmpty(urls, nameof(urls)); - - return new FluentMockServer(new FluentMockServerSettings - { - Urls = urls, - StartAdminInterface = true, - ReadStaticMappings = true - }); - } - - private FluentMockServer(FluentMockServerSettings settings) - { - if (settings.Urls != null) - { - Urls = settings.Urls; - } - else - { - int port = settings.Port > 0 ? settings.Port.Value : PortUtil.FindFreeTcpPort(); - Urls = new[] { (settings.UseSSL == true ? "https" : "http") + "://localhost:" + port + "/" }; - } - -#if NETSTANDARD - _httpServer = new AspNetCoreSelfHost(_options, Urls); -#else - _httpServer = new OwinSelfHost(_options, Urls); -#endif - Ports = _httpServer.Ports; - - _httpServer.StartAsync(); - - if (settings.AllowPartialMapping == true) - { - AllowPartialMapping(); - } - - if (settings.StartAdminInterface == true) - { - if (!string.IsNullOrEmpty(settings.AdminUsername) && !string.IsNullOrEmpty(settings.AdminPassword)) - { - SetBasicAuthentication(settings.AdminUsername, settings.AdminPassword); - } - - InitAdmin(); - } - - if (settings.ReadStaticMappings == true) - { - ReadStaticMappings(); - } - - if (settings.ProxyAndRecordSettings != null) - { - InitProxyAndRecord(settings.ProxyAndRecordSettings); - } - } - - /// - /// Stop this server. - /// - [PublicAPI] - public void Stop() - { - _httpServer?.StopAsync(); - } - #endregion - - /// - /// Adds the catch all mapping. - /// - [PublicAPI] - public void AddCatchAllMapping() - { - Given(Request.Create().WithPath("/*").UsingAnyVerb()) - .WithGuid(Guid.Parse("90008000-0000-4444-a17e-669cd84f1f05")) - .AtPriority(1000) - .RespondWith(new DynamicResponseProvider(request => new ResponseMessage { StatusCode = 404, Body = "No matching mapping found" })); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - if (_httpServer != null && _httpServer.IsStarted) - { - _httpServer.StopAsync(); - } - } - - /// - /// Resets LogEntries and Mappings. - /// - [PublicAPI] - public void Reset() - { - ResetLogEntries(); - - ResetMappings(); - } - - /// - /// Resets the Mappings. - /// - [PublicAPI] - public void ResetMappings() - { - lock (((ICollection)_options.Mappings).SyncRoot) - { - _options.Mappings = _options.Mappings.Where(m => m.IsAdminInterface).ToList(); - } - } - - /// - /// Deletes the mapping. - /// - /// The unique identifier. - [PublicAPI] - public bool DeleteMapping(Guid guid) - { - lock (((ICollection)_options.Mappings).SyncRoot) - { - // Check a mapping exists with the same GUID, if so, remove it. - var existingMapping = _options.Mappings.FirstOrDefault(m => m.Guid == guid); - if (existingMapping != null) - { - _options.Mappings.Remove(existingMapping); - return true; - } - - return false; - } - } - - /// - /// The add request processing delay. - /// - /// The delay. - [PublicAPI] - public void AddGlobalProcessingDelay(TimeSpan delay) - { - lock (_syncRoot) - { - _options.RequestProcessingDelay = delay; - } - } - - /// - /// Allows the partial mapping. - /// - [PublicAPI] - public void AllowPartialMapping() - { - lock (_syncRoot) - { - _options.AllowPartialMapping = true; - } - } - - /// - /// Sets the basic authentication. - /// - /// The username. - /// The password. - [PublicAPI] - public void SetBasicAuthentication([NotNull] string username, [NotNull] string password) - { - Check.NotNull(username, nameof(username)); - Check.NotNull(password, nameof(password)); - - string authorization = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(username + ":" + password)); - _options.AuthorizationMatcher = new RegexMatcher("^(?i)BASIC " + authorization + "$"); - } - - /// - /// The given. - /// - /// The request matcher. - /// The . - [PublicAPI] - public IRespondWithAProvider Given(IRequestMatcher requestMatcher) - { - return new RespondWithAProvider(RegisterMapping, requestMatcher); - } - - /// - /// The register mapping. - /// - /// - /// The mapping. - /// - private void RegisterMapping(Mapping mapping) - { - lock (((ICollection)_options.Mappings).SyncRoot) - { - // Check a mapping exists with the same GUID, if so, remove it first. - DeleteMapping(mapping.Guid); - - _options.Mappings.Add(mapping); - } - } - } +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using JetBrains.Annotations; +using WireMock.Http; +using WireMock.Matchers; +using WireMock.Matchers.Request; +using WireMock.RequestBuilders; +using WireMock.Settings; +using WireMock.Validation; +using WireMock.Owin; + +namespace WireMock.Server +{ + /// + /// The fluent mock server. + /// + public partial class FluentMockServer : IDisposable + { + private readonly IOwinSelfHost _httpServer; + private readonly object _syncRoot = new object(); + private readonly WireMockMiddlewareOptions _options = new WireMockMiddlewareOptions(); + + /// + /// Gets the ports. + /// + /// + /// The ports. + /// + [PublicAPI] + public List Ports { get; } + + /// + /// Gets the urls. + /// + [PublicAPI] + public string[] Urls { get; } + + /// + /// Gets the mappings. + /// + [PublicAPI] + public IEnumerable Mappings + { + get + { + lock (((ICollection)_options.Mappings).SyncRoot) + { + return new ReadOnlyCollection(_options.Mappings); + } + } + } + + #region Start/Stop + /// + /// Starts the specified settings. + /// + /// The FluentMockServerSettings. + /// The . + [PublicAPI] + public static FluentMockServer Start(FluentMockServerSettings settings) + { + Check.NotNull(settings, nameof(settings)); + + return new FluentMockServer(settings); + } + + /// + /// Start this FluentMockServer. + /// + /// The port. + /// The SSL support. + /// The . + [PublicAPI] + public static FluentMockServer Start([CanBeNull] int? port = 0, bool ssl = false) + { + return new FluentMockServer(new FluentMockServerSettings + { + Port = port, + UseSSL = ssl + }); + } + + /// + /// Start this FluentMockServer. + /// + /// The urls to listen on. + /// The . + [PublicAPI] + public static FluentMockServer Start(params string[] urls) + { + Check.NotEmpty(urls, nameof(urls)); + + return new FluentMockServer(new FluentMockServerSettings + { + Urls = urls + }); + } + + /// + /// Start this FluentMockServer with the admin interface. + /// + /// The port. + /// The SSL support. + /// The . + [PublicAPI] + public static FluentMockServer StartWithAdminInterface(int? port = 0, bool ssl = false) + { + return new FluentMockServer(new FluentMockServerSettings + { + Port = port, + UseSSL = ssl, + StartAdminInterface = true + }); + } + + /// + /// Start this FluentMockServer with the admin interface. + /// + /// The urls. + /// The . + [PublicAPI] + public static FluentMockServer StartWithAdminInterface(params string[] urls) + { + Check.NotEmpty(urls, nameof(urls)); + + return new FluentMockServer(new FluentMockServerSettings + { + Urls = urls, + StartAdminInterface = true + }); + } + + /// + /// Start this FluentMockServer with the admin interface and read static mappings. + /// + /// The urls. + /// The . + [PublicAPI] + public static FluentMockServer StartWithAdminInterfaceAndReadStaticMappings(params string[] urls) + { + Check.NotEmpty(urls, nameof(urls)); + + return new FluentMockServer(new FluentMockServerSettings + { + Urls = urls, + StartAdminInterface = true, + ReadStaticMappings = true + }); + } + + private FluentMockServer(FluentMockServerSettings settings) + { + if (settings.Urls != null) + { + Urls = settings.Urls; + } + else + { + int port = settings.Port > 0 ? settings.Port.Value : PortUtil.FindFreeTcpPort(); + Urls = new[] { (settings.UseSSL == true ? "https" : "http") + "://localhost:" + port + "/" }; + } + +#if NETSTANDARD + _httpServer = new AspNetCoreSelfHost(_options, Urls); +#else + _httpServer = new OwinSelfHost(_options, Urls); +#endif + Ports = _httpServer.Ports; + + _httpServer.StartAsync(); + + if (settings.AllowPartialMapping == true) + { + AllowPartialMapping(); + } + + if (settings.StartAdminInterface == true) + { + if (!string.IsNullOrEmpty(settings.AdminUsername) && !string.IsNullOrEmpty(settings.AdminPassword)) + { + SetBasicAuthentication(settings.AdminUsername, settings.AdminPassword); + } + + InitAdmin(); + } + + if (settings.ReadStaticMappings == true) + { + ReadStaticMappings(); + } + + if (settings.ProxyAndRecordSettings != null) + { + InitProxyAndRecord(settings.ProxyAndRecordSettings); + } + + if (settings.MaxRequestLogCount != null) + { + SetMaxRequestLogCount(settings.MaxRequestLogCount); + } + } + + /// + /// Stop this server. + /// + [PublicAPI] + public void Stop() + { + _httpServer?.StopAsync(); + } + #endregion + + /// + /// Adds the catch all mapping. + /// + [PublicAPI] + public void AddCatchAllMapping() + { + Given(Request.Create().WithPath("/*").UsingAnyVerb()) + .WithGuid(Guid.Parse("90008000-0000-4444-a17e-669cd84f1f05")) + .AtPriority(1000) + .RespondWith(new DynamicResponseProvider(request => new ResponseMessage { StatusCode = 404, Body = "No matching mapping found" })); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + if (_httpServer != null && _httpServer.IsStarted) + { + _httpServer.StopAsync(); + } + } + + /// + /// Resets LogEntries and Mappings. + /// + [PublicAPI] + public void Reset() + { + ResetLogEntries(); + + ResetMappings(); + } + + /// + /// Resets the Mappings. + /// + [PublicAPI] + public void ResetMappings() + { + lock (((ICollection)_options.Mappings).SyncRoot) + { + _options.Mappings = _options.Mappings.Where(m => m.IsAdminInterface).ToList(); + } + } + + /// + /// Deletes the mapping. + /// + /// The unique identifier. + [PublicAPI] + public bool DeleteMapping(Guid guid) + { + lock (((ICollection)_options.Mappings).SyncRoot) + { + // Check a mapping exists with the same GUID, if so, remove it. + var existingMapping = _options.Mappings.FirstOrDefault(m => m.Guid == guid); + if (existingMapping != null) + { + _options.Mappings.Remove(existingMapping); + return true; + } + + return false; + } + } + + /// + /// The add request processing delay. + /// + /// The delay. + [PublicAPI] + public void AddGlobalProcessingDelay(TimeSpan delay) + { + lock (_syncRoot) + { + _options.RequestProcessingDelay = delay; + } + } + + /// + /// Allows the partial mapping. + /// + [PublicAPI] + public void AllowPartialMapping() + { + lock (_syncRoot) + { + _options.AllowPartialMapping = true; + } + } + + /// + /// Sets the basic authentication. + /// + /// The username. + /// The password. + [PublicAPI] + public void SetBasicAuthentication([NotNull] string username, [NotNull] string password) + { + Check.NotNull(username, nameof(username)); + Check.NotNull(password, nameof(password)); + + string authorization = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(username + ":" + password)); + _options.AuthorizationMatcher = new RegexMatcher("^(?i)BASIC " + authorization + "$"); + } + + /// + /// Removes the basic authentication. + /// + [PublicAPI] + public void RemoveBasicAuthentication() + { + _options.AuthorizationMatcher = null; + } + + /// + /// Sets the maximum RequestLog count. + /// + /// The maximum RequestLog count. + [PublicAPI] + public void SetMaxRequestLogCount([CanBeNull] int? maxRequestLogCount) + { + _options.MaxRequestLogCount = maxRequestLogCount; + } + + /// + /// Sets RequestLog expiration in hours. + /// + /// The RequestLog expiration in hours. + [PublicAPI] + public void SetRequestLogExpirationDuration([CanBeNull] int? requestLogExpirationDuration) + { + _options.RequestLogExpirationDuration = requestLogExpirationDuration; + } + + /// + /// The given. + /// + /// The request matcher. + /// The . + [PublicAPI] + public IRespondWithAProvider Given(IRequestMatcher requestMatcher) + { + return new RespondWithAProvider(RegisterMapping, requestMatcher); + } + + /// + /// The register mapping. + /// + /// + /// The mapping. + /// + private void RegisterMapping(Mapping mapping) + { + lock (((ICollection)_options.Mappings).SyncRoot) + { + // Check a mapping exists with the same GUID, if so, remove it first. + DeleteMapping(mapping.Guid); + + _options.Mappings.Add(mapping); + } + } + } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/FluentMockServerSettings.cs b/src/WireMock.Net/Settings/FluentMockServerSettings.cs index 936b6eab..718f57b6 100644 --- a/src/WireMock.Net/Settings/FluentMockServerSettings.cs +++ b/src/WireMock.Net/Settings/FluentMockServerSettings.cs @@ -69,5 +69,15 @@ /// The password needed for __admin access. /// public string AdminPassword { get; set; } + + /// + /// The RequestLog expiration in hours (optional). + /// + public int? RequestLogExpirationDuration { get; set; } + + /// + /// The MaxRequestLog count (optional). + /// + public int? MaxRequestLogCount { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index ba929198..b7b787e6 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -18,7 +18,7 @@ git https://github.com/WireMock-Net/WireMock.Net True - full + portable ../../WireMock.Net-Logo.ico WireMock diff --git a/test/WireMock.Net.Tests/FluentMockServerTests.cs b/test/WireMock.Net.Tests/FluentMockServerTests.cs index 4a383b31..a8875a87 100644 --- a/test/WireMock.Net.Tests/FluentMockServerTests.cs +++ b/test/WireMock.Net.Tests/FluentMockServerTests.cs @@ -33,10 +33,10 @@ namespace WireMock.Net.Tests [Fact] public void FluentMockServer_StartStop() { - var server1 = FluentMockServer.Start("http://localhost:9090/"); + var server1 = FluentMockServer.Start("http://localhost:9091/"); server1.Stop(); - var server2 = FluentMockServer.Start("http://localhost:9090/"); + var server2 = FluentMockServer.Start("http://localhost:9091/"); server2.Stop(); } @@ -370,7 +370,7 @@ namespace WireMock.Net.Tests } //Leaving commented as this requires an actual certificate with password, along with a service that expects a client certificate - [Fact] + //[Fact] //public async Task Should_proxy_responses_with_client_certificate() //{ // // given @@ -386,7 +386,29 @@ namespace WireMock.Net.Tests // Check.That(result).Contains("google"); //} - //[TearDown] + [Fact] + public async Task FluentMockServer_Logging_SetMaxRequestLogCount() + { + // Assign + var client = new HttpClient(); + // Act + _server = FluentMockServer.Start(); + _server.SetMaxRequestLogCount(2); + + await client.GetAsync("http://localhost:" + _server.Ports[0] + "/foo1"); + await client.GetAsync("http://localhost:" + _server.Ports[0] + "/foo2"); + await client.GetAsync("http://localhost:" + _server.Ports[0] + "/foo3"); + + // Assert + Check.That(_server.LogEntries).HasSize(2); + + var requestLoggedA = _server.LogEntries.First(); + Check.That(requestLoggedA.RequestMessage.Path).EndsWith("/foo2"); + + var requestLoggedB = _server.LogEntries.Last(); + Check.That(requestLoggedB.RequestMessage.Path).EndsWith("/foo3"); + } + public void Dispose() { _server?.Stop(); diff --git a/test/WireMock.Net.Tests/__admin/mappings/documentdb_root.json b/test/WireMock.Net.Tests/__admin/mappings/documentdb_root.json index b9e84681..7c1e7464 100644 --- a/test/WireMock.Net.Tests/__admin/mappings/documentdb_root.json +++ b/test/WireMock.Net.Tests/__admin/mappings/documentdb_root.json @@ -18,7 +18,7 @@ }, "Response": { "StatusCode": 200, - "Body": "{\"_self\":\"\",\"id\":\"abc\",\"_rid\":\"abc.documents.azure.com\",\"media\":\"//media/\",\"addresses\":\"//addresses/\",\"_dbs\":\"//dbs/\",\"writableLocations\":[{\"name\":\"West Europe\",\"databaseAccountEndpoint\":\"http://localhost:9090/\"}],\"readableLocations\":[{\"name\":\"West Europe\",\"databaseAccountEndpoint\":\"http://localhost:9090/\"}],\"userReplicationPolicy\":{\"asyncReplication\":false,\"minReplicaSetSize\":3,\"maxReplicasetSize\":4},\"userConsistencyPolicy\":{\"defaultConsistencyLevel\":\"Session\"},\"systemReplicationPolicy\":{\"minReplicaSetSize\":3,\"maxReplicasetSize\":4},\"readPolicy\":{\"primaryReadCoefficient\":1,\"secondaryReadCoefficient\":1},\"queryEngineConfiguration\":\"{\\\"maxSqlQueryInputLength\\\":30720,\\\"maxJoinsPerSqlQuery\\\":5,\\\"maxLogicalAndPerSqlQuery\\\":500,\\\"maxLogicalOrPerSqlQuery\\\":500,\\\"maxUdfRefPerSqlQuery\\\":2,\\\"maxInExpressionItemsCount\\\":8000,\\\"queryMaxInMemorySortDocumentCount\\\":500,\\\"maxQueryRequestTimeoutFraction\\\":0.9,\\\"sqlAllowNonFiniteNumbers\\\":false,\\\"sqlAllowAggregateFunctions\\\":true,\\\"sqlAllowSubQuery\\\":false,\\\"allowNewKeywords\\\":true,\\\"sqlAllowLike\\\":false,\\\"maxSpatialQueryCells\\\":12,\\\"spatialMaxGeometryPointCount\\\":256,\\\"sqlAllowTop\\\":true,\\\"enableSpatialIndexing\\\":true}\"}", + "Body": "{\"_self\":\"\",\"id\":\"abc\",\"_rid\":\"abc.documents.azure.com\",\"media\":\"//media/\",\"addresses\":\"//addresses/\",\"_dbs\":\"//dbs/\",\"writableLocations\":[{\"name\":\"West Europe\",\"databaseAccountEndpoint\":\"http://localhost:9091/\"}],\"readableLocations\":[{\"name\":\"West Europe\",\"databaseAccountEndpoint\":\"http://localhost:9091/\"}],\"userReplicationPolicy\":{\"asyncReplication\":false,\"minReplicaSetSize\":3,\"maxReplicasetSize\":4},\"userConsistencyPolicy\":{\"defaultConsistencyLevel\":\"Session\"},\"systemReplicationPolicy\":{\"minReplicaSetSize\":3,\"maxReplicasetSize\":4},\"readPolicy\":{\"primaryReadCoefficient\":1,\"secondaryReadCoefficient\":1},\"queryEngineConfiguration\":\"{\\\"maxSqlQueryInputLength\\\":30720,\\\"maxJoinsPerSqlQuery\\\":5,\\\"maxLogicalAndPerSqlQuery\\\":500,\\\"maxLogicalOrPerSqlQuery\\\":500,\\\"maxUdfRefPerSqlQuery\\\":2,\\\"maxInExpressionItemsCount\\\":8000,\\\"queryMaxInMemorySortDocumentCount\\\":500,\\\"maxQueryRequestTimeoutFraction\\\":0.9,\\\"sqlAllowNonFiniteNumbers\\\":false,\\\"sqlAllowAggregateFunctions\\\":true,\\\"sqlAllowSubQuery\\\":false,\\\"allowNewKeywords\\\":true,\\\"sqlAllowLike\\\":false,\\\"maxSpatialQueryCells\\\":12,\\\"spatialMaxGeometryPointCount\\\":256,\\\"sqlAllowTop\\\":true,\\\"enableSpatialIndexing\\\":true}\"}", "BodyEncoding": { "CodePage": 65001, "EncodingName": "Unicode (UTF-8)", @@ -33,7 +33,7 @@ "x-ms-gatewayversion": "version=1.11.164.3", "x-ms-media-storage-usage-mb": "0", "x-ms-databaseaccount-provisioned-mb": "0", - "Content-Location": "http://localhost:9090/", + "Content-Location": "http://localhost:9091/", "Date": "Mon, 06 Mar 2017 10:56:40 GMT", "Content-Type": "application/json", "Server": "Microsoft-HTTPAPI/2.0"