Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/download-native-libs.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

FFI_VERSION="0.4.27"
FFI_VERSION="0.5.12"
FFI_BASE_URL="https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v$FFI_VERSION"

GREEN="\e[32m"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@
],
"metadata": {
"pactRust": {
"ffi": "0.4.27",
"models": "1.2.8"
"ffi": "0.5.12",
"models": "1.3.17"
},
"pactSpecification": {
"version": "4.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;

namespace PactNet.Exceptions
{
/// <summary>
/// The mock server received requests which didn't match the configured interactions, or didn't receive the
/// expected requests
/// </summary>
[Serializable]
public class PactMockServerMismatchException : PactFailureException
{
/// <summary>
/// Mismatches reported by the mock server, as a JSON array (the format produced by <c>pactffi_mock_server_mismatches</c>)
/// </summary>
public string Mismatches { get; }

/// <summary>
/// Log lines captured for this mock server (the format produced by <c>pactffi_mock_server_logs</c>)
/// </summary>
/// <remarks>
/// The level of these logs is set by <see cref="PactConfig.LogLevel"/> the first time a pact is created
/// in the process, because the native logger can only be initialised once
/// </remarks>
public string MockServerLogs { get; }

/// <summary>
/// Initialises a new instance of the <see cref="PactMockServerMismatchException"/> class.
/// </summary>
/// <param name="message">The message that describes the error</param>
/// <param name="mismatches">Mismatches reported by the mock server, as a JSON array</param>
/// <param name="mockServerLogs">Log lines captured for this mock server</param>
public PactMockServerMismatchException(string message, string mismatches, string mockServerLogs)
: base(message)
{
this.Mismatches = mismatches;
this.MockServerLogs = mockServerLogs;
}
}
}
72 changes: 72 additions & 0 deletions src/PactNet.Abstractions/IRequestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,30 @@ public interface IRequestBuilderV2
/// <returns>Fluent builder</returns>
IRequestBuilderV2 WithHeader(string key, IMatcher matcher);

/// <summary>
/// Add a request header with multiple values, e.g. a header which appears on multiple field lines
/// </summary>
/// <param name="key">Header key</param>
/// <param name="values">Header values, in order</param>
/// <returns>Fluent builder</returns>
/// <remarks>
/// Equivalent to calling <c>WithHeader(key, value)</c> once per value. Each value is added at the next index for
/// that header, so values added by previous calls for the same key are kept and these are appended after them.
/// </remarks>
IRequestBuilderV2 WithHeader(string key, params string[] values);

/// <summary>
/// Add a request header with multiple values, each with its own matcher
/// </summary>
/// <param name="key">Header key</param>
/// <param name="matchers">Header value matchers, in order</param>
/// <returns>Fluent builder</returns>
/// <remarks>
/// Equivalent to calling <c>WithHeader(key, matcher)</c> once per matcher. Each matcher is added at the next index
/// for that header, so values added by previous calls for the same key are kept and these are appended after them.
/// </remarks>
IRequestBuilderV2 WithHeader(string key, params IMatcher[] matchers);

/// <summary>
/// Set a body which is serialised as JSON
/// </summary>
Expand Down Expand Up @@ -175,6 +199,30 @@ public interface IRequestBuilderV3
/// <returns>Fluent builder</returns>
IRequestBuilderV3 WithHeader(string key, IMatcher matcher);

/// <summary>
/// Add a request header with multiple values, e.g. a header which appears on multiple field lines
/// </summary>
/// <param name="key">Header key</param>
/// <param name="values">Header values, in order</param>
/// <returns>Fluent builder</returns>
/// <remarks>
/// Equivalent to calling <c>WithHeader(key, value)</c> once per value. Each value is added at the next index for
/// that header, so values added by previous calls for the same key are kept and these are appended after them.
/// </remarks>
IRequestBuilderV3 WithHeader(string key, params string[] values);

/// <summary>
/// Add a request header with multiple values, each with its own matcher
/// </summary>
/// <param name="key">Header key</param>
/// <param name="matchers">Header value matchers, in order</param>
/// <returns>Fluent builder</returns>
/// <remarks>
/// Equivalent to calling <c>WithHeader(key, matcher)</c> once per matcher. Each matcher is added at the next index
/// for that header, so values added by previous calls for the same key are kept and these are appended after them.
/// </remarks>
IRequestBuilderV3 WithHeader(string key, params IMatcher[] matchers);

/// <summary>
/// Set a body which is serialised as JSON
/// </summary>
Expand Down Expand Up @@ -294,6 +342,30 @@ public interface IRequestBuilderV4
/// <returns>Fluent builder</returns>
IRequestBuilderV4 WithHeader(string key, IMatcher matcher);

/// <summary>
/// Add a request header with multiple values, e.g. a header which appears on multiple field lines
/// </summary>
/// <param name="key">Header key</param>
/// <param name="values">Header values, in order</param>
/// <returns>Fluent builder</returns>
/// <remarks>
/// Equivalent to calling <c>WithHeader(key, value)</c> once per value. Each value is added at the next index for
/// that header, so values added by previous calls for the same key are kept and these are appended after them.
/// </remarks>
IRequestBuilderV4 WithHeader(string key, params string[] values);

/// <summary>
/// Add a request header with multiple values, each with its own matcher
/// </summary>
/// <param name="key">Header key</param>
/// <param name="matchers">Header value matchers, in order</param>
/// <returns>Fluent builder</returns>
/// <remarks>
/// Equivalent to calling <c>WithHeader(key, matcher)</c> once per matcher. Each matcher is added at the next index
/// for that header, so values added by previous calls for the same key are kept and these are appended after them.
/// </remarks>
IRequestBuilderV4 WithHeader(string key, params IMatcher[] matchers);

/// <summary>
/// Set a body which is serialised as JSON
/// </summary>
Expand Down
72 changes: 72 additions & 0 deletions src/PactNet.Abstractions/IResponseBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,30 @@ public interface IResponseBuilderV2
/// <returns>Fluent builder</returns>
IResponseBuilderV2 WithHeader(string key, IMatcher matcher);

/// <summary>
/// Add a response header with multiple values, e.g. a header which appears on multiple field lines
/// </summary>
/// <param name="key">Header key</param>
/// <param name="values">Header values, in order</param>
/// <returns>Fluent builder</returns>
/// <remarks>
/// Equivalent to calling <c>WithHeader(key, value)</c> once per value. Each value is added at the next index for
/// that header, so values added by previous calls for the same key are kept and these are appended after them.
/// </remarks>
IResponseBuilderV2 WithHeader(string key, params string[] values);

/// <summary>
/// Add a response header with multiple values, each with its own matcher
/// </summary>
/// <param name="key">Header key</param>
/// <param name="matchers">Header value matchers, in order</param>
/// <returns>Fluent builder</returns>
/// <remarks>
/// Equivalent to calling <c>WithHeader(key, matcher)</c> once per matcher. Each matcher is added at the next index
/// for that header, so values added by previous calls for the same key are kept and these are appended after them.
/// </remarks>
IResponseBuilderV2 WithHeader(string key, params IMatcher[] matchers);

/// <summary>
/// Set a response body which is serialised as JSON
/// </summary>
Expand Down Expand Up @@ -98,6 +122,30 @@ public interface IResponseBuilderV3
/// <returns>Fluent builder</returns>
IResponseBuilderV3 WithHeader(string key, IMatcher matcher);

/// <summary>
/// Add a response header with multiple values, e.g. a header which appears on multiple field lines
/// </summary>
/// <param name="key">Header key</param>
/// <param name="values">Header values, in order</param>
/// <returns>Fluent builder</returns>
/// <remarks>
/// Equivalent to calling <c>WithHeader(key, value)</c> once per value. Each value is added at the next index for
/// that header, so values added by previous calls for the same key are kept and these are appended after them.
/// </remarks>
IResponseBuilderV3 WithHeader(string key, params string[] values);

/// <summary>
/// Add a response header with multiple values, each with its own matcher
/// </summary>
/// <param name="key">Header key</param>
/// <param name="matchers">Header value matchers, in order</param>
/// <returns>Fluent builder</returns>
/// <remarks>
/// Equivalent to calling <c>WithHeader(key, matcher)</c> once per matcher. Each matcher is added at the next index
/// for that header, so values added by previous calls for the same key are kept and these are appended after them.
/// </remarks>
IResponseBuilderV3 WithHeader(string key, params IMatcher[] matchers);

/// <summary>
/// Set a response body which is serialised as JSON
/// </summary>
Expand Down Expand Up @@ -157,6 +205,30 @@ public interface IResponseBuilderV4
/// <returns>Fluent builder</returns>
IResponseBuilderV4 WithHeader(string key, IMatcher matcher);

/// <summary>
/// Add a response header with multiple values, e.g. a header which appears on multiple field lines
/// </summary>
/// <param name="key">Header key</param>
/// <param name="values">Header values, in order</param>
/// <returns>Fluent builder</returns>
/// <remarks>
/// Equivalent to calling <c>WithHeader(key, value)</c> once per value. Each value is added at the next index for
/// that header, so values added by previous calls for the same key are kept and these are appended after them.
/// </remarks>
IResponseBuilderV4 WithHeader(string key, params string[] values);

/// <summary>
/// Add a response header with multiple values, each with its own matcher
/// </summary>
/// <param name="key">Header key</param>
/// <param name="matchers">Header value matchers, in order</param>
/// <returns>Fluent builder</returns>
/// <remarks>
/// Equivalent to calling <c>WithHeader(key, matcher)</c> once per matcher. Each matcher is added at the next index
/// for that header, so values added by previous calls for the same key are kept and these are appended after them.
/// </remarks>
IResponseBuilderV4 WithHeader(string key, params IMatcher[] matchers);

/// <summary>
/// Set a response body which is serialised as JSON
/// </summary>
Expand Down
33 changes: 21 additions & 12 deletions src/PactNet/PactBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,51 +73,59 @@ internal RequestBuilder UponReceiving(string description)
/// Verify the configured interactions
/// </summary>
/// <param name="interact">Action to perform the real interactions against the mock driver</param>
/// <exception cref="PactFailureException">Failed to verify the interactions</exception>
/// <exception cref="PactMockServerMismatchException">Failed to verify the interactions</exception>
public void Verify(Action<IConsumerContext> interact)
{
Guard.NotNull(interact, nameof(interact));

using IMockServerDriver mockServer = this.StartMockServer();

// reading the logs drains the FFI buffer, so read them at most once
string logs = null;
string ReadLogs() => logs ??= mockServer.MockServerLogs();

try
{
interact(new ConsumerContext
{
MockServerUri = mockServer.Uri
});

this.VerifyInternal(mockServer);
this.VerifyInternal(mockServer, ReadLogs);
}
finally
{
this.PrintLogs(mockServer);
this.PrintLogs(ReadLogs());
}
}

/// <summary>
/// Verify the configured interactions
/// </summary>
/// <param name="interact">Action to perform the real interactions against the mock driver</param>
/// <exception cref="PactFailureException">Failed to verify the interactions</exception>
/// <exception cref="PactMockServerMismatchException">Failed to verify the interactions</exception>
public async Task VerifyAsync(Func<IConsumerContext, Task> interact)
{
Guard.NotNull(interact, nameof(interact));

using IMockServerDriver mockServer = this.StartMockServer();

// reading the logs drains the FFI buffer, so read them at most once
string logs = null;
string ReadLogs() => logs ??= mockServer.MockServerLogs();

try
{
await interact(new ConsumerContext
{
MockServerUri = mockServer.Uri
});

this.VerifyInternal(mockServer);
this.VerifyInternal(mockServer, ReadLogs);
}
finally
{
this.PrintLogs(mockServer);
this.PrintLogs(ReadLogs());
}
}

Expand All @@ -142,7 +150,8 @@ private IMockServerDriver StartMockServer()
/// Verify the interactions after the consumer client has been invoked
/// </summary>
/// <param name="mockServer">Mock server</param>
private void VerifyInternal(IMockServerDriver mockServer)
/// <param name="readLogs">Reads the mock server logs</param>
private void VerifyInternal(IMockServerDriver mockServer, Func<string> readLogs)
{
string errors = mockServer.MockServerMismatches();

Expand All @@ -157,17 +166,17 @@ private void VerifyInternal(IMockServerDriver mockServer)
this.config.WriteLine(string.Empty);
this.config.WriteLine(errors);

throw new PactFailureException("Pact verification failed. See output for details");
throw new PactMockServerMismatchException("Pact verification failed. See output for details",
errors,
readLogs());
}

/// <summary>
/// Print logs to the configured outputs
/// </summary>
/// <param name="mockServer">Mock server</param>
private void PrintLogs(IMockServerDriver mockServer)
/// <param name="logs">Mock server logs</param>
private void PrintLogs(string logs)
{
string logs = mockServer.MockServerLogs();

this.config.WriteLine("Mock driver logs:");
this.config.WriteLine(string.Empty);
this.config.WriteLine(logs);
Expand Down
Loading
Loading