// 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.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using JetBrains.Annotations; using Newtonsoft.Json; using WireMock.Admin.Mappings; using WireMock.Exceptions; using WireMock.Handlers; using WireMock.Logging; using WireMock.Matchers; using WireMock.Matchers.Request; using WireMock.Owin; using WireMock.RequestBuilders; using WireMock.ResponseProviders; using WireMock.Serialization; using WireMock.Settings; using WireMock.Validation; namespace WireMock.Server { /// /// The fluent mock server. /// public partial class WireMockServer : IDisposable { private const int ServerStartDelayInMs = 100; private readonly IWireMockServerSettings _settings; private readonly IOwinSelfHost _httpServer; private readonly IWireMockMiddlewareOptions _options = new WireMockMiddlewareOptions(); private readonly MappingConverter _mappingConverter; private readonly MatcherMapper _matcherMapper; /// /// 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 mappings as MappingModels. /// [PublicAPI] public IEnumerable MappingModels => ToMappingModels(); /// /// 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 WireMockServerSettings. /// The . [PublicAPI] public static WireMockServer Start([NotNull] IWireMockServerSettings settings) { Check.NotNull(settings, nameof(settings)); return new WireMockServer(settings); } /// /// Start this WireMockServer. /// /// The port. /// The SSL support. /// The . [PublicAPI] public static WireMockServer Start([CanBeNull] int? port = 0, bool ssl = false) { return new WireMockServer(new WireMockServerSettings { Port = port, UseSSL = ssl }); } /// /// Start this WireMockServer. /// /// The urls to listen on. /// The . [PublicAPI] public static WireMockServer Start(params string[] urls) { Check.NotNullOrEmpty(urls, nameof(urls)); return new WireMockServer(new WireMockServerSettings { Urls = urls }); } /// /// Start this WireMockServer with the admin interface. /// /// The port. /// The SSL support. /// The . [PublicAPI] public static WireMockServer StartWithAdminInterface(int? port = 0, bool ssl = false) { return new WireMockServer(new WireMockServerSettings { Port = port, UseSSL = ssl, StartAdminInterface = true }); } /// /// Start this WireMockServer with the admin interface. /// /// The urls. /// The . [PublicAPI] public static WireMockServer StartWithAdminInterface(params string[] urls) { Check.NotNullOrEmpty(urls, nameof(urls)); return new WireMockServer(new WireMockServerSettings { Urls = urls, StartAdminInterface = true }); } /// /// Start this WireMockServer with the admin interface and read static mappings. /// /// The urls. /// The . [PublicAPI] public static WireMockServer StartWithAdminInterfaceAndReadStaticMappings(params string[] urls) { Check.NotNullOrEmpty(urls, nameof(urls)); return new WireMockServer(new WireMockServerSettings { Urls = urls, StartAdminInterface = true, ReadStaticMappings = true }); } protected WireMockServer(IWireMockServerSettings settings) { _settings = settings; // Set default values if not provided _settings.Logger = settings.Logger ?? new WireMockNullLogger(); _settings.FileSystemHandler = settings.FileSystemHandler ?? new LocalFileSystemHandler(); _settings.Logger.Info("WireMock.Net by Stef Heyenrath (https://github.com/WireMock-Net/WireMock.Net)"); _settings.Logger.Debug("WireMock.Net server settings {0}", JsonConvert.SerializeObject(settings, Formatting.Indented)); HostUrlOptions urlOptions; if (settings.Urls != null) { urlOptions = new HostUrlOptions { Urls = settings.Urls }; } else { urlOptions = new HostUrlOptions { UseSSL = settings.UseSSL == true, Port = settings.Port }; } _options.FileSystemHandler = _settings.FileSystemHandler; _options.PreWireMockMiddlewareInit = _settings.PreWireMockMiddlewareInit; _options.PostWireMockMiddlewareInit = _settings.PostWireMockMiddlewareInit; _options.Logger = _settings.Logger; _options.DisableJsonBodyParsing = _settings.DisableJsonBodyParsing; _matcherMapper = new MatcherMapper(_settings); _mappingConverter = new MappingConverter(_matcherMapper); #if USE_ASPNETCORE _httpServer = new AspNetCoreSelfHost(_options, urlOptions); #else _httpServer = new OwinSelfHost(_options, urlOptions); #endif var startTask = _httpServer.StartAsync(); using (var ctsStartTimeout = new CancellationTokenSource(settings.StartTimeout)) { while (!_httpServer.IsStarted) { // Throw exception if service start fails if (_httpServer.RunningException != null) { throw new WireMockException($"Service start failed with error: {_httpServer.RunningException.Message}", _httpServer.RunningException); } if (ctsStartTimeout.IsCancellationRequested) { // In case of an aggregate exception, throw the exception. if (startTask.Exception != null) { throw new WireMockException($"Service start failed with error: {startTask.Exception.Message}", startTask.Exception); } // Else throw TimeoutException throw new TimeoutException($"Service start timed out after {TimeSpan.FromMilliseconds(settings.StartTimeout)}"); } ctsStartTimeout.Token.WaitHandle.WaitOne(ServerStartDelayInMs); } Urls = _httpServer.Urls.ToArray(); Ports = _httpServer.Ports; } if (settings.AllowBodyForAllHttpMethods == true) { _options.AllowBodyForAllHttpMethods = _settings.AllowBodyForAllHttpMethods; _settings.Logger.Info("AllowBodyForAllHttpMethods is set to True"); } if (settings.AllowOnlyDefinedHttpStatusCodeInResponse == true) { _options.AllowOnlyDefinedHttpStatusCodeInResponse = _settings.AllowOnlyDefinedHttpStatusCodeInResponse; _settings.Logger.Info("AllowOnlyDefinedHttpStatusCodeInResponse is set to True"); } 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); } if (settings.RequestLogExpirationDuration != null) { SetRequestLogExpirationDuration(settings.RequestLogExpirationDuration); } 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) { _settings.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. /// Optional boolean to indicate if this mapping should be saved as static mapping file. /// The . [PublicAPI] public IRespondWithAProvider Given(IRequestMatcher requestMatcher, bool saveToFile = false) { return new RespondWithAProvider(RegisterMapping, requestMatcher, _settings, saveToFile); } private void RegisterMapping(IMapping mapping, bool saveToFile) { // 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); } if (saveToFile) { SaveMappingToFile(mapping); } } } }