using JetBrains.Annotations;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using WireMock.Handlers;
using WireMock.Http;
using WireMock.Logging;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Owin;
using WireMock.RequestBuilders;
using WireMock.ResponseProviders;
using WireMock.Settings;
using WireMock.Validation;
namespace WireMock.Server
{
///
/// The fluent mock server.
///
public partial class FluentMockServer : IDisposable
{
private readonly IWireMockLogger _logger;
private readonly IFileSystemHandler _fileSystemHandler;
private const int ServerStartDelay = 100;
private readonly IOwinSelfHost _httpServer;
private readonly WireMockMiddlewareOptions _options = new WireMockMiddlewareOptions();
///
/// Gets a value indicating whether this server is started.
///
[PublicAPI]
public bool IsStarted => _httpServer != null && _httpServer.IsStarted;
///
/// Gets the ports.
///
[PublicAPI]
public List Ports { get; }
///
/// Gets the urls.
///
[PublicAPI]
public string[] Urls { get; }
///
/// Gets the mappings.
///
[PublicAPI]
public IEnumerable Mappings => _options.Mappings.Values.ToArray();
///
/// Gets the scenarios.
///
[PublicAPI]
public ConcurrentDictionary Scenarios => new ConcurrentDictionary(_options.Scenarios);
#region IDisposable Members
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Releases unmanaged and - optionally - managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (_httpServer != null)
{
_httpServer.StopAsync();
}
}
#endregion
#region Start/Stop
///
/// Starts the specified settings.
///
/// The FluentMockServerSettings.
/// The .
[PublicAPI]
public static FluentMockServer Start(IFluentMockServerSettings 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.NotNullOrEmpty(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.NotNullOrEmpty(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.NotNullOrEmpty(urls, nameof(urls));
return new FluentMockServer(new FluentMockServerSettings
{
Urls = urls,
StartAdminInterface = true,
ReadStaticMappings = true
});
}
private FluentMockServer(IFluentMockServerSettings settings)
{
settings.Logger = settings.Logger ?? new WireMockConsoleLogger();
_logger = settings.Logger;
_fileSystemHandler = settings.FileSystemHandler ?? new LocalFileSystemHandler();
_logger.Info("WireMock.Net by Stef Heyenrath (https://github.com/WireMock-Net/WireMock.Net)");
_logger.Debug("WireMock.Net server settings {0}", JsonConvert.SerializeObject(settings, Formatting.Indented));
if (settings.Urls != null)
{
Urls = settings.Urls.ToArray();
}
else
{
int port = settings.Port > 0 ? settings.Port.Value : PortUtil.FindFreeTcpPort();
Urls = new[] { $"{(settings.UseSSL == true ? "https" : "http")}://localhost:{port}" };
}
_options.PreWireMockMiddlewareInit = settings.PreWireMockMiddlewareInit;
_options.PostWireMockMiddlewareInit = settings.PostWireMockMiddlewareInit;
_options.Logger = _logger;
#if USE_ASPNETCORE
_httpServer = new AspNetCoreSelfHost(_options, Urls);
#else
_httpServer = new OwinSelfHost(_options, Urls);
#endif
Ports = _httpServer.Ports;
_httpServer.StartAsync();
using (var ctsStartTimeout = new CancellationTokenSource(settings.StartTimeout))
{
while (!_httpServer.IsStarted)
{
// Throw out exception if service start fails
if (_httpServer.RunningException != null)
{
throw new Exception($"Service start failed with error: {_httpServer.RunningException.Message}", _httpServer.RunningException);
}
// Respect start timeout setting by throwing TimeoutException
if (ctsStartTimeout.IsCancellationRequested)
{
throw new TimeoutException($"Service start timed out after {TimeSpan.FromMilliseconds(settings.StartTimeout)}");
}
ctsStartTimeout.Token.WaitHandle.WaitOne(ServerStartDelay);
}
}
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.WatchStaticMappings == true)
{
WatchStaticMappings();
}
if (settings.ProxyAndRecordSettings != null)
{
InitProxyAndRecord(settings.ProxyAndRecordSettings);
}
if (settings.MaxRequestLogCount != null)
{
SetMaxRequestLogCount(settings.MaxRequestLogCount);
}
}
///
/// Stop this server.
///
[PublicAPI]
public void Stop()
{
var result = _httpServer?.StopAsync();
result?.Wait(); // wait for stop to actually happen
}
#endregion
///
/// Adds the catch all mapping.
///
[PublicAPI]
public void AddCatchAllMapping()
{
Given(Request.Create().WithPath("/*").UsingAnyMethod())
.WithGuid(Guid.Parse("90008000-0000-4444-a17e-669cd84f1f05"))
.AtPriority(1000)
.RespondWith(new DynamicResponseProvider(request => ResponseMessageBuilder.Create("No matching mapping found", 404)));
}
///
/// Resets LogEntries and Mappings.
///
[PublicAPI]
public void Reset()
{
ResetLogEntries();
ResetMappings();
}
///
/// Resets the Mappings.
///
[PublicAPI]
public void ResetMappings()
{
foreach (var nonAdmin in _options.Mappings.ToArray().Where(m => !m.Value.IsAdminInterface))
{
_options.Mappings.TryRemove(nonAdmin.Key, out _);
}
}
///
/// Deletes the mapping.
///
/// The unique identifier.
[PublicAPI]
public bool DeleteMapping(Guid guid)
{
// Check a mapping exists with the same GUID, if so, remove it.
if (_options.Mappings.ContainsKey(guid))
{
return _options.Mappings.TryRemove(guid, out _);
}
return false;
}
private bool DeleteMapping(string path)
{
// Check a mapping exists with the same path, if so, remove it.
var mapping = _options.Mappings.ToArray().FirstOrDefault(entry => string.Equals(entry.Value.Path, path, StringComparison.OrdinalIgnoreCase));
return DeleteMapping(mapping.Key);
}
///
/// The add request processing delay.
///
/// The delay.
[PublicAPI]
public void AddGlobalProcessingDelay(TimeSpan delay)
{
_options.RequestProcessingDelay = delay;
}
///
/// Allows the partial mapping.
///
[PublicAPI]
public void AllowPartialMapping(bool allow = true)
{
_logger.Info("AllowPartialMapping is set to {0}", allow);
_options.AllowPartialMapping = allow;
}
///
/// 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(MatchBehaviour.AcceptOnMatch, "^(?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;
}
///
/// Resets the Scenarios.
///
[PublicAPI]
public void ResetScenarios()
{
_options.Scenarios.Clear();
}
///
/// The given.
///
/// The request matcher.
/// The .
[PublicAPI]
public IRespondWithAProvider Given(IRequestMatcher requestMatcher)
{
return new RespondWithAProvider(RegisterMapping, requestMatcher);
}
private void RegisterMapping(Mapping mapping)
{
// Check a mapping exists with the same Guid, if so, replace it.
if (_options.Mappings.ContainsKey(mapping.Guid))
{
_options.Mappings[mapping.Guid] = mapping;
}
else
{
_options.Mappings.TryAdd(mapping.Guid, mapping);
}
}
}
}