// 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.Generic; using System.Linq; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Stef.Validation; using WireMock.Proxy; using WireMock.ResponseProviders; using WireMock.Settings; using WireMock.Transformers; using WireMock.Transformers.Handlebars; using WireMock.Transformers.Scriban; using WireMock.Types; using WireMock.Util; namespace WireMock.ResponseBuilders { /// /// The Response. /// public partial class Response : IResponseBuilder { private static readonly ThreadLocal Random = new ThreadLocal(() => new Random(DateTime.UtcNow.Millisecond)); private TimeSpan? _delay; /// /// The minimum random delay in milliseconds. /// public int? MinimumDelayMilliseconds { get; private set; } /// /// The maximum random delay in milliseconds. /// public int? MaximumDelayMilliseconds { get; private set; } /// /// The delay /// public TimeSpan? Delay { get { if (MinimumDelayMilliseconds != null && MaximumDelayMilliseconds != null) { return TimeSpan.FromMilliseconds(Random.Value.Next(MinimumDelayMilliseconds.Value, MaximumDelayMilliseconds.Value)); } return _delay; } private set => _delay = value; } /// /// Gets a value indicating whether [use transformer]. /// public bool UseTransformer { get; private set; } /// /// Gets the type of the transformer. /// public TransformerType TransformerType { get; private set; } /// /// Gets a value indicating whether to use the Handlebars transformer for the content from the referenced BodyAsFile. /// public bool UseTransformerForBodyAsFile { get; private set; } /// /// Gets the ReplaceNodeOptions to use when transforming a JSON node. /// public ReplaceNodeOptions TransformerReplaceNodeOptions { get; private set; } /// /// Gets the response message. /// public ResponseMessage ResponseMessage { get; } /// /// Creates this instance. /// /// ResponseMessage /// A . [PublicAPI] public static IResponseBuilder Create([CanBeNull] ResponseMessage responseMessage = null) { var message = responseMessage ?? new ResponseMessage(); return new Response(message); } /// /// Creates this instance with the specified function. /// /// The callback function. /// A . [PublicAPI] public static IResponseBuilder Create([NotNull] Func func) { Guard.NotNull(func, nameof(func)); return new Response(func()); } /// /// Initializes a new instance of the class. /// /// /// The response. /// private Response(ResponseMessage responseMessage) { ResponseMessage = responseMessage; } /// [PublicAPI] public IResponseBuilder WithStatusCode(int code) { ResponseMessage.StatusCode = code; return this; } /// [PublicAPI] public IResponseBuilder WithStatusCode(string code) { ResponseMessage.StatusCode = code; return this; } /// [PublicAPI] public IResponseBuilder WithStatusCode(HttpStatusCode code) { return WithStatusCode((int)code); } /// /// The with Success status code (200). /// /// A . [PublicAPI] public IResponseBuilder WithSuccess() { return WithStatusCode((int)HttpStatusCode.OK); } /// /// The with NotFound status code (404). /// /// The . [PublicAPI] public IResponseBuilder WithNotFound() { return WithStatusCode((int)HttpStatusCode.NotFound); } /// public IResponseBuilder WithHeader(string name, params string[] values) { Guard.NotNull(name, nameof(name)); ResponseMessage.AddHeader(name, values); return this; } /// public IResponseBuilder WithHeaders(IDictionary headers) { Guard.NotNull(headers, nameof(headers)); ResponseMessage.Headers = headers.ToDictionary(header => header.Key, header => new WireMockList(header.Value)); return this; } /// public IResponseBuilder WithHeaders(IDictionary headers) { Guard.NotNull(headers, nameof(headers)); ResponseMessage.Headers = headers.ToDictionary(header => header.Key, header => new WireMockList(header.Value)); return this; } /// public IResponseBuilder WithHeaders(IDictionary> headers) { ResponseMessage.Headers = headers; return this; } /// public IResponseBuilder WithBody(Func bodyFactory, string destination = BodyDestinationFormat.SameAsSource, Encoding encoding = null) { Guard.NotNull(bodyFactory, nameof(bodyFactory)); return WithCallbackInternal(true, req => new ResponseMessage { BodyData = new BodyData { DetectedBodyType = BodyType.String, BodyAsString = bodyFactory(req), Encoding = encoding ?? Encoding.UTF8 } }); } /// public IResponseBuilder WithBody(Func> bodyFactory, string destination = BodyDestinationFormat.SameAsSource, Encoding encoding = null) { Guard.NotNull(bodyFactory, nameof(bodyFactory)); return WithCallbackInternal(true, async req => new ResponseMessage { BodyData = new BodyData { DetectedBodyType = BodyType.String, BodyAsString = await bodyFactory(req).ConfigureAwait(false), Encoding = encoding ?? Encoding.UTF8 } }); } /// public IResponseBuilder WithBody(byte[] body, string destination = BodyDestinationFormat.SameAsSource, Encoding encoding = null) { Guard.NotNull(body, nameof(body)); ResponseMessage.BodyDestination = destination; ResponseMessage.BodyData = new BodyData(); switch (destination) { case BodyDestinationFormat.String: var enc = encoding ?? Encoding.UTF8; ResponseMessage.BodyData.DetectedBodyType = BodyType.String; ResponseMessage.BodyData.BodyAsString = enc.GetString(body); ResponseMessage.BodyData.Encoding = enc; break; default: ResponseMessage.BodyData.DetectedBodyType = BodyType.Bytes; ResponseMessage.BodyData.BodyAsBytes = body; break; } return this; } /// public IResponseBuilder WithBodyFromFile(string filename, bool cache = true) { Guard.NotNull(filename, nameof(filename)); ResponseMessage.BodyData = new BodyData { BodyAsFileIsCached = cache, BodyAsFile = filename }; if (cache && !UseTransformer) { ResponseMessage.BodyData.DetectedBodyType = BodyType.Bytes; } else { ResponseMessage.BodyData.DetectedBodyType = BodyType.File; } return this; } /// public IResponseBuilder WithBody(string body, string destination = BodyDestinationFormat.SameAsSource, Encoding encoding = null) { Guard.NotNull(body, nameof(body)); encoding = encoding ?? Encoding.UTF8; ResponseMessage.BodyDestination = destination; ResponseMessage.BodyData = new BodyData { Encoding = encoding }; switch (destination) { case BodyDestinationFormat.Bytes: ResponseMessage.BodyData.DetectedBodyType = BodyType.Bytes; ResponseMessage.BodyData.BodyAsBytes = encoding.GetBytes(body); break; case BodyDestinationFormat.Json: ResponseMessage.BodyData.DetectedBodyType = BodyType.Json; ResponseMessage.BodyData.BodyAsJson = JsonUtils.DeserializeObject(body); break; default: ResponseMessage.BodyData.DetectedBodyType = BodyType.String; ResponseMessage.BodyData.BodyAsString = body; break; } return this; } /// public IResponseBuilder WithBodyAsJson(object body, Encoding encoding = null, bool? indented = null) { Guard.NotNull(body, nameof(body)); ResponseMessage.BodyDestination = null; ResponseMessage.BodyData = new BodyData { Encoding = encoding, DetectedBodyType = BodyType.Json, BodyAsJson = body, BodyAsJsonIndented = indented }; return this; } /// public IResponseBuilder WithBodyAsJson(object body, bool indented) { return WithBodyAsJson(body, null, indented); } /// public IResponseBuilder WithTransformer(bool transformContentFromBodyAsFile) { return WithTransformer(TransformerType.Handlebars, transformContentFromBodyAsFile); } /// public IResponseBuilder WithTransformer(ReplaceNodeOptions options) { return WithTransformer(TransformerType.Handlebars, false, options); } #pragma warning disable CS1574 /// #pragma warning restore CS1574 public IResponseBuilder WithTransformer(TransformerType transformerType, bool transformContentFromBodyAsFile = false, ReplaceNodeOptions options = ReplaceNodeOptions.None) { UseTransformer = true; TransformerType = transformerType; UseTransformerForBodyAsFile = transformContentFromBodyAsFile; TransformerReplaceNodeOptions = options; return this; } /// public IResponseBuilder WithDelay(TimeSpan delay) { Guard.Condition(delay, d => d > TimeSpan.Zero, nameof(delay)); Delay = delay; return this; } /// public IResponseBuilder WithDelay(int milliseconds) { return WithDelay(TimeSpan.FromMilliseconds(milliseconds)); } /// public IResponseBuilder WithRandomDelay(int minimumMilliseconds = 0, int maximumMilliseconds = 60_000) { Guard.Condition(minimumMilliseconds, min => min >= 0, nameof(minimumMilliseconds)); Guard.Condition(maximumMilliseconds, max => max > minimumMilliseconds, nameof(maximumMilliseconds)); MinimumDelayMilliseconds = minimumMilliseconds; MaximumDelayMilliseconds = maximumMilliseconds; return this; } /// public async Task<(ResponseMessage Message, IMapping Mapping)> ProvideResponseAsync(RequestMessage requestMessage, IWireMockServerSettings settings) { Guard.NotNull(requestMessage, nameof(requestMessage)); Guard.NotNull(settings, nameof(settings)); if (Delay != null) { await Task.Delay(Delay.Value).ConfigureAwait(false); } if (ProxyAndRecordSettings != null && _httpClientForProxy != null) { string RemoveFirstOccurrence(string source, string find) { int place = source.IndexOf(find, StringComparison.OrdinalIgnoreCase); return place >= 0 ? source.Remove(place, find.Length) : source; } var requestUri = new Uri(requestMessage.Url); // Build the proxy url and skip duplicates string extra = RemoveFirstOccurrence(requestUri.LocalPath.TrimEnd('/'), new Uri(ProxyAndRecordSettings.Url).LocalPath.TrimEnd('/')); requestMessage.ProxyUrl = ProxyAndRecordSettings.Url + extra + requestUri.Query; var proxyHelper = new ProxyHelper(settings); return await proxyHelper.SendAsync( ProxyAndRecordSettings, _httpClientForProxy, requestMessage, requestMessage.ProxyUrl ).ConfigureAwait(false); } ResponseMessage responseMessage; if (!WithCallbackUsed) { responseMessage = ResponseMessage; } else { if (Callback != null) { responseMessage = Callback(requestMessage); } else { responseMessage = await CallbackAsync(requestMessage).ConfigureAwait(false); } // Copy StatusCode from ResponseMessage (if defined) if (ResponseMessage.StatusCode != null) { responseMessage.StatusCode = ResponseMessage.StatusCode; } // Copy Headers from ResponseMessage (if defined) if (ResponseMessage.Headers?.Count > 0) { responseMessage.Headers = ResponseMessage.Headers; } } if (UseTransformer) { ITransformer responseMessageTransformer; switch (TransformerType) { case TransformerType.Handlebars: var factoryHandlebars = new HandlebarsContextFactory(settings.FileSystemHandler, settings.HandlebarsRegistrationCallback); responseMessageTransformer = new Transformer(factoryHandlebars); break; case TransformerType.Scriban: case TransformerType.ScribanDotLiquid: var factoryDotLiquid = new ScribanContextFactory(settings.FileSystemHandler, TransformerType); responseMessageTransformer = new Transformer(factoryDotLiquid); break; default: throw new NotImplementedException($"TransformerType '{TransformerType}' is not supported."); } return (responseMessageTransformer.Transform(requestMessage, responseMessage, UseTransformerForBodyAsFile, TransformerReplaceNodeOptions), null); } if (!UseTransformer && ResponseMessage.BodyData?.BodyAsFileIsCached == true) { ResponseMessage.BodyData.BodyAsBytes = settings.FileSystemHandler.ReadResponseBodyAsFile(responseMessage.BodyData.BodyAsFile); } return (responseMessage, null); } } }