PactVerifier.cs
using System;
using System.IO;
using Newtonsoft.Json;
using System.Linq;
using ComPact.Models;
using ComPact.Models.V3;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text;
using Newtonsoft.Json.Linq;
using ComPact.Exceptions;
namespace ComPact.Verifier
{
public clast PactVerifier
{
private readonly PactVerifierConfig _config;
///
/// Set up a mock consumer that will call your code based on the defined interactions or messages in a supplied Pact contract.
///
///
public PactVerifier(PactVerifierConfig config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
}
///
///
///
/// Will be interpreted as Pact Broker path when a Pact Broker Client is configured and as a file path otherwise.
///
public async Task VerifyPactAsync(string path)
{
string pactContent = null;
string publishVerificationResultsUrl = null;
if (_config.PactBrokerClient != null)
{
var pactBrokerResults = await GetPactFromBroker(_config.PactBrokerClient, path);
pactContent = pactBrokerResults.PactContent;
publishVerificationResultsUrl = pactBrokerResults.PublishVerificationResultsUrl;
}
else
{
pactContent = ReadPactFile(path);
}
Contract pact = DeserializePactContent(pactContent);
var tests = new List();
if (pact.Interactions != null)
{
if (_config.ProviderBaseUrl == null && _config.ProviderHttpClient?.BaseAddress == null)
{
throw new PactException("Could not verify pacts. Please configure a ProviderBaseUrl or a pre-configured ProviderHttpClient with at least a BaseAddress.");
}
var client = _config.ProviderHttpClient ?? new HttpClient {BaseAddress = new Uri(_config.ProviderBaseUrl) };
tests.AddRange(await VerifyInteractions(pact.Interactions, client.SendAsync, _config.ProviderStateHandler));
}
if (pact.Messages != null)
{
tests.AddRange(VerifyMessages(pact.Messages, _config.ProviderStateHandler, _config.MessageProducer));
}
if (_config.PublishVerificationResults)
{
await PublishVerificationResultsAsync(pact, tests, _config.ProviderVersion, _config.PactBrokerClient, publishVerificationResultsUrl);
var tags = _config.ProviderTags?.ToList();
if (tags?.FirstOrDefault() != null)
{
if (tags.Any())
{
await PublishTags(_config.PactBrokerClient, pact.Provider.Name, _config.ProviderVersion, tags);
}
}
}
if (tests.Any(t => t.Status == "failed"))
{
throw new PactVerificationException(string.Join(Environment.NewLine, tests.Select(f => f.ToTestMessageString())));
}
}
internal static async Task GetPactFromBroker(HttpClient pactBrokerClient, string path)
{
if (pactBrokerClient.BaseAddress == null)
{
throw new PactException("A PactBrokerClient with at least a BaseAddress should be configured to be able to retrieve contracts.");
}
HttpResponseMessage response;
try
{
response = await pactBrokerClient.GetAsync(path);
}
catch (Exception e)
{
throw new PactException($"Pact cannot be retrieved using the provided Pact Broker Client: {e.Message}");
}
if (!response.IsSuccessStatusCode)
{
throw new PactException("Getting pact from Pact Broker failed. Pact Broker returned " + response.StatusCode);
}
var stringContent = await response.Content.ReadasttringAsync();
var pactJObject = JObject.Parse(stringContent);
var publishVerificationResultsUrl = pactJObject.SelectToken("_links.pb:publish-verification-results.href").Value();
return new PactBrokerResults
{
PactContent = stringContent,
PublishVerificationResultsUrl = publishVerificationResultsUrl
};
}
internal static string ReadPactFile(string filePath)
{
try
{
return File.ReadAllText(filePath);
}
catch
{
throw new PactException($"Could not read file at {filePath}");
}
}
internal static Contract DeserializePactContent(string pactContent)
{
try
{
var contractWithSomeVersion = JsonConvert.DeserializeObject(pactContent);
if (contractWithSomeVersion.Metadata.GetVersion() == SpecificationVersion.Three)
{
return JsonConvert.DeserializeObject(pactContent);
}
else if (contractWithSomeVersion.Metadata.GetVersion() == SpecificationVersion.Two)
{
var pactV2 = JsonConvert.DeserializeObject(pactContent);
return new Contract(pactV2);
}
else if (contractWithSomeVersion.Metadata.GetVersion() == SpecificationVersion.Unsupported)
{
throw new PactException("Pact specification version is not supported.");
}
}
catch (Exception e)
{
throw new PactException($"File was not recognized as a valid Pact contract: {e.Message}");
}
return null;
}
internal static async Task VerifyInteractions(List interactions, Func providerClient, Action providerStateHandler)
{
var tests = new List();
foreach (var interaction in interactions)
{
var test = new Test { Description = interaction.Description };
var verificationMessages = InvokeProviderStateHandler(interaction.ProviderStates, providerStateHandler);
if (verificationMessages.Any())
{
test.Issues = verificationMessages;
}
else
{
var httpRequestMessage = interaction.Request.ToHttpRequestMessage();
var actualResponse = await providerClient.Invoke(httpRequestMessage);
var differences = interaction.Response.Match(new Response(actualResponse));
if (differences.Any())
{
test.Issues = differences;
}
}
tests.Add(test);
}
return tests;
}
internal static List VerifyMessages(List messages, Action providerStateHandler, Func messageProducer)
{
var tests = new List();
foreach (var message in messages)
{
var test = new Test { Description = message.Description };
var verificationMessages = InvokeProviderStateHandler(message.ProviderStates, providerStateHandler);
if (verificationMessages.Any())
{
test.Issues = verificationMessages;
}
else
{
object providedMessage = null;
try
{
providedMessage = messageProducer.Invoke(message.Description);
}
catch (PactVerificationException e)
{
test.Issues = new List { $"Provider could not produce message {message.Description}: {e.Message}" };
tests.Add(test);
continue;
}
catch
{
throw new PactException("Exception occured while invoking MessageProducer.");
}
if (providedMessage is string messageString)
{
providedMessage = JsonConvert.DeserializeObject(messageString);
}
else
{
providedMessage = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(providedMessage));
}
var differences = message.Match(providedMessage);
if (differences.Any())
{
test.Issues = differences;
}
}
tests.Add(test);
}
return tests;
}
internal static List InvokeProviderStateHandler(List providerStates, Action providerStateHandler)
{
var verificationMessages = new List();
if (providerStates == null)
{
return verificationMessages;
}
if (providerStateHandler == null)
{
throw new PactException("Cannot verify this Pact contract because a ProviderStateHandler was not configured.");
}
foreach (var providerState in providerStates)
{
try
{
providerStateHandler.Invoke(providerState);
}
catch (PactVerificationException e)
{
verificationMessages.Add($"Provider could not handle provider state \"{providerState.Name}\": {e.Message}");
}
catch
{
throw new PactException("Exception occured while invoking ProviderStateHandler.");
}
}
return verificationMessages;
}
internal static async Task PublishVerificationResultsAsync(Contract pact, List tests, string providerVersion, HttpClient pactBrokerClient, string publishVerificationResultsUrl)
{
if (string.IsNullOrWhiteSpace(providerVersion))
{
throw new PactException("ProviderVersion should be configured to be able to publish verification results.");
}
var failureCount = tests.Count(t => t.Status == "failed");
var testResults = new TestResults
{
Summary = new Summary { TestCount = tests.Count(), FailureCount = failureCount },
Tests = tests
};
var verificationResults = new VerificationResults
{
ProviderName = pact.Provider.Name,
ProviderApplicationVersion = providerVersion,
Success = failureCount == 0,
VerificationDate = DateTime.UtcNow.ToString("u"),
TestResults = testResults
};
var content = new StringContent(JsonConvert.SerializeObject(verificationResults), Encoding.UTF8, "application/json");
var response = await pactBrokerClient.PostAsync(publishVerificationResultsUrl, content);
if (!response.IsSuccessStatusCode)
{
throw new PactException("Publishing verification results failed. Pact Broker returned " + response.StatusCode);
}
}
internal static async Task PublishTags(HttpClient pactBrokerClient, string providerName, string providerVersion, IList tags)
{
foreach (var tag in tags)
{
var content = new StringContent("", Encoding.UTF8, "application/json");
var response = await pactBrokerClient.PutAsync($"pacticipants/{WebUtility.UrlEncode(providerName)}/versions/{WebUtility.UrlEncode(providerVersion)}/tags/{WebUtility.UrlEncode(tag)}", content);
if (!response.IsSuccessStatusCode)
{
throw new PactException($"Publishing tag '{tag}' failed. Pact Broker returned " + response.StatusCode);
}
}
}
}
internal clast PactBrokerResults
{
internal string PactContent { get; set; }
internal string PublishVerificationResultsUrl { get; set; }
}
}