// 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.Net.Http; using System.Threading; using JetBrains.Annotations; using Newtonsoft.Json; using Stef.Validation; using WireMock.Admin.Mappings; using WireMock.Authentication; using WireMock.Exceptions; using WireMock.Handlers; using WireMock.Http; using WireMock.Logging; using WireMock.Matchers.Request; using WireMock.Owin; using WireMock.RequestBuilders; using WireMock.ResponseProviders; using WireMock.Serialization; using WireMock.Settings; using WireMock.Types; using WireMock.Util; namespace WireMock.Server; /// /// The fluent mock server. /// public partial class WireMockServer : IWireMockServer { private const int ServerStartDelayInMs = 100; private readonly WireMockServerSettings _settings; private readonly IOwinSelfHost? _httpServer; private readonly IWireMockMiddlewareOptions _options = new WireMockMiddlewareOptions(); private readonly MappingConverter _mappingConverter; private readonly MatcherMapper _matcherMapper; private readonly MappingToFileSaver _mappingToFileSaver; private readonly MappingBuilder _mappingBuilder; private readonly IGuidUtils _guidUtils = new GuidUtils(); private readonly IDateTimeUtils _dateTimeUtils = new DateTimeUtils(); /// [PublicAPI] public bool IsStarted => _httpServer is { IsStarted: true }; /// [PublicAPI] public bool IsStartedWithAdminInterface => IsStarted && _settings.StartAdminInterface.GetValueOrDefault(); /// [PublicAPI] public List Ports { get; } /// [PublicAPI] public int Port => Ports?.FirstOrDefault() ?? default; /// [PublicAPI] public string[] Urls { get; } /// [PublicAPI] public string? Url => Urls?.FirstOrDefault(); /// [PublicAPI] public string? Consumer { get; private set; } /// [PublicAPI] public string? Provider { get; private set; } /// /// Gets the mappings. /// [PublicAPI] public IEnumerable Mappings => _options.Mappings.Values.ToArray(); /// [PublicAPI] public IEnumerable MappingModels => ToMappingModels(); /// /// Gets the scenarios. /// [PublicAPI] public ConcurrentDictionary Scenarios => new(_options.Scenarios); #region IDisposable Members /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { _options.LogEntries.CollectionChanged -= LogEntries_CollectionChanged; 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) { DisposeEnhancedFileSystemWatcher(); _httpServer?.StopAsync(); } #endregion #region HttpClient /// /// Create a which can be used to call this instance. /// /// An ordered list of System.Net.Http.DelegatingHandler instances to be invoked /// as an System.Net.Http.HttpRequestMessage travels from the System.Net.Http.HttpClient /// to the network and an System.Net.Http.HttpResponseMessage travels from the network /// back to System.Net.Http.HttpClient. The handlers are invoked in a top-down fashion. /// That is, the first entry is invoked first for an outbound request message but /// last for an inbound response message. /// /// [PublicAPI] public HttpClient CreateClient(params DelegatingHandler[] handlers) { if (!IsStarted) { throw new InvalidOperationException("Unable to create HttpClient because the service is not started."); } var client = HttpClientFactory2.Create(handlers); client.BaseAddress = new Uri(Url!); return client; } /// /// Create s (one for each URL) which can be used to call this instance. /// The inner handler represents the destination of the HTTP message channel. /// /// An ordered list of System.Net.Http.DelegatingHandler instances to be invoked /// as an System.Net.Http.HttpRequestMessage travels from the System.Net.Http.HttpClient /// to the network and an System.Net.Http.HttpResponseMessage travels from the network /// back to System.Net.Http.HttpClient. The handlers are invoked in a top-down fashion. /// That is, the first entry is invoked first for an outbound request message but /// last for an inbound response message. /// /// [PublicAPI] public HttpClient[] CreateClients(HttpMessageHandler innerHandler, params DelegatingHandler[] handlers) { if (!IsStarted) { throw new InvalidOperationException("Unable to create HttpClients because the service is not started."); } return Urls.Select(url => { var client = HttpClientFactory2.Create(innerHandler, handlers); client.BaseAddress = new Uri(url); return client; }).ToArray(); } #endregion #region Start/Stop /// /// Starts this WireMockServer with the specified settings. /// /// The WireMockServerSettings. /// The . [PublicAPI] public static WireMockServer Start(WireMockServerSettings settings) { Guard.NotNull(settings); return new WireMockServer(settings); } /// /// Start this WireMockServer. /// /// The port. /// The SSL support. /// The . [PublicAPI] public static WireMockServer Start(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) { Guard.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) { Guard.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) { Guard.NotNullOrEmpty(urls); return new WireMockServer(new WireMockServerSettings { Urls = urls, StartAdminInterface = true, ReadStaticMappings = true }); } /// /// Initializes a new instance of the class. /// /// The settings. /// /// Service start failed with error: {_httpServer.RunningException.Message} /// or /// Service start failed with error: {startTask.Exception.Message} /// /// Service start timed out after {TimeSpan.FromMilliseconds(settings.StartTimeout)} protected WireMockServer(WireMockServerSettings settings) { _settings = Guard.NotNull(settings); // Set default values if not provided _settings.Logger = settings.Logger ?? new WireMockNullLogger(); _settings.FileSystemHandler = settings.FileSystemHandler ?? new LocalFileSystemHandler(); _settings.Logger.Info("By Stef Heyenrath (https://github.com/WireMock-Net/WireMock.Net)"); _settings.Logger.Debug("Server settings {0}", JsonConvert.SerializeObject(settings, Formatting.Indented)); HostUrlOptions urlOptions; if (settings.Urls != null) { urlOptions = new HostUrlOptions { Urls = settings.Urls }; } else { if (settings.HostingScheme is not null) { urlOptions = new HostUrlOptions { HostingScheme = settings.HostingScheme.Value, Port = settings.Port }; } else { urlOptions = new HostUrlOptions { HostingScheme = settings.UseSSL == true ? HostingScheme.Https : HostingScheme.Http, Port = settings.Port }; } } WireMockMiddlewareOptionsHelper.InitFromSettings(settings, _options); _options.LogEntries.CollectionChanged += LogEntries_CollectionChanged; _matcherMapper = new MatcherMapper(_settings); _mappingConverter = new MappingConverter(_matcherMapper); _mappingToFileSaver = new MappingToFileSaver(_settings, _mappingConverter); _mappingBuilder = new MappingBuilder( settings, _options, _mappingConverter, _mappingToFileSaver, _guidUtils, _dateTimeUtils ); #if USE_ASPNETCORE _options.AdditionalServiceRegistration = _settings.AdditionalServiceRegistration; _options.CorsPolicyOptions = _settings.CorsPolicyOptions; _options.ClientCertificateMode = _settings.ClientCertificateMode; _options.AcceptAnyClientCertificate = _settings.AcceptAnyClientCertificate; _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; } InitSettings(settings); } /// [PublicAPI] public void Stop() { var result = _httpServer?.StopAsync(); result?.Wait(); // wait for stop to actually happen } #endregion /// [PublicAPI] public void AddCatchAllMapping() { Given(Request.Create().WithPath("/*").UsingAnyMethod()) .WithGuid(Guid.Parse("90008000-0000-4444-a17e-669cd84f1f05")) .AtPriority(1000) .RespondWith(new DynamicResponseProvider(_ => ResponseMessageBuilder.Create("No matching mapping found", 404))); } /// [PublicAPI] public void Reset() { ResetLogEntries(); ResetMappings(); } /// [PublicAPI] public void ResetMappings() { foreach (var nonAdmin in _options.Mappings.ToArray().Where(m => !m.Value.IsAdminInterface)) { _options.Mappings.TryRemove(nonAdmin.Key, out _); } } /// [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); } /// [PublicAPI] public void AddGlobalProcessingDelay(TimeSpan delay) { _options.RequestProcessingDelay = delay; } /// [PublicAPI] public void AllowPartialMapping(bool allow = true) { _settings.Logger.Info("AllowPartialMapping is set to {0}", allow); _options.AllowPartialMapping = allow; } /// [PublicAPI] public void SetAzureADAuthentication(string tenant, string audience) { Guard.NotNull(tenant); Guard.NotNull(audience); #if NETSTANDARD1_3 throw new NotSupportedException("AzureADAuthentication is not supported for NETStandard 1.3"); #else _options.AuthenticationMatcher = new AzureADAuthenticationMatcher(tenant, audience); #endif } /// [PublicAPI] public void SetBasicAuthentication(string username, string password) { Guard.NotNull(username, nameof(username)); Guard.NotNull(password, nameof(password)); _options.AuthenticationMatcher = new BasicAuthenticationMatcher(username, password); } /// [PublicAPI] public void RemoveAuthentication() { _options.AuthenticationMatcher = null; } /// [PublicAPI] public void SetMaxRequestLogCount(int? maxRequestLogCount) { _options.MaxRequestLogCount = maxRequestLogCount; } /// [PublicAPI] public void SetRequestLogExpirationDuration(int? requestLogExpirationDuration) { _options.RequestLogExpirationDuration = requestLogExpirationDuration; } /// [PublicAPI] public void ResetScenarios() { _options.Scenarios.Clear(); } /// [PublicAPI] public bool ResetScenario(string name) { return _options.Scenarios.ContainsKey(name) && _options.Scenarios.TryRemove(name, out _); } /// [PublicAPI] public IWireMockServer WithMapping(params MappingModel[] mappings) { foreach (var mapping in mappings) { ConvertMappingAndRegisterAsRespondProvider(mapping, mapping.Guid ?? Guid.NewGuid()); } return this; } /// [PublicAPI] public IWireMockServer WithMapping(string mappings) { var mappingModels = DeserializeJsonToArray(mappings); foreach (var mappingModel in mappingModels) { ConvertMappingAndRegisterAsRespondProvider(mappingModel, mappingModel.Guid ?? Guid.NewGuid()); } return this; } /// /// 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 _mappingBuilder.Given(requestMatcher, saveToFile); } /// [PublicAPI] public string? MappingToCSharpCode(Guid guid, MappingConverterType converterType) { return _mappingBuilder.ToCSharpCode(guid, converterType); } /// [PublicAPI] public string MappingsToCSharpCode(MappingConverterType converterType) { return _mappingBuilder.ToCSharpCode(converterType); } private void InitSettings(WireMockServerSettings settings) { 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!); } if (!string.IsNullOrEmpty(settings.AdminAzureADTenant) && !string.IsNullOrEmpty(settings.AdminAzureADAudience)) { SetAzureADAuthentication(settings.AdminAzureADTenant!, settings.AdminAzureADAudience!); } InitAdmin(); } if (settings.ReadStaticMappings == true) { ReadStaticMappings(); } if (settings.WatchStaticMappings == true) { WatchStaticMappings(); } InitProxyAndRecord(settings); if (settings.RequestLogExpirationDuration != null) { SetRequestLogExpirationDuration(settings.RequestLogExpirationDuration); } if (settings.MaxRequestLogCount != null) { SetMaxRequestLogCount(settings.MaxRequestLogCount); } } }