Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to use WireMock as a middleware? #1035

Open
adrianiftode opened this issue Dec 8, 2023 · 15 comments
Open

Is it possible to use WireMock as a middleware? #1035

adrianiftode opened this issue Dec 8, 2023 · 15 comments
Labels

Comments

@adrianiftode
Copy link

Currently I see that I can use wiremock hosted in different forms, however what I would like to do is to have a path prefix and everything after that path to be handled by wiremock.

So something like

app.Use(async (context, next) =>
{
    if (context.Request.Path.StartsWith("wiremock"))
    {
        await UseWiremock()
        return;
    }

    await next(context);
});

I would like to deploy an arbitrary .NET Service, but still use wiremock at the port 80 and let it handle some prefixed requests.

@StefH
Copy link
Collaborator

StefH commented Dec 9, 2023

@adrianiftode
Sounds like an interesting idea.

I guess you are using a modern .NET version like 6 or higher?

There is already an internal class named WireMockMiddleware which I register in the AspNetCoreSelfHost.cs:

appBuilder.UseMiddleware<WireMockMiddleware>();

Maybe that can be used?

@adrianiftode adrianiftode changed the title Is it possible to use Wiremock as a middleware? Is it possible to use WireMock as a middleware? Dec 11, 2023
@adrianiftode
Copy link
Author

Yes, but the reason for asking this is I would like to deploy it with the tested service.

I have a service ServiceB that I would like to mock.
Then I have the client service, ServiceA, that will do requests to ServiceB. ServiceB is an external (and expensive and legacy) service, and the only way to use it in test scenarios is to mock it.

I want to configure WireMock inside ServiceA. So when ServiceA is started, the Mocked ServiceB is also started and ready to accept requests. The Mock should listen to any HTTP request to a path that starts with /service-b-mock.

I will give it a try to the WireMockMiddleware.

To expand this, I would also like to deploy a WireMock image configured with different mocked services

  • appBuilder.UseMiddleware("service-b")
  • appBuilder.UseMiddleware("service-c");

This image is then deployed and accessible from the Sandbox/Tests servers, and it has some kind of "mock isolation". So each path prefix has its own WireMock configuration.

@StefH
Copy link
Collaborator

StefH commented Dec 12, 2023

A quick question:

Why not deploy 1 (or multiple) docker container(s) of WireMock.Net ?

@adrianiftode
Copy link
Author

adrianiftode commented Dec 12, 2023

Even deploying it within a separated container, I was hoping to be a single one. I think I will go with this route anyway (multiple containers, one per every mocked service)

@StefH
Copy link
Collaborator

StefH commented Dec 12, 2023

Or use one container and register the mappings with a prefix in the path?

@matteus6007
Copy link

Why not just use wiremock as a docker container using docker compose, see https://github.com/matteus6007/MyDomain.Api.Template/blob/main/docker-compose.dev-env.yml#L58 as an example of setting this up, then add your mocks in the normal JSON format into the __files folder and change any paths in your config to http://localhost:8081/api-name/ where api-name is a unique name for each API you want to mock. Doing it like this means you don't have to add anything specific to your code.

@matthewyost
Copy link

I actually built this as a combination of a .NET HostedService (for Wiremock Server) and a DelegatingHandler which checks the original request headers for something like "X-WireMockStatus" and then rerouted all HttpClient calls to WireMockServer. This worked well to allow me to run this WireMockServer in all our lower environments for testing purposes.

@StefH
Copy link
Collaborator

StefH commented Jul 6, 2024

@matteus6007
Is it possible that you share this complete solution?

@matthewyost
Copy link

@StefH Let me see what I can do about sharing this with you all.

@Act0r
Copy link

Act0r commented Sep 9, 2024

I'm very interested in that question as well. I'm working in enviroment where every service must have a bunch of specific middlewares, i can't avoid it. So i have to add that middlewares to wiremock or wiremock middleware to my service. So if this is possible please provide some hint in either direction

@matthewyost
Copy link

matthewyost commented Sep 10, 2024

So here's a snapshot of how the implementation is performed:

WiremockServerInstance - This is the class that will be used by the background service.

    /// <summary>
    /// WireMockServer Instance object
    /// </summary>
    public class WireMockServerInstance
    {
        private readonly Action<WireMockServer> _configureAction;
        private readonly WireMockServerSettings _settings;

        #region Constructors

        /// <summary>
        /// Creates a new instance and provides ability to add configuration
        /// to the <see cref="WireMockServer"/>
        /// </summary>
        /// <param name="configure"></param>
        public WireMockServerInstance(Action<WireMockServer> configure)
            : this(configure, null) { }

        /// <summary>
        /// Creates a new instance and provides ability to add configuration
        /// for the start method of <see cref="WireMockServer"/>
        /// </summary>
        /// <param name="configure"></param>
        /// <param name="settings"></param>
        public WireMockServerInstance(Action<WireMockServer> configure, WireMockServerSettings settings)
        {
            _configureAction = configure;
            _settings = settings;
        }
        #endregion

        #region Properties

        /// <summary>
        /// Instance accessor for the <see cref="WireMockServer" />
        /// </summary>
        public WireMockServer Instance { get; private set; }

        /// <summary>
        /// Retrieves the URI for the <see cref="WireMockServer"/>
        /// </summary>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public string GetInstanceUri() => Instance.Urls.FirstOrDefault() ?? throw new Exception("No URL found for WireMockServer");

        #endregion

        #region Methods

        /// <summary>
        /// Configures and starts <see cref="WireMockServer"/> instance for use.
        /// </summary>
        public void Start()
        {
            Instance = (_settings != null)
                ? WireMockServer.Start(_settings)
                : WireMockServer.Start();

            _configureAction.Invoke(Instance);
        }

        #endregion

        /// <summary>
        /// Stops the <see cref="WireMockServer"/>
        /// </summary>
        public void Stop()
        {
            if (Instance != null && (Instance.IsStarted || Instance.IsStartedWithAdminInterface))
                Instance.Stop();
        }
    }

WiremockContext - Context to allow me to control certain functionality of the Wiremock instance

    /// <summary>
    /// Wiremock context
    /// </summary>
    public class WiremockContext : IWiremockContext
    {
        /// <summary>
        /// Is Wiremock enabled?
        /// </summary>
        public bool? IsEnabled { get; set; }

        /// <summary>
        /// Duration to delay the response in milliseconds
        /// </summary>
        public int ResponseDelayInMs { get; set; }
    }

WireMockDelegationHandler - DelegatingHandler class allowing us to tap into the HttpClient object and perform our magic redirects to Wiremock without having to change our code. THIS is where the magic happens

    /// <summary>
    /// DelegatingHandler that takes requests made via the <see cref="HttpClient"/>
    /// and routes them to the <see cref="WireMockServer"/>
    /// </summary>
    public class WireMockDelegationHandler : DelegatingHandler
    {
        private readonly WireMockServerInstance _server;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly ILogger<WireMockDelegationHandler> _logger;

        /// <summary>
        /// Creates a new instance of <see cref="WireMockDelegationHandler"/>
        /// </summary>
        /// <param name="server"></param>
        /// <param name="httpContextAccessor"></param>
        /// <param name="logger"></param>
        /// <exception cref="ArgumentNullException"></exception>
        public WireMockDelegationHandler(WireMockServerInstance server, IHttpContextAccessor httpContextAccessor, ILogger<WireMockDelegationHandler> logger)
        {
            _server = server ?? throw new ArgumentNullException(nameof(server));
            _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
            _logger = logger;
        }

        /// <inheritdoc />
        /// <exception cref="ArgumentNullException"></exception>
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (request is null)
                throw new ArgumentNullException(nameof(request));

            if (_httpContextAccessor.HttpContext is null)
                throw new ArgumentNullException(nameof(_httpContextAccessor.HttpContext));

            bool shouldRedirectToWireMock = IsWireMockStatusHeaderSet();

            var (shouldDelayResponse, delayInMs) = IsDelayHeaderSet();

            if (shouldRedirectToWireMock)
            {
                _logger?.LogDebug("Redirecting request to WireMock server");
                if (_server.Instance is not null
                    && _server.Instance.Urls is not null
                    && _server.Instance.Urls.Any())
                    request.RequestUri = new Uri(_server.GetInstanceUri() + request.RequestUri.PathAndQuery);
            }

            if (shouldDelayResponse)
                await Task.Delay(delayInMs);

            return await base.SendAsync(request, cancellationToken);
        }

        private bool IsWireMockStatusHeaderSet()
        {
            bool shouldRedirectToWireMock = false;
            if (_httpContextAccessor.HttpContext.Request.Headers.ContainsKey(AppConstants.HEADER_WIREMOCK_STATUS))
            {
                _logger?.LogDebug("Found WireMock header on request");

                if (_httpContextAccessor.HttpContext.Request.Headers[AppConstants.HEADER_WIREMOCK_STATUS].ToString().Equals("true", StringComparison.OrdinalIgnoreCase))
                    shouldRedirectToWireMock = true;
            }
            return shouldRedirectToWireMock;
        }

        private (bool, int) IsDelayHeaderSet()
        {
            bool shouldDelayResponse = false;
            int delayInMs = 0;

            if (_httpContextAccessor.HttpContext.Request.Headers.ContainsKey(AppConstants.HEADER_RESPONSE_DELAY))
            {
                string delay = _httpContextAccessor.HttpContext.Request.Headers[AppConstants.HEADER_RESPONSE_DELAY].ToString();
                if (!int.TryParse(delay, out delayInMs))
                    throw new ArgumentOutOfRangeException(nameof(delay), "Delay must be an integer");

                _logger?.LogDebug("Delaying response by {0}ms", delayInMs);
                shouldDelayResponse = true;
            }

            return (shouldDelayResponse, delayInMs);
        }
    }

WireMockBgService - This is the background service that will hold onto our instance of Wiremock and allow us to keep from spinning up new copies with every request.

    /// <summary>
    /// <see cref="BackgroundService"/> used to start/stop the <see cref="WireMockServer"/>
    /// </summary>
    public class WireMockBgService : BackgroundService
    {
        private readonly WireMockServerInstance _server;

        /// <summary>
        /// Creates a new <see cref="BackgroundService"/> using an instance
        /// of <see cref="WireMockServerInstance"/>
        /// </summary>
        /// <param name="server"></param>
        public WireMockBgService(WireMockServerInstance server)
        {
            _server = server ?? throw new ArgumentNullException(nameof(server));
        }

        /// <inheritdoc />
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _server.Start();
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public override Task StopAsync(CancellationToken cancellationToken)
        {
            _server.Stop();
            return base.StopAsync(cancellationToken);
        }
    }

ServiceCollectionExtensions - Extension methods to make it easy for integrating into any app.

    /// <summary>
    /// Extension methods for <see cref="IServiceCollection"/>.
    /// </summary>
    public static class ServiceCollectionExtensions
    {
        /// <summary>
        /// Adds all the components necessary to run Wiremock.NET in the background.
        /// </summary>
        /// <param name="services"></param>
        /// <param name="server"></param>
        /// <returns></returns>
        public static IServiceCollection AddWireMockService(this IServiceCollection services, Action<WireMockServer> server)
        {
            return services.AddWireMockService(server, null);
        }

        /// <summary>
        /// Adds all the components necessary to run Wiremock.NET in the background.
        /// </summary>
        /// <param name="services"></param>
        /// <param name="configure"></param>
        /// <param name="settings"></param>
        /// <returns></returns>
        public static IServiceCollection AddWireMockService(this IServiceCollection services, Action<WireMockServer> configure, WireMockServerSettings settings)
        {
            services.AddTransient<WireMockDelegationHandler>();

            if (settings is null)
                services.AddSingleton(new WireMockServerInstance(configure));
            else
                services.AddSingleton(new WireMockServerInstance(configure, settings));

            services.AddHostedService<WireMockBgService>();
            services.AddHttpClient();
            services.AddHttpContextAccessor();
            services.ConfigureAll<HttpClientFactoryOptions>(options =>
            {
                options.HttpMessageHandlerBuilderActions.Add(builder =>
                {
                    builder.AdditionalHandlers.Add(builder.Services.GetRequiredService<WireMockDelegationHandler>());
                });
            });
            return services;
        }
    }

Now for it's usage! Using a minimal API, we can have something like this:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();

if (!builder.Environment.IsProduction())
{
    builder.Services.AddWireMockService(server =>
    {
            server.Given(Request.Create()
                .WithPath("<your path that you want to mock>")
                .UsingAnyMethod()
            ).RespondWith(Response.Create()
                .WithStatusCode(200)
                .WithBody("<your body to respond with when it's called>");
    });
}

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseAuthorization();

app.MapControllers();

app.Run();

Hopefully this helps some of y'all develop the pattern to make this a thing!

@Act0r
Copy link

Act0r commented Sep 11, 2024

Thank you very much for the detailed example

@StefH
Copy link
Collaborator

StefH commented Sep 19, 2024

@matthewyost
I did try your solution, however it seems that the path defined in WithPath is not available, it returns a 404.

@matthewyost
Copy link

@StefH The stuff between the < > is just supposed to be whatever path you want to use. It's meant to be replaced with whatever path you're attempting to mock a response for.

@StefH
Copy link
Collaborator

StefH commented Sep 19, 2024

@StefH The stuff between the < > is just supposed to be whatever path you want to use. It's meant to be replaced with whatever path you're attempting to mock a response for.

About the "path" : I did chaage that. If you have time, please review my PR: #1175

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants