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

feat: Remote mock server service for consumer tests #277

Merged
merged 18 commits into from May 10, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -282,7 +282,7 @@ public void WillRespondWith_WithResponseThatDoesNotHaveAResponseStatusSet_Throws
}

[Fact]
public void WillRespondWith_WhenHostIsNull_ThrowsInvalidOperationException()
public void WillRespondWith_WhenHostIsNull_DoNotThrowsInvalidOperationException()
{
var providerState = "My provider state";
var description = "My description";
Expand All @@ -298,8 +298,7 @@ public void WillRespondWith_WhenHostIsNull_ThrowsInvalidOperationException()

mockService.Stop();

Assert.Throws<InvalidOperationException>(() => mockService.WillRespondWith(response));
Assert.Equal(0, _fakeHttpMessageHandler.RequestsReceived.Count());
Assert.Equal(1, _fakeHttpMessageHandler.RequestsReceived.Count());
}

[Fact]
Expand Down Expand Up @@ -398,14 +397,14 @@ public void VerifyInteractions_WhenResponseFromHostIsNotOk_ThrowsPactFailureExce
}

[Fact]
public void ClearInteractions_WhenHostIsNull_DoesNotPerformAdminInteractionsDeleteRequest()
public void ClearInteractions_WhenHostIsNull_CanPerformAdminInteractionsDeleteRequest()
{
var mockService = GetSubject();
mockService.Stop();

mockService.ClearInteractions();

Assert.Equal(0, _fakeHttpMessageHandler.RequestsReceived.Count());
Assert.Equal(2, _fakeHttpMessageHandler.RequestsReceived.Count());
}

[Fact]
Expand Down
1 change: 1 addition & 0 deletions PactNet.Tests/PactBuilderTests.cs
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using NSubstitute;
using PactNet.Configuration.Json;
Expand Down
4 changes: 2 additions & 2 deletions PactNet/IPactBuilder.cs
Expand Up @@ -8,9 +8,9 @@ public interface IPactBuilder
{
IPactBuilder ServiceConsumer(string consumerName);
IPactBuilder HasPactWith(string providerName);
IMockProviderService MockService(int port, bool enableSsl = false, IPAddress host = IPAddress.Loopback, string sslCert = null, string sslKey = null);
IMockProviderService MockService(int port, bool enableSsl = false, IPAddress host = IPAddress.Loopback, string sslCert = null, string sslKey = null, bool useRemoteMockService = false);
IMockProviderService MockService(int port, JsonSerializerSettings jsonSerializerSettings,
bool enableSsl = false, IPAddress host = IPAddress.Loopback, string sslCert = null, string sslKey = null);
bool enableSsl = false, IPAddress host = IPAddress.Loopback, string sslCert = null, string sslKey = null, bool useRemoteMockService = false);

void Build();
}
Expand Down
11 changes: 9 additions & 2 deletions PactNet/Mocks/MockHttpService/AdminHttpClient.cs
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -39,9 +41,9 @@ internal class AdminHttpClient
{
}

public async Task SendAdminHttpRequest(HttpVerb method, string path)
public async Task SendAdminHttpRequest(HttpVerb method, string path, IDictionary<string, string> headers = null)
{
await SendAdminHttpRequest<object>(method, path, null);
await SendAdminHttpRequest<object>(method, path, null, headers:headers);
}

public async Task SendAdminHttpRequest<T>(HttpVerb method, string path, T requestContent, IDictionary<string, string> headers = null) where T : class
Expand Down Expand Up @@ -76,6 +78,11 @@ public async Task SendAdminHttpRequest(HttpVerb method, string path)
Dispose(request);
Dispose(response);

if (path == "/pact" && headers != null && headers.ContainsKey("fileNameAndPath"))
HugoDoyon marked this conversation as resolved.
Show resolved Hide resolved
{
File.WriteAllText(headers["fileNameAndPath"], responseContent);
}

if (responseStatusCode != HttpStatusCode.OK)
{
throw new PactFailureException(responseContent);
Expand Down
4 changes: 3 additions & 1 deletion PactNet/Mocks/MockHttpService/IMockProviderService.cs
@@ -1,15 +1,17 @@
using System.Collections.Generic;
using PactNet.Mocks.MockHttpService.Models;

namespace PactNet.Mocks.MockHttpService
{
public interface IMockProviderService : IMockProvider<IMockProviderService>
{
IMockProviderService With(ProviderServiceRequest request);
bool UseRemoteMockService { get; set; }
void WillRespondWith(ProviderServiceResponse response);
void Start();
void Stop();
void ClearInteractions();
void VerifyInteractions();
void SendAdminHttpRequest(HttpVerb method, string path);
void SendAdminHttpRequest(HttpVerb method, string path, Dictionary<string, string> headers = null);
}
}
36 changes: 27 additions & 9 deletions PactNet/Mocks/MockHttpService/MockProviderService.cs
Expand Up @@ -21,14 +21,16 @@ public class MockProviderService : IMockProviderService

public Uri BaseUri { get; }

public bool UseRemoteMockService { get; set; } = false;

internal MockProviderService(
Func<Uri, IHttpHost> hostFactory,
int port,
bool enableSsl,
Func<Uri, AdminHttpClient> adminHttpClientFactory)
{
_hostFactory = hostFactory;
BaseUri = new Uri( $"{(enableSsl ? "https" : "http")}://localhost:{port}");
BaseUri = new Uri($"{(enableSsl ? "https" : "http")}://localhost:{port}");
_adminHttpClient = adminHttpClientFactory(BaseUri);
}

Expand All @@ -41,7 +43,7 @@ public MockProviderService(int port, bool enableSsl, string consumerName, string
: this(port, enableSsl, consumerName, providerName, config, ipAddress, jsonSerializerSettings, null, null)
{
}

public MockProviderService(int port, bool enableSsl, string consumerName, string providerName, PactConfig config, IPAddress ipAddress, Newtonsoft.Json.JsonSerializerSettings jsonSerializerSettings, string sslCert, string sslKey)
: this(
baseUri => new RubyHttpHost(baseUri, consumerName, providerName, config, ipAddress, sslCert, sslKey),
Expand Down Expand Up @@ -125,32 +127,48 @@ public void VerifyInteractions()
Async.RunSync(() => _adminHttpClient.SendAdminHttpRequest(HttpVerb.Get, $"{Constants.InteractionsVerificationPath}?example_description={testContext}"));
}

public void SendAdminHttpRequest(HttpVerb method, string path)
public void SendAdminHttpRequest(HttpVerb method, string path, Dictionary<string, string> headers = null)
{
Async.RunSync(() => _adminHttpClient.SendAdminHttpRequest(method, path));
Async.RunSync(() => _adminHttpClient.SendAdminHttpRequest(method, path, headers:headers));
}

public void Start()
{
StopRunningHost();
if (UseRemoteMockService == false)
{
StopRunningHost();
_host = _hostFactory(BaseUri);
_host.Start();
}

_host = _hostFactory(BaseUri);
_host.Start();
}

public void Stop()
{
ClearAllState();
StopRunningHost();
if (UseRemoteMockService == false)
{
StopRunningHost();
}
}

public void ClearInteractions()
{
if (_host != null)
if (_host != null && UseRemoteMockService == false)
HugoDoyon marked this conversation as resolved.
Show resolved Hide resolved
{
var testContext = BuildTestContext();
Async.RunSync(() => _adminHttpClient.SendAdminHttpRequest(HttpVerb.Delete, $"{Constants.InteractionsPath}?example_description={testContext}"));
}
else
{
ClearAllInteractions();
}

}

public void ClearAllInteractions()
HugoDoyon marked this conversation as resolved.
Show resolved Hide resolved
{
Async.RunSync(() => _adminHttpClient.SendAdminHttpRequest(HttpVerb.Delete, $"{Constants.InteractionsPath}?"));
}
HugoDoyon marked this conversation as resolved.
Show resolved Hide resolved

private void RegisterInteraction()
Expand Down
27 changes: 21 additions & 6 deletions PactNet/PactBuilder.cs
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using PactNet.Mocks.MockHttpService;
using PactNet.Mocks.MockHttpService.Models;
Expand All @@ -11,6 +12,8 @@ public class PactBuilder : IPactBuilder
public string ConsumerName { get; private set; }
public string ProviderName { get; private set; }

private readonly string _pactDir;

private readonly
Func<int, bool, string, string, IPAddress, JsonSerializerSettings, string, string, IMockProviderService>
_mockProviderServiceFactory;
Expand All @@ -34,6 +37,7 @@ public PactBuilder(PactConfig config)
new MockProviderService(port, enableSsl, consumerName, providerName, config, host,
jsonSerializerSettings, sslCert, sslKey))
{
_pactDir = config.PactDir;
}

public IPactBuilder ServiceConsumer(string consumerName)
Expand Down Expand Up @@ -65,9 +69,10 @@ public IPactBuilder HasPactWith(string providerName)
bool enableSsl = false,
IPAddress host = IPAddress.Loopback,
string sslCert = null,
string sslKey = null)
string sslKey = null,
bool useRemoteMockService = false)
{
return MockService(port, jsonSerializerSettings: null, enableSsl: enableSsl, host: host, sslCert: sslCert, sslKey: sslKey);
return MockService(port, jsonSerializerSettings: null, enableSsl: enableSsl, host: host, sslCert: sslCert, sslKey: sslKey, useRemoteMockService);
}

public IMockProviderService MockService(
Expand All @@ -76,7 +81,8 @@ public IPactBuilder HasPactWith(string providerName)
bool enableSsl = false,
IPAddress host = IPAddress.Loopback,
string sslCert = null,
string sslKey = null)
string sslKey = null,
bool useRemoteMockService = false)
{
if (string.IsNullOrEmpty(ConsumerName))
{
Expand All @@ -90,14 +96,16 @@ public IPactBuilder HasPactWith(string providerName)
"ProviderName has not been set, please supply a provider name using the HasPactWith method.");
}

if (_mockProviderService != null)
if (_mockProviderService != null && useRemoteMockService == false )
{
_mockProviderService.Stop();
}

_mockProviderService = _mockProviderServiceFactory(port, enableSsl, ConsumerName, ProviderName, host,
jsonSerializerSettings, sslCert, sslKey);

_mockProviderService.UseRemoteMockService = useRemoteMockService;

_mockProviderService.Start();

return _mockProviderService;
Expand All @@ -108,7 +116,7 @@ public void Build()
if (_mockProviderService == null)
{
throw new InvalidOperationException(
"The Pact file could not be saved because the mock provider service is not initialised. Please initialise by calling the MockService() method.");
"The Pact file could not be saved because the mock provider service is not initialized. Please initialise by calling the MockService() method.");
}

PersistPactFile();
Expand All @@ -117,7 +125,14 @@ public void Build()

private void PersistPactFile()
{
_mockProviderService.SendAdminHttpRequest(HttpVerb.Post, Constants.PactPath);
var fileNameAndPathToSavePactTo = new Dictionary<string, string>();

if (_mockProviderService.UseRemoteMockService)
{
fileNameAndPathToSavePactTo.Add("fileNameAndPath", $"{_pactDir}\\{ConsumerName.ToLower()}{ProviderName.ToLower()}.json");
}

_mockProviderService.SendAdminHttpRequest(HttpVerb.Post, Constants.PactPath, fileNameAndPathToSavePactTo.Count == 0? null : fileNameAndPathToSavePactTo);
}
}
}
33 changes: 33 additions & 0 deletions README.md
Expand Up @@ -415,6 +415,39 @@ var sslKey = @"{PathTo}\localhost.key";
MockProviderService = PactBuilder.MockService(MockServerPort, true, IPAddress.Any, sslCrt, sslKey);
```

### Using a Remote Mock Server Service to execute Consumer tests

It is possible to execute your consumer test against any remote Mock Server (including a docker container).

In that case you don't need any other package than PactNet. (Ruby packages are not required i.e.: PactNet.Windows/PactNet.OSX, PactNet.Linux.*)

This also fix the building issues of path too long in windows with ruby packages.

To run your consumer tests against a remote server, in PactBuilder class, set the "useRemoteMockService" parameter to true:

```c#
PactBuilder.MockService(MockServerPort, useRemoteMockService = true);
```

Then simply use the remote host url and port to send the call to your remote server.

The pacts file generated this way will be saved to the path that you define in the PactDir property of PactConfig object.

You can run a remote mock server service with the [pact-cli docker container](https://hub.docker.com/r/pactfoundation/pact-cli) as follow:

```
docker run -dit \
--rm \
--name pact-mock-service \
-p 1234:1234 \
-v ${HOST_PACT_DIRECTORY}:/tmp/pacts \
pactfoundation/pact-cli:latest \
mock-service \
-p 1234 \
--host 0.0.0.0 \
--pact-dir /tmp/pacts
```

### Publishing Pacts to a Broker
The Pact broker is a useful tool that can be used to share pacts between the consumer and provider. In order to make this easy, below are a couple of options for publishing your Pacts to a Pact Broker.

Expand Down