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); } } } }