diff --git a/PactNet.sln b/PactNet.sln
index 243019fa..ddecf41d 100644
--- a/PactNet.sln
+++ b/PactNet.sln
@@ -37,6 +37,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PactNet.Output.Xunit", "src\PactNet.Output.Xunit\PactNet.Output.Xunit.csproj", "{02E265A1-A7A2-4106-8F6A-5027FDC3FC50}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Grpc", "Grpc", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrpcGreeter", "samples\Grpc\GrpcGreeter\GrpcGreeter.csproj", "{529F37CB-CDA0-6553-EAC9-8DAC2195ED69}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrpcGreeterClient", "samples\Grpc\GrpcGreeterClient\GrpcGreeterClient.csproj", "{917DAC61-55B4-D721-B1ED-B0E352E4CF1A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrpcGreeterClient.Tests", "samples\Grpc\GrpcGreeterClient.Tests\GrpcGreeterClient.Tests.csproj", "{13756BC3-0750-E2AF-E1F0-565855A3E636}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrpcGreeter.Tests", "samples\Grpc\GrpcGreeter.Tests\GrpcGreeter.Tests.csproj", "{DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PactNet.Extensions.Grpc", "src\PactNet.Extensions.Grpc\PactNet.Extensions.Grpc.csproj", "{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pacts", "pacts", "{AF3752A7-877C-4958-8438-222D2C842D45}"
+ ProjectSection(SolutionItems) = preProject
+ samples\Grpc\pacts\grpc-greeter-client-grpc-greeter.json = samples\Grpc\pacts\grpc-greeter-client-grpc-greeter.json
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PactNet.Extensions.Grpc.Tests", "tests\PactNet.Extensions.Grpc.Tests\PactNet.Extensions.Grpc.Tests.csproj", "{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -155,6 +174,78 @@ Global
{02E265A1-A7A2-4106-8F6A-5027FDC3FC50}.Release|x64.Build.0 = Release|Any CPU
{02E265A1-A7A2-4106-8F6A-5027FDC3FC50}.Release|x86.ActiveCfg = Release|Any CPU
{02E265A1-A7A2-4106-8F6A-5027FDC3FC50}.Release|x86.Build.0 = Release|Any CPU
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69}.Debug|x64.Build.0 = Debug|Any CPU
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69}.Debug|x86.Build.0 = Debug|Any CPU
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69}.Release|Any CPU.Build.0 = Release|Any CPU
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69}.Release|x64.ActiveCfg = Release|Any CPU
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69}.Release|x64.Build.0 = Release|Any CPU
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69}.Release|x86.ActiveCfg = Release|Any CPU
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69}.Release|x86.Build.0 = Release|Any CPU
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A}.Debug|x64.Build.0 = Debug|Any CPU
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A}.Debug|x86.Build.0 = Debug|Any CPU
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A}.Release|x64.ActiveCfg = Release|Any CPU
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A}.Release|x64.Build.0 = Release|Any CPU
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A}.Release|x86.ActiveCfg = Release|Any CPU
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A}.Release|x86.Build.0 = Release|Any CPU
+ {13756BC3-0750-E2AF-E1F0-565855A3E636}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {13756BC3-0750-E2AF-E1F0-565855A3E636}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {13756BC3-0750-E2AF-E1F0-565855A3E636}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {13756BC3-0750-E2AF-E1F0-565855A3E636}.Debug|x64.Build.0 = Debug|Any CPU
+ {13756BC3-0750-E2AF-E1F0-565855A3E636}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {13756BC3-0750-E2AF-E1F0-565855A3E636}.Debug|x86.Build.0 = Debug|Any CPU
+ {13756BC3-0750-E2AF-E1F0-565855A3E636}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {13756BC3-0750-E2AF-E1F0-565855A3E636}.Release|Any CPU.Build.0 = Release|Any CPU
+ {13756BC3-0750-E2AF-E1F0-565855A3E636}.Release|x64.ActiveCfg = Release|Any CPU
+ {13756BC3-0750-E2AF-E1F0-565855A3E636}.Release|x64.Build.0 = Release|Any CPU
+ {13756BC3-0750-E2AF-E1F0-565855A3E636}.Release|x86.ActiveCfg = Release|Any CPU
+ {13756BC3-0750-E2AF-E1F0-565855A3E636}.Release|x86.Build.0 = Release|Any CPU
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}.Debug|x64.Build.0 = Debug|Any CPU
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}.Debug|x86.Build.0 = Debug|Any CPU
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}.Release|x64.ActiveCfg = Release|Any CPU
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}.Release|x64.Build.0 = Release|Any CPU
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}.Release|x86.ActiveCfg = Release|Any CPU
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6}.Release|x86.Build.0 = Release|Any CPU
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Debug|x64.Build.0 = Debug|Any CPU
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Debug|x86.Build.0 = Debug|Any CPU
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Release|Any CPU.Build.0 = Release|Any CPU
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Release|x64.ActiveCfg = Release|Any CPU
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Release|x64.Build.0 = Release|Any CPU
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Release|x86.ActiveCfg = Release|Any CPU
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Release|x86.Build.0 = Release|Any CPU
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Debug|x64.Build.0 = Debug|Any CPU
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Debug|x86.Build.0 = Debug|Any CPU
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Release|x64.ActiveCfg = Release|Any CPU
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Release|x64.Build.0 = Release|Any CPU
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Release|x86.ActiveCfg = Release|Any CPU
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -170,6 +261,14 @@ Global
{5E915D66-917B-4730-B31A-C9727C196346} = {6663C12E-9912-40D0-9310-D119D1F6B023}
{D8B75E48-6E45-468B-8049-B73823C14CB8} = {6663C12E-9912-40D0-9310-D119D1F6B023}
{02E265A1-A7A2-4106-8F6A-5027FDC3FC50} = {CF67D7A1-AE96-420B-9971-65E535B903E8}
+ {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {547DB478-460A-428F-9371-1D653CE85DB5}
+ {529F37CB-CDA0-6553-EAC9-8DAC2195ED69} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {917DAC61-55B4-D721-B1ED-B0E352E4CF1A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {13756BC3-0750-E2AF-E1F0-565855A3E636} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {30EF8B05-ACE6-482B-97D3-B8EE45F1DE10} = {CF67D7A1-AE96-420B-9971-65E535B903E8}
+ {AF3752A7-877C-4958-8438-222D2C842D45} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA} = {A117BBC6-60BB-4282-BF10-E616DE0AFAD0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C2CBC30C-92D4-4E3A-A5B8-1E5D4E938DFC}
diff --git a/samples/Grpc/GrpcGreeter.Tests/GrpcGreeter.Tests.csproj b/samples/Grpc/GrpcGreeter.Tests/GrpcGreeter.Tests.csproj
new file mode 100644
index 00000000..1fdf3de2
--- /dev/null
+++ b/samples/Grpc/GrpcGreeter.Tests/GrpcGreeter.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+ net8.0
+ false
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Grpc/GrpcGreeter.Tests/GrpcGreeterTest.cs b/samples/Grpc/GrpcGreeter.Tests/GrpcGreeterTest.cs
new file mode 100644
index 00000000..27a6806e
--- /dev/null
+++ b/samples/Grpc/GrpcGreeter.Tests/GrpcGreeterTest.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using PactNet;
+using PactNet.Exceptions;
+using PactNet.Infrastructure.Outputters;
+using Xunit;
+using PactNet.Output.Xunit;
+using PactNet.Verifier;
+using Xunit.Abstractions;
+
+namespace GrpcGreeter.Tests
+{
+ public class GrpcGreeterTests(ITestOutputHelper output, ServerFixture serverFixture) : IClassFixture, IDisposable
+ {
+ private readonly PactVerifier verifier = new("Grpc Greeter Api", new PactVerifierConfig
+ {
+ LogLevel = PactLogLevel.Information,
+ Outputters = new List
+ {
+ new XunitOutput(output)
+ }
+ });
+
+ private readonly string pactPath = Path.Combine("..", "..", "..", "..", "..", "Grpc", "pacts",
+ "grpc-greeter-client-grpc-greeter.json");
+
+ [Fact]
+ public void VerificationThrowsExceptionWhenNoRunningProvider()
+ {
+ Assert.Throws(() => verifier
+ .WithHttpEndpoint(new Uri("http://localhost:5060"))
+ .WithFileSource(new FileInfo(pactPath))
+ .Verify());
+ }
+
+ [Fact]
+ public void VerificationSuccessForRunningProvider()
+ {
+ verifier.WithHttpEndpoint(serverFixture.ProviderUri)
+ .WithFileSource(new FileInfo(pactPath))
+ .Verify();
+ }
+
+ public void Dispose()
+ {
+ this.verifier?.Dispose();
+ }
+ }
+}
diff --git a/samples/Grpc/GrpcGreeter.Tests/ServerFixture.cs b/samples/Grpc/GrpcGreeter.Tests/ServerFixture.cs
new file mode 100644
index 00000000..f2f64b20
--- /dev/null
+++ b/samples/Grpc/GrpcGreeter.Tests/ServerFixture.cs
@@ -0,0 +1,28 @@
+using System;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+using PactNet;
+using PactNet.Interop;
+
+namespace GrpcGreeter.Tests;
+
+public class ServerFixture : IDisposable
+{
+ public readonly Uri ProviderUri = new("http://localhost:5000");
+ private readonly IHost server;
+
+ public ServerFixture()
+ {
+ this.server = Host.CreateDefaultBuilder()
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseUrls(this.ProviderUri.ToString());
+ webBuilder.UseStartup();
+ })
+ .Build();
+
+ this.server.Start();
+ }
+
+ public void Dispose() => this.server?.Dispose();
+}
diff --git a/samples/Grpc/GrpcGreeter/GrpcGreeter.csproj b/samples/Grpc/GrpcGreeter/GrpcGreeter.csproj
new file mode 100644
index 00000000..ef753ccd
--- /dev/null
+++ b/samples/Grpc/GrpcGreeter/GrpcGreeter.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Grpc/GrpcGreeter/Program.cs b/samples/Grpc/GrpcGreeter/Program.cs
new file mode 100644
index 00000000..21afa97a
--- /dev/null
+++ b/samples/Grpc/GrpcGreeter/Program.cs
@@ -0,0 +1,16 @@
+namespace GrpcGreeter;
+
+public class GrpcGreeterService
+{
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+}
diff --git a/samples/Grpc/GrpcGreeter/Properties/launchSettings.json b/samples/Grpc/GrpcGreeter/Properties/launchSettings.json
new file mode 100644
index 00000000..f67a267d
--- /dev/null
+++ b/samples/Grpc/GrpcGreeter/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5251",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:7272;http://localhost:5251",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/samples/Grpc/GrpcGreeter/Protos/greet.proto b/samples/Grpc/GrpcGreeter/Protos/greet.proto
new file mode 100644
index 00000000..02343fa0
--- /dev/null
+++ b/samples/Grpc/GrpcGreeter/Protos/greet.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+option csharp_namespace = "GrpcGreeter";
+
+package greet;
+
+// The greeting service definition.
+service Greeter {
+ // Sends a greeting
+ rpc SayHello (HelloRequest) returns (HelloReply);
+}
+
+// The request message containing the user's name.
+message HelloRequest {
+ string name = 1;
+}
+
+// The response message containing the greetings.
+message HelloReply {
+ string message = 1;
+}
diff --git a/samples/Grpc/GrpcGreeter/Services/GreeterService.cs b/samples/Grpc/GrpcGreeter/Services/GreeterService.cs
new file mode 100644
index 00000000..59061912
--- /dev/null
+++ b/samples/Grpc/GrpcGreeter/Services/GreeterService.cs
@@ -0,0 +1,22 @@
+using Grpc.Core;
+using GrpcGreeter;
+
+namespace GrpcGreeter.Services
+{
+ public class GreeterService : Greeter.GreeterBase
+ {
+ private readonly ILogger _logger;
+ public GreeterService(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public override Task SayHello(HelloRequest request, ServerCallContext context)
+ {
+ return Task.FromResult(new HelloReply
+ {
+ Message = "Hello " + request.Name
+ });
+ }
+ }
+}
diff --git a/samples/Grpc/GrpcGreeter/Startup.cs b/samples/Grpc/GrpcGreeter/Startup.cs
new file mode 100644
index 00000000..4d4196e8
--- /dev/null
+++ b/samples/Grpc/GrpcGreeter/Startup.cs
@@ -0,0 +1,37 @@
+using GrpcGreeter.Services;
+
+namespace GrpcGreeter;
+
+public class Startup
+{
+ public IConfiguration Configuration { get; }
+
+ public Startup(IConfiguration configuration)
+ {
+ this.Configuration = configuration;
+ }
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ // Add services to the container.
+ services.AddGrpc();
+ services.AddGrpcReflection();
+
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ app.UseRouting();
+ app.UseEndpoints(b =>
+ {
+ // Configure the HTTP request pipeline.
+ b.MapGrpcService();
+ b.MapGet("/",
+ () =>
+ "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
+ b.MapGrpcReflectionService();
+ });
+ }
+}
diff --git a/samples/Grpc/GrpcGreeter/appsettings.Development.json b/samples/Grpc/GrpcGreeter/appsettings.Development.json
new file mode 100644
index 00000000..0c208ae9
--- /dev/null
+++ b/samples/Grpc/GrpcGreeter/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/samples/Grpc/GrpcGreeter/appsettings.json b/samples/Grpc/GrpcGreeter/appsettings.json
new file mode 100644
index 00000000..1aef5074
--- /dev/null
+++ b/samples/Grpc/GrpcGreeter/appsettings.json
@@ -0,0 +1,14 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "Kestrel": {
+ "EndpointDefaults": {
+ "Protocols": "Http2"
+ }
+ }
+}
diff --git a/samples/Grpc/GrpcGreeterClient.Tests/GrpcGreeterClient.Tests.csproj b/samples/Grpc/GrpcGreeterClient.Tests/GrpcGreeterClient.Tests.csproj
new file mode 100644
index 00000000..8b74b7ad
--- /dev/null
+++ b/samples/Grpc/GrpcGreeterClient.Tests/GrpcGreeterClient.Tests.csproj
@@ -0,0 +1,24 @@
+
+
+ net8.0
+ false
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Grpc/GrpcGreeterClient.Tests/GrpcGreeterClientTest.cs b/samples/Grpc/GrpcGreeterClient.Tests/GrpcGreeterClientTest.cs
new file mode 100644
index 00000000..2119a061
--- /dev/null
+++ b/samples/Grpc/GrpcGreeterClient.Tests/GrpcGreeterClientTest.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Text.Json;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Xunit;
+using System.IO;
+using System.Text.Json.Serialization;
+using PactNet;
+using PactNet.Exceptions;
+using PactNet.Extensions.Grpc;
+using PactNet.Output.Xunit;
+using Xunit.Abstractions;
+
+namespace GrpcGreeterClient.Tests
+{
+ public class GrpcGreeterClientTests : IDisposable
+ {
+ private readonly IGrpcPactBuilderV4 pact;
+
+ public GrpcGreeterClientTests(ITestOutputHelper output)
+ {
+ var config = new PactConfig
+ {
+ PactDir = "../../../../pacts/",
+ Outputters = new[]
+ {
+ new XunitOutput(output)
+ },
+ DefaultJsonSettings = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ PropertyNameCaseInsensitive = true,
+ Converters = { new JsonStringEnumConverter() }
+ },
+ LogLevel = PactLogLevel.Information
+ };
+
+ this.pact = Pact.V4("grpc-greeter-client", "grpc-greeter", config).WithGrpcInteractions();
+ }
+
+ [Fact]
+ public void ThrowsExceptionWhenNoGrpcClientRequestMade()
+ {
+ string protoFilePath = Path.Join(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "GrpcGreeterClient", "Protos", "greet.proto");
+ this.pact
+ .UponReceiving("A greeting request to say hello.")
+ .WithRequest(protoFilePath, nameof(Greeter), "SayHello",
+ new { name = "matching(equalTo, 'foo')" })
+ .WillRespond()
+ .WithBody(new { message = "matching(equalTo, 'Hello foo')" });
+
+ Assert.Throws(() =>
+ this.pact.Verify(_ =>
+ {
+ // No grpc call here results in failure.
+ }));
+ }
+
+ [Fact]
+ public async Task WritesPactForGreeterSayHelloRequest()
+ {
+ // Arrange
+ string protoFilePath = Path.Join(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "GrpcGreeterClient", "Protos", "greet.proto");
+ this.pact
+ .UponReceiving("A greeting request to say hello.")
+ .WithRequest(protoFilePath, nameof(Greeter), "SayHello",
+ new { name = "matching(equalTo, 'foo')" })
+ .WillRespond()
+ .WithBody(new { message = "matching(equalTo, 'Hello foo')" });
+
+ await this.pact.VerifyAsync(async ctx =>
+ {
+
+ // Arrange
+ var client = new GreeterClientWrapper(ctx.MockServerUri.AbsoluteUri);
+
+ // Act
+ var greeting = await client.SayHello("foo");
+
+ // Assert
+ greeting.Should().Be("Hello foo");
+ });
+ }
+
+ public void Dispose() => this.pact?.Dispose();
+ }
+}
diff --git a/samples/Grpc/GrpcGreeterClient/GrpcGreeterClient.csproj b/samples/Grpc/GrpcGreeterClient/GrpcGreeterClient.csproj
new file mode 100644
index 00000000..94331d73
--- /dev/null
+++ b/samples/Grpc/GrpcGreeterClient/GrpcGreeterClient.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
diff --git a/samples/Grpc/GrpcGreeterClient/Program.cs b/samples/Grpc/GrpcGreeterClient/Program.cs
new file mode 100644
index 00000000..7beb6292
--- /dev/null
+++ b/samples/Grpc/GrpcGreeterClient/Program.cs
@@ -0,0 +1,32 @@
+using Grpc.Net.Client;
+using GrpcGreeterClient;
+
+public class GreeterClientWrapper
+{
+ private readonly Greeter.GreeterClient _client;
+
+ public GreeterClientWrapper(string url)
+ {
+ var channel = GrpcChannel.ForAddress(url);
+ _client = new Greeter.GreeterClient(channel);
+ }
+
+ public async Task SayHello(string name)
+ {
+ var reply = await _client.SayHelloAsync(new HelloRequest { Name = name });
+ return reply.Message;
+ }
+}
+
+public class Program
+{
+ public static async Task Main(string[] args)
+ {
+ var client = new GreeterClientWrapper("http://localhost:5099");
+ // var client = new GreeterClientWrapper("https://localhost:5099");
+ var greeting = await client.SayHello("GreeterClient");
+ Console.WriteLine("Greeting: " + greeting);
+ Console.WriteLine("Press any key to exit...");
+ Console.ReadKey();
+ }
+}
diff --git a/samples/Grpc/GrpcGreeterClient/Protos/greet.proto b/samples/Grpc/GrpcGreeterClient/Protos/greet.proto
new file mode 100644
index 00000000..1a015952
--- /dev/null
+++ b/samples/Grpc/GrpcGreeterClient/Protos/greet.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+option csharp_namespace = "GrpcGreeterClient";
+
+package greet;
+
+// The greeting service definition.
+service Greeter {
+ // Sends a greeting
+ rpc SayHello (HelloRequest) returns (HelloReply);
+}
+
+// The request message containing the user's name.
+message HelloRequest {
+ string name = 1;
+}
+
+// The response message containing the greetings.
+message HelloReply {
+ string message = 1;
+}
diff --git a/samples/Grpc/pacts/grpc-greeter-client-grpc-greeter.json b/samples/Grpc/pacts/grpc-greeter-client-grpc-greeter.json
new file mode 100644
index 00000000..c2318c69
--- /dev/null
+++ b/samples/Grpc/pacts/grpc-greeter-client-grpc-greeter.json
@@ -0,0 +1,96 @@
+{
+ "consumer": {
+ "name": "grpc-greeter-client"
+ },
+ "interactions": [
+ {
+ "description": "A greeting request to say hello.",
+ "interactionMarkup": {
+ "markup": "```protobuf\nmessage HelloReply {\n string message = 1;\n}\n```\n",
+ "markupType": "COMMON_MARK"
+ },
+ "pending": false,
+ "pluginConfiguration": {
+ "protobuf": {
+ "descriptorKey": "e8e1fe144f808b9b0faecd7b2605efea",
+ "service": "Greeter/SayHello"
+ }
+ },
+ "request": {
+ "contents": {
+ "content": "CgNmb28=",
+ "contentType": "application/protobuf;message=HelloRequest",
+ "contentTypeHint": "BINARY",
+ "encoded": "base64"
+ },
+ "matchingRules": {
+ "body": {
+ "$.name": {
+ "combine": "AND",
+ "matchers": [
+ {
+ "match": "equality"
+ }
+ ]
+ }
+ }
+ },
+ "metadata": {
+ "contentType": "application/protobuf;message=HelloRequest"
+ }
+ },
+ "response": [
+ {
+ "contents": {
+ "content": "CglIZWxsbyBmb28=",
+ "contentType": "application/protobuf;message=HelloReply",
+ "contentTypeHint": "BINARY",
+ "encoded": "base64"
+ },
+ "matchingRules": {
+ "body": {
+ "$.message": {
+ "combine": "AND",
+ "matchers": [
+ {
+ "match": "equality"
+ }
+ ]
+ }
+ }
+ },
+ "metadata": {
+ "contentType": "application/protobuf;message=HelloReply"
+ }
+ }
+ ],
+ "transport": "grpc",
+ "type": "Synchronous/Messages"
+ }
+ ],
+ "metadata": {
+ "pactRust": {
+ "ffi": "0.4.27",
+ "mockserver": "1.2.11",
+ "models": "1.2.8"
+ },
+ "pactSpecification": {
+ "version": "4.0"
+ },
+ "plugins": [
+ {
+ "configuration": {
+ "e8e1fe144f808b9b0faecd7b2605efea": {
+ "protoDescriptors": "Cr0BCgtncmVldC5wcm90bxIFZ3JlZXQiIgoMSGVsbG9SZXF1ZXN0EhIKBG5hbWUYASABKAlSBG5hbWUiJgoKSGVsbG9SZXBseRIYCgdtZXNzYWdlGAEgASgJUgdtZXNzYWdlMj0KB0dyZWV0ZXISMgoIU2F5SGVsbG8SEy5ncmVldC5IZWxsb1JlcXVlc3QaES5ncmVldC5IZWxsb1JlcGx5QhSqAhFHcnBjR3JlZXRlckNsaWVudGIGcHJvdG8z",
+ "protoFile": "syntax = \"proto3\";\r\n\r\noption csharp_namespace = \"GrpcGreeterClient\";\r\n\r\npackage greet;\r\n\r\n// The greeting service definition.\r\nservice Greeter {\r\n // Sends a greeting\r\n rpc SayHello (HelloRequest) returns (HelloReply);\r\n}\r\n\r\n// The request message containing the user's name.\r\nmessage HelloRequest {\r\n string name = 1;\r\n}\r\n\r\n// The response message containing the greetings.\r\nmessage HelloReply {\r\n string message = 1;\r\n}\r\n"
+ }
+ },
+ "name": "protobuf",
+ "version": "0.4.0"
+ }
+ ]
+ },
+ "provider": {
+ "name": "grpc-greeter"
+ }
+}
\ No newline at end of file
diff --git a/samples/OrdersApi/Consumer.Tests/pacts/Fulfilment API-Orders API.json b/samples/OrdersApi/Consumer.Tests/pacts/Fulfilment API-Orders API.json
index b6f1a702..2a79a1b1 100644
--- a/samples/OrdersApi/Consumer.Tests/pacts/Fulfilment API-Orders API.json
+++ b/samples/OrdersApi/Consumer.Tests/pacts/Fulfilment API-Orders API.json
@@ -69,6 +69,7 @@
},
"status": 200
},
+ "transport": "http",
"type": "Synchronous/HTTP"
},
{
@@ -86,6 +87,7 @@
"response": {
"status": 404
},
+ "transport": "http",
"type": "Synchronous/HTTP"
},
{
@@ -129,6 +131,7 @@
"response": {
"status": 204
},
+ "transport": "http",
"type": "Synchronous/HTTP"
},
{
@@ -159,6 +162,7 @@
"metadata": {
"pactRust": {
"ffi": "0.4.27",
+ "mockserver": "1.2.11",
"models": "1.2.8"
},
"pactSpecification": {
diff --git a/samples/OrdersApi/Provider.Tests/ProviderTests.cs b/samples/OrdersApi/Provider.Tests/ProviderTests.cs
index 1feb302e..e8c6e8ec 100644
--- a/samples/OrdersApi/Provider.Tests/ProviderTests.cs
+++ b/samples/OrdersApi/Provider.Tests/ProviderTests.cs
@@ -16,7 +16,7 @@ namespace Provider.Tests
{
public class ProviderTests : IDisposable
{
- private static readonly Uri ProviderUri = new("http://localhost:5000");
+ private static readonly Uri ProviderUri = new("http://localhost:65098");
private static readonly JsonSerializerOptions Options = new()
{
@@ -38,7 +38,7 @@ public ProviderTests(ITestOutputHelper output)
.Build();
this.server.Start();
-
+
this.verifier = new PactVerifier("Orders API", new PactVerifierConfig
{
LogLevel = PactLogLevel.Debug,
diff --git a/src/PactNet.Abstractions/Drivers/ICompletedPactDriver.cs b/src/PactNet.Abstractions/Drivers/ICompletedPactDriver.cs
new file mode 100644
index 00000000..b62db319
--- /dev/null
+++ b/src/PactNet.Abstractions/Drivers/ICompletedPactDriver.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace PactNet.Drivers
+{
+ ///
+ /// Driver for writing completed pact files containing interactions
+ ///
+ public interface ICompletedPactDriver
+ {
+ ///
+ /// Write the pact file to disk
+ ///
+ /// Directory of the pact file
+ /// Status code
+ /// Failed to write pact file
+ void WritePactFile(string directory);
+
+ ///
+ /// Write the pact file to disk
+ ///
+ /// Port of the mock server
+ /// Directory of the pact file
+ void WritePactFile(int port, string directory);
+
+ ///
+ /// Create the mock server for the current pact
+ ///
+ /// Host for the mock server
+ /// Port for the mock server, or null to allocate one automatically
+ /// Enable TLS
+ /// The transport to use (i.e. http, https, grpc). Must be a valid UTF-8 NULL-terminated string, or NULL or empty, in which case http will be used.
+ /// Mock server port
+ /// Failed to start mock server
+ IMockServerDriver CreateMockServer(string host, int? port, bool tls, string transport = null);
+ }
+}
diff --git a/src/PactNet/Drivers/IMockServerDriver.cs b/src/PactNet.Abstractions/Drivers/IMockServerDriver.cs
similarity index 81%
rename from src/PactNet/Drivers/IMockServerDriver.cs
rename to src/PactNet.Abstractions/Drivers/IMockServerDriver.cs
index 8060613e..8d8cadfd 100644
--- a/src/PactNet/Drivers/IMockServerDriver.cs
+++ b/src/PactNet.Abstractions/Drivers/IMockServerDriver.cs
@@ -5,8 +5,13 @@ namespace PactNet.Drivers
///
/// Driver for managing a HTTP mock server
///
- internal interface IMockServerDriver : IDisposable
+ public interface IMockServerDriver : IDisposable
{
+ ///
+ /// Mock server port
+ ///
+ int Port { get; }
+
///
/// Mock server URI
///
diff --git a/src/PactNet.Abstractions/ISynchronousPluginPactBuilder.cs b/src/PactNet.Abstractions/ISynchronousPluginPactBuilder.cs
new file mode 100644
index 00000000..6b5a9a5e
--- /dev/null
+++ b/src/PactNet.Abstractions/ISynchronousPluginPactBuilder.cs
@@ -0,0 +1,19 @@
+using System;
+using PactNet.Drivers;
+
+namespace PactNet;
+
+public interface ISynchronousPluginPactBuilderV4 : IPactBuilder, IDisposable
+{
+ ///
+ /// Add a new interaction to the pact
+ ///
+ /// Interaction description
+ /// Fluent builder
+ ISynchronousPluginRequestBuilderV4 UponReceiving(string description);
+
+ ///
+ /// Driver for writing completed pact files containing interactions
+ ///
+ ICompletedPactDriver CompletedPactDriver { get; }
+}
diff --git a/src/PactNet.Abstractions/ISynchronousPluginRequestBuilder.cs b/src/PactNet.Abstractions/ISynchronousPluginRequestBuilder.cs
new file mode 100644
index 00000000..eefe5b51
--- /dev/null
+++ b/src/PactNet.Abstractions/ISynchronousPluginRequestBuilder.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+
+namespace PactNet;
+
+public interface ISynchronousPluginRequestBuilderV4
+{
+ ///
+ /// Add a provider state
+ ///
+ /// Provider state description
+ /// Fluent builder
+ ISynchronousPluginRequestBuilderV4 Given(string description);
+
+ ///
+ /// Add a provider state with a parameter to the interaction
+ ///
+ /// Provider state description
+ /// Parameter name
+ /// Parameter value
+ ISynchronousPluginRequestBuilderV4 Given(string description, string name, string value);
+
+ ///
+ /// Add plugin interaction content
+ ///
+ /// Content type
+ /// A dictionary containing the plugin content.
+ ISynchronousPluginRequestBuilderV4 WithContent(string contentType, Dictionary content);
+}
diff --git a/src/PactNet.Extensions.Grpc/AssemblyInfo.cs b/src/PactNet.Extensions.Grpc/AssemblyInfo.cs
new file mode 100644
index 00000000..038dfc74
--- /dev/null
+++ b/src/PactNet.Extensions.Grpc/AssemblyInfo.cs
@@ -0,0 +1,2 @@
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("PactNet.Extensions.Grpc.Tests")]
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/src/PactNet.Extensions.Grpc/GrpcExtensions.cs b/src/PactNet.Extensions.Grpc/GrpcExtensions.cs
new file mode 100644
index 00000000..28334d12
--- /dev/null
+++ b/src/PactNet.Extensions.Grpc/GrpcExtensions.cs
@@ -0,0 +1,24 @@
+using PactNet.Models;
+
+
+namespace PactNet.Extensions.Grpc;
+
+///
+/// Grpc extensions for Pact V4
+///
+public static class GrpcExtensions
+{
+ ///
+ /// Add asynchronous message (i.e. consumer/producer) interactions to the pact
+ ///
+ /// Pact details
+ /// Port for the mock server. If null, one will be assigned automatically
+ /// Host for the mock server
+ /// Pact builder
+ public static IGrpcPactBuilderV4 WithGrpcInteractions(this IPactV4 pact, int? port = null, IPAddress host = IPAddress.Loopback)
+ {
+ var pluginBuilder = pact.WithSynchronousPluginInteractions("protobuf", "0.4.0", transport: "grpc", port, host);
+ var builder = new GrpcPactBuilder(pluginBuilder, pact.Config, port, host);
+ return builder;
+ }
+}
diff --git a/src/PactNet.Extensions.Grpc/GrpcPactBuilder.cs b/src/PactNet.Extensions.Grpc/GrpcPactBuilder.cs
new file mode 100644
index 00000000..72936ada
--- /dev/null
+++ b/src/PactNet.Extensions.Grpc/GrpcPactBuilder.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Threading.Tasks;
+using PactNet.Models;
+
+namespace PactNet.Extensions.Grpc;
+
+
+///
+/// Grpc pact v4 builder
+///
+public interface IGrpcPactBuilderV4: IPactBuilder, IDisposable
+{
+ ///
+ /// Add a new interaction to the pact
+ ///
+ /// Interaction description
+ /// Fluent builder
+ IGrpcRequestBuilderV4 UponReceiving(string description);
+}
+
+internal class GrpcPactBuilder : AbstractPactBuilder, IGrpcPactBuilderV4
+{
+ private readonly ISynchronousPluginPactBuilderV4 pactBuilder;
+ private ISynchronousPluginRequestBuilderV4 synchronousPluginRequestBuilder;
+ private GrpcRequestBuilder requestBuilder;
+ private bool interactionInitialized;
+
+ ///
+ /// Initialises a new instance of the class.
+ ///
+ internal GrpcPactBuilder(ISynchronousPluginPactBuilderV4 pactBuilder,
+ PactConfig config, int? port, IPAddress host) : base(pactBuilder.CompletedPactDriver, config, port, host, "grpc")
+ {
+ this.pactBuilder = pactBuilder;
+ }
+
+ ///
+ /// Create a new request/response interaction
+ ///
+ /// Interaction description
+ /// Fluent builder
+ public IGrpcRequestBuilderV4 UponReceiving(string description)
+ {
+ if (interactionInitialized)
+ {
+ throw new InvalidOperationException("An interaction has already been initialized for this pact.");
+ }
+
+ interactionInitialized = true;
+ synchronousPluginRequestBuilder = pactBuilder.UponReceiving(description);
+ requestBuilder = new GrpcRequestBuilder(synchronousPluginRequestBuilder);
+ return requestBuilder;
+ }
+
+ ///
+ ///
+ ///
+ public override void Verify(Action interact)
+ {
+ if (!interactionInitialized)
+ {
+ throw new InvalidOperationException("No pact has been initialized.");
+ }
+
+ this.synchronousPluginRequestBuilder.WithContent("application/grpc", requestBuilder.InteractionContents);
+ base.Verify(interact);
+ }
+
+ ///
+ ///
+ ///
+ public override async Task VerifyAsync(Func interact)
+ {
+ if (!interactionInitialized)
+ {
+ throw new InvalidOperationException("No pact has been initialized.");
+ }
+
+ this.synchronousPluginRequestBuilder.WithContent("application/grpc", requestBuilder.InteractionContents);
+ await base.VerifyAsync(interact);
+ }
+
+ public void Dispose() => this.pactBuilder.Dispose();
+}
diff --git a/src/PactNet.Extensions.Grpc/GrpcRequestBuilder.cs b/src/PactNet.Extensions.Grpc/GrpcRequestBuilder.cs
new file mode 100644
index 00000000..e39ca5ec
--- /dev/null
+++ b/src/PactNet.Extensions.Grpc/GrpcRequestBuilder.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+
+namespace PactNet.Extensions.Grpc;
+
+///
+/// Grpc request builder
+///
+public interface IGrpcRequestBuilderV4
+{
+ ///
+ /// Add a provider state
+ ///
+ /// Provider state description
+ /// Fluent builder
+ IGrpcRequestBuilderV4 Given(string providerState);
+
+ ///
+ /// Add a provider state with a parameter to the interaction
+ ///
+ /// Provider state description
+ /// Parameter name
+ /// Parameter value
+ /// Fluent builder
+ IGrpcRequestBuilderV4 Given(string description, string name, string value);
+
+ ///
+ /// Define the response to this request
+ ///
+ /// Response builder
+ IGrpcResponseBuilderV4 WillRespond();
+
+ ///
+ /// Configure grpc request
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ IGrpcRequestBuilderV4 WithRequest(string protoFilePath, string serviceName, string methodName, dynamic body);
+}
+
+internal class GrpcRequestBuilder(ISynchronousPluginRequestBuilderV4 requestBuilder) : IGrpcRequestBuilderV4
+{
+ private const string PactProtoKey = "pact:proto";
+ private const string PactProtoServiceKey = "pact:proto-service";
+ private const string RequestKey = "request";
+ private const string PactContentType = "pact:content-type";
+ private bool requestConfigured;
+
+ internal readonly Dictionary InteractionContents = new();
+
+ ///
+ ///
+ ///
+ public IGrpcRequestBuilderV4 Given(string providerState)
+ {
+ requestBuilder.Given(providerState);
+ return this;
+ }
+
+ ///
+ ///
+ ///
+ public IGrpcRequestBuilderV4 Given(string description, string name, string value)
+ {
+ requestBuilder.Given(description, name, value);
+ return this;
+ }
+
+ ///
+ /// Configure grpc request
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IGrpcRequestBuilderV4 WithRequest(string protoFilePath, string serviceName, string methodName, dynamic body)
+ {
+ if (this.requestConfigured)
+ {
+ throw new InvalidOperationException("Request has already been configured.");
+ }
+
+ this.InteractionContents.Add(PactProtoKey, protoFilePath);
+ this.InteractionContents.Add(PactProtoServiceKey, $"{serviceName}/{methodName}");
+ this.InteractionContents.Add(PactContentType, "application/protobuf");
+ this.InteractionContents.Add(RequestKey, body);
+ this.requestConfigured = true;
+ return this;
+ }
+
+ ///
+ /// Define the response to this request
+ ///
+ /// Response builder
+ public IGrpcResponseBuilderV4 WillRespond()
+ {
+ if (!this.requestConfigured)
+ {
+ throw new InvalidOperationException("You must configure the request before defining the response");
+ }
+
+ var builder = new GrpcResponseBuilder(this.InteractionContents);
+ return builder;
+ }
+}
diff --git a/src/PactNet.Extensions.Grpc/GrpcResponseBuilder.cs b/src/PactNet.Extensions.Grpc/GrpcResponseBuilder.cs
new file mode 100644
index 00000000..4d8ad0bf
--- /dev/null
+++ b/src/PactNet.Extensions.Grpc/GrpcResponseBuilder.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+
+namespace PactNet.Extensions.Grpc;
+
+///
+/// Grpc response builder.
+///
+public interface IGrpcResponseBuilderV4
+{
+ ///
+ /// Defines the response body as a dynamic object to be serialized as json.
+ ///
+ /// Response body
+ void WithBody(dynamic body);
+}
+
+internal class GrpcResponseBuilder(
+ Dictionary interactionContents) : IGrpcResponseBuilderV4
+{
+
+ public void WithBody(dynamic body)
+ {
+ interactionContents.Add("response", body);
+ }
+}
diff --git a/src/PactNet.Extensions.Grpc/PactNet.Extensions.Grpc.csproj b/src/PactNet.Extensions.Grpc/PactNet.Extensions.Grpc.csproj
new file mode 100644
index 00000000..8c289b58
--- /dev/null
+++ b/src/PactNet.Extensions.Grpc/PactNet.Extensions.Grpc.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.0
+ latest
+ true
+ Library
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PactNet/AbstractPactBuilder.cs b/src/PactNet/AbstractPactBuilder.cs
new file mode 100644
index 00000000..45aa3be2
--- /dev/null
+++ b/src/PactNet/AbstractPactBuilder.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Threading.Tasks;
+using PactNet.Drivers;
+using PactNet.Exceptions;
+using PactNet.Internal;
+using PactNet.Models;
+
+namespace PactNet;
+
+///
+/// Abstract pact builder that contains shared functionality of different types of pact interactions.
+///
+public abstract class AbstractPactBuilder : IPactBuilder
+{
+ private readonly ICompletedPactDriver pact;
+ private readonly PactConfig config;
+ private readonly int? port;
+ private readonly IPAddress host;
+ private readonly string transport;
+
+ ///
+ /// Initialises a new instance of the class.
+ ///
+ /// Pact driver
+ /// Pact config
+ /// Optional port, otherwise one is dynamically allocated
+ /// Optional host, otherwise loopback is used
+ /// The transport to use (i.e. http, https, grpc). Must be a valid UTF-8 NULL-terminated string, or NULL or empty, in which case http will be used.
+ protected AbstractPactBuilder(ICompletedPactDriver pact, PactConfig config, int? port, IPAddress host,
+ string transport = null)
+ {
+ this.pact = pact;
+ this.config = config;
+ this.port = port;
+ this.host = host;
+ this.transport = transport;
+ }
+
+ ///
+ /// Verify the configured interactions
+ ///
+ /// Action to perform the real interactions against the mock driver
+ /// Failed to verify the interactions
+ public virtual void Verify(Action interact)
+ {
+ Guard.NotNull(interact, nameof(interact));
+
+ using IMockServerDriver mockServer = this.StartMockServer();
+
+ try
+ {
+ interact(new ConsumerContext { MockServerUri = mockServer.Uri });
+
+ this.VerifyInternal(mockServer);
+ }
+ finally
+ {
+ this.PrintLogs(mockServer);
+ }
+ }
+
+ ///
+ /// Verify the configured interactions
+ ///
+ /// Action to perform the real interactions against the mock driver
+ /// Failed to verify the interactions
+ public virtual async Task VerifyAsync(Func interact)
+ {
+ Guard.NotNull(interact, nameof(interact));
+
+ using IMockServerDriver mockServer = this.StartMockServer();
+
+ try
+ {
+ await interact(new ConsumerContext { MockServerUri = mockServer.Uri });
+
+ this.VerifyInternal(mockServer);
+ }
+ finally
+ {
+ this.PrintLogs(mockServer);
+ }
+ }
+
+ ///
+ /// Start the mock driver
+ ///
+ /// Mock driver
+ private IMockServerDriver StartMockServer()
+ {
+ string hostIp = this.host switch
+ {
+ IPAddress.Loopback => "127.0.0.1",
+ IPAddress.Any => "0.0.0.0",
+ _ => throw new ArgumentOutOfRangeException(nameof(this.host), this.host, "Unsupported IPAddress value")
+ };
+
+ // TODO: add TLS support
+ return this.pact.CreateMockServer(hostIp, this.port, false, transport);
+ }
+
+ ///
+ /// Verify the interactions after the consumer client has been invoked
+ ///
+ /// Mock server
+ private void VerifyInternal(IMockServerDriver mockServer)
+ {
+ string errors = mockServer.MockServerMismatches();
+
+ if (string.IsNullOrWhiteSpace(errors) || errors == "[]")
+ {
+ this.pact.WritePactFile(mockServer.Port, this.config.PactDir);
+ return;
+ }
+
+ this.config.WriteLine(string.Empty);
+ this.config.WriteLine("Verification mismatches:");
+ this.config.WriteLine(string.Empty);
+ this.config.WriteLine(errors);
+
+ throw new PactFailureException("Pact verification failed. See output for details");
+ }
+
+ ///
+ /// Print logs to the configured outputs
+ ///
+ /// Mock server
+ private void PrintLogs(IMockServerDriver mockServer)
+ {
+ string logs = mockServer.MockServerLogs();
+
+ this.config.WriteLine("Mock driver logs:");
+ this.config.WriteLine(string.Empty);
+ this.config.WriteLine(logs);
+ }
+}
diff --git a/src/PactNet/ConfiguredMessageVerifier.cs b/src/PactNet/ConfiguredMessageVerifier.cs
index 45f125d2..46b758f3 100644
--- a/src/PactNet/ConfiguredMessageVerifier.cs
+++ b/src/PactNet/ConfiguredMessageVerifier.cs
@@ -1,7 +1,7 @@
using System;
using System.Text.Json;
using System.Threading.Tasks;
-using PactNet.Drivers;
+using PactNet.Drivers.Message;
using PactNet.Exceptions;
using PactNet.Interop;
using PactNet.Models;
diff --git a/src/PactNet/Drivers/AbstractPactDriver.cs b/src/PactNet/Drivers/AbstractPactDriver.cs
index 8e70fc3c..060d8177 100644
--- a/src/PactNet/Drivers/AbstractPactDriver.cs
+++ b/src/PactNet/Drivers/AbstractPactDriver.cs
@@ -8,7 +8,7 @@ namespace PactNet.Drivers
///
internal abstract class AbstractPactDriver : ICompletedPactDriver
{
- private readonly PactHandle pact;
+ protected readonly PactHandle pact;
///
/// Initialises a new instance of the class.
@@ -28,7 +28,22 @@ protected AbstractPactDriver(PactHandle pact)
public void WritePactFile(string directory)
{
var result = NativeInterop.WritePactFile(this.pact, directory, false);
+ ThrowExceptionOnWritePactFileFailure(result);
+ }
+
+ ///
+ /// Write the pact file to disk
+ ///
+ /// Port of the mock server
+ /// Directory of the pact file
+ public void WritePactFile(int port, string directory)
+ {
+ var result = NativeInterop.WritePactFileForPort(port, directory, false);
+ ThrowExceptionOnWritePactFileFailure(result);
+ }
+ private static void ThrowExceptionOnWritePactFileFailure(int result)
+ {
if (result != 0)
{
throw result switch
@@ -40,5 +55,34 @@ public void WritePactFile(string directory)
};
}
}
+
+ ///
+ /// Create the mock server for the current pact
+ ///
+ /// Host for the mock server
+ /// Port for the mock server, or null to allocate one automatically
+ /// Enable TLS
+ /// The transport to use (i.e. http, https, grpc). Must be a valid UTF-8 NULL-terminated string, or NULL or empty, in which case http will be used.
+ /// Mock server port
+ /// Failed to start mock server
+ public IMockServerDriver CreateMockServer(string host, int? port, bool tls, string transport = null)
+ {
+ int result = NativeInterop.CreateMockServerForTransport(this.pact, host, (ushort)port.GetValueOrDefault(0), transport, null);
+
+ if (result > 0)
+ {
+ return new MockServerDriver(host, result, tls);
+ }
+
+ throw result switch
+ {
+ -1 => new InvalidOperationException("Invalid handle when starting mock server"),
+ -3 => new InvalidOperationException("Unable to start mock server"),
+ -4 => new InvalidOperationException("The pact reference library panicked"),
+ -5 => new InvalidOperationException("The IPAddress is invalid"),
+ -6 => new InvalidOperationException("Could not create the TLS configuration with the self-signed certificate"),
+ _ => new InvalidOperationException($"Unknown mock server error: {result}")
+ };
+ }
}
}
diff --git a/src/PactNet/Drivers/HttpInteractionDriver.cs b/src/PactNet/Drivers/Http/HttpInteractionDriver.cs
similarity index 94%
rename from src/PactNet/Drivers/HttpInteractionDriver.cs
rename to src/PactNet/Drivers/Http/HttpInteractionDriver.cs
index a91d6b4f..6072ca6d 100644
--- a/src/PactNet/Drivers/HttpInteractionDriver.cs
+++ b/src/PactNet/Drivers/Http/HttpInteractionDriver.cs
@@ -1,24 +1,21 @@
using System;
using PactNet.Interop;
-namespace PactNet.Drivers
+namespace PactNet.Drivers.Http
{
///
/// Driver for synchronous HTTP interactions
///
internal class HttpInteractionDriver : IHttpInteractionDriver
{
- private readonly PactHandle pact;
private readonly InteractionHandle interaction;
///
/// Initialises a new instance of the class.
///
- /// Pact handle
/// Interaction handle
- internal HttpInteractionDriver(PactHandle pact, InteractionHandle interaction)
+ internal HttpInteractionDriver(InteractionHandle interaction)
{
- this.pact = pact;
this.interaction = interaction;
}
diff --git a/src/PactNet/Drivers/Http/HttpPactDriver.cs b/src/PactNet/Drivers/Http/HttpPactDriver.cs
new file mode 100644
index 00000000..7c8395ff
--- /dev/null
+++ b/src/PactNet/Drivers/Http/HttpPactDriver.cs
@@ -0,0 +1,30 @@
+using System;
+using PactNet.Interop;
+
+namespace PactNet.Drivers.Http
+{
+ ///
+ /// Driver for synchronous HTTP pacts
+ ///
+ internal class HttpPactDriver : AbstractPactDriver, IHttpPactDriver
+ {
+ ///
+ /// Initialises a new instance of the class.
+ ///
+ /// Pact handle
+ internal HttpPactDriver(PactHandle pact) : base(pact)
+ {
+ }
+
+ ///
+ /// Create a new interaction on the current pact
+ ///
+ /// Interaction description
+ /// HTTP interaction handle
+ public IHttpInteractionDriver NewHttpInteraction(string description)
+ {
+ InteractionHandle interaction = NativeInterop.NewInteraction(this.pact, description);
+ return new HttpInteractionDriver(interaction);
+ }
+ }
+}
diff --git a/src/PactNet/Drivers/IHttpInteractionDriver.cs b/src/PactNet/Drivers/Http/IHttpInteractionDriver.cs
similarity index 98%
rename from src/PactNet/Drivers/IHttpInteractionDriver.cs
rename to src/PactNet/Drivers/Http/IHttpInteractionDriver.cs
index ccb1473e..653ce2fc 100644
--- a/src/PactNet/Drivers/IHttpInteractionDriver.cs
+++ b/src/PactNet/Drivers/Http/IHttpInteractionDriver.cs
@@ -1,4 +1,4 @@
-namespace PactNet.Drivers
+namespace PactNet.Drivers.Http
{
///
/// Driver for synchronous HTTP interactions
diff --git a/src/PactNet/Drivers/Http/IHttpPactDriver.cs b/src/PactNet/Drivers/Http/IHttpPactDriver.cs
new file mode 100644
index 00000000..0194f34b
--- /dev/null
+++ b/src/PactNet/Drivers/Http/IHttpPactDriver.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace PactNet.Drivers.Http
+{
+ ///
+ /// Driver for synchronous HTTP pacts
+ ///
+ internal interface IHttpPactDriver : ICompletedPactDriver
+ {
+ ///
+ /// Create a new interaction on the current pact
+ ///
+ /// Interaction description
+ /// HTTP interaction handle
+ IHttpInteractionDriver NewHttpInteraction(string description);
+ }
+}
diff --git a/src/PactNet/Drivers/HttpPactDriver.cs b/src/PactNet/Drivers/HttpPactDriver.cs
deleted file mode 100644
index 52026bff..00000000
--- a/src/PactNet/Drivers/HttpPactDriver.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System;
-using PactNet.Interop;
-
-namespace PactNet.Drivers
-{
- ///
- /// Driver for synchronous HTTP pacts
- ///
- internal class HttpPactDriver : AbstractPactDriver, IHttpPactDriver
- {
- private readonly PactHandle pact;
-
- ///
- /// Initialises a new instance of the class.
- ///
- /// Pact handle
- internal HttpPactDriver(PactHandle pact) : base(pact)
- {
- this.pact = pact;
- }
-
- ///
- /// Create a new interaction on the current pact
- ///
- /// Interaction description
- /// HTTP interaction handle
- public IHttpInteractionDriver NewHttpInteraction(string description)
- {
- InteractionHandle interaction = NativeInterop.NewInteraction(this.pact, description);
- return new HttpInteractionDriver(this.pact, interaction);
- }
-
- ///
- /// Create the mock server for the current pact
- ///
- /// Host for the mock server
- /// Port for the mock server, or null to allocate one automatically
- /// Enable TLS
- /// Mock server port
- /// Failed to start mock server
- public IMockServerDriver CreateMockServer(string host, int? port, bool tls)
- {
- int result = NativeInterop.CreateMockServerForTransport(this.pact, host, (ushort)port.GetValueOrDefault(0), "http", null);
-
- if (result > 0)
- {
- return new MockServerDriver(host, result, tls);
- }
-
- throw result switch
- {
- -1 => new InvalidOperationException("Invalid handle when starting mock server"),
- -3 => new InvalidOperationException("Unable to start mock server"),
- -4 => new InvalidOperationException("The pact reference library panicked"),
- -5 => new InvalidOperationException("The IPAddress is invalid"),
- -6 => new InvalidOperationException("Could not create the TLS configuration with the self-signed certificate"),
- _ => new InvalidOperationException($"Unknown mock server error: {result}")
- };
- }
- }
-}
diff --git a/src/PactNet/Drivers/ICompletedPactDriver.cs b/src/PactNet/Drivers/ICompletedPactDriver.cs
deleted file mode 100644
index 199b075d..00000000
--- a/src/PactNet/Drivers/ICompletedPactDriver.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System;
-
-namespace PactNet.Drivers
-{
- ///
- /// Driver for writing completed pact files containing interactions
- ///
- internal interface ICompletedPactDriver
- {
- ///
- /// Write the pact file to disk
- ///
- /// Directory of the pact file
- /// Status code
- /// Failed to write pact file
- void WritePactFile(string directory);
- }
-}
diff --git a/src/PactNet/Drivers/IHttpPactDriver.cs b/src/PactNet/Drivers/IHttpPactDriver.cs
deleted file mode 100644
index 8cb815ff..00000000
--- a/src/PactNet/Drivers/IHttpPactDriver.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System;
-
-namespace PactNet.Drivers
-{
- ///
- /// Driver for synchronous HTTP pacts
- ///
- internal interface IHttpPactDriver : ICompletedPactDriver
- {
- ///
- /// Create a new interaction on the current pact
- ///
- /// Interaction description
- /// HTTP interaction handle
- IHttpInteractionDriver NewHttpInteraction(string description);
-
- ///
- /// Create the mock server for the current pact
- ///
- /// Host for the mock server
- /// Port for the mock server, or null to allocate one automatically
- /// Enable TLS
- /// Mock server port
- /// Failed to start mock server
- IMockServerDriver CreateMockServer(string host, int? port, bool tls);
- }
-}
diff --git a/src/PactNet/Drivers/IPactDriver.cs b/src/PactNet/Drivers/IPactDriver.cs
index 449aac9a..a3eaa1f7 100644
--- a/src/PactNet/Drivers/IPactDriver.cs
+++ b/src/PactNet/Drivers/IPactDriver.cs
@@ -1,9 +1,12 @@
-using PactNet.Interop;
+using PactNet.Drivers.Http;
+using PactNet.Drivers.Message;
+using PactNet.Drivers.Plugins;
+using PactNet.Interop;
namespace PactNet.Drivers
{
///
- /// Driver for creating a new pact and
+ /// Driver for creating a new pact and
///
internal interface IPactDriver
{
@@ -30,5 +33,16 @@ internal interface IPactDriver
///
/// Logs
string DriverLogs();
+
+ ///
+ /// Create a new plugin pact
+ ///
+ /// Consumer name
+ /// Provider name
+ /// Plugin name
+ /// Plugin version
+ /// Specification version
+ /// Plugin pact driver
+ IPluginPactDriver NewPluginPact(string consumerName, string providerName, string pluginName, string pluginVersion, PactSpecification version);
}
}
diff --git a/src/PactNet/Drivers/IMessageInteractionDriver.cs b/src/PactNet/Drivers/Message/IMessageInteractionDriver.cs
similarity index 97%
rename from src/PactNet/Drivers/IMessageInteractionDriver.cs
rename to src/PactNet/Drivers/Message/IMessageInteractionDriver.cs
index 964b8377..ec3ec38f 100644
--- a/src/PactNet/Drivers/IMessageInteractionDriver.cs
+++ b/src/PactNet/Drivers/Message/IMessageInteractionDriver.cs
@@ -1,4 +1,4 @@
-namespace PactNet.Drivers
+namespace PactNet.Drivers.Message
{
///
/// Driver for asynchronous message interactions
diff --git a/src/PactNet/Drivers/IMessagePactDriver.cs b/src/PactNet/Drivers/Message/IMessagePactDriver.cs
similarity index 95%
rename from src/PactNet/Drivers/IMessagePactDriver.cs
rename to src/PactNet/Drivers/Message/IMessagePactDriver.cs
index 4999226a..a5c724c8 100644
--- a/src/PactNet/Drivers/IMessagePactDriver.cs
+++ b/src/PactNet/Drivers/Message/IMessagePactDriver.cs
@@ -1,4 +1,4 @@
-namespace PactNet.Drivers
+namespace PactNet.Drivers.Message
{
///
/// Driver for message pacts
diff --git a/src/PactNet/Drivers/MessageInteractionDriver.cs b/src/PactNet/Drivers/Message/MessageInteractionDriver.cs
similarity index 98%
rename from src/PactNet/Drivers/MessageInteractionDriver.cs
rename to src/PactNet/Drivers/Message/MessageInteractionDriver.cs
index 035f8f4c..58ee7128 100644
--- a/src/PactNet/Drivers/MessageInteractionDriver.cs
+++ b/src/PactNet/Drivers/Message/MessageInteractionDriver.cs
@@ -2,7 +2,7 @@
using System.Runtime.InteropServices;
using PactNet.Interop;
-namespace PactNet.Drivers
+namespace PactNet.Drivers.Message
{
///
/// Driver for asynchronous message interactions
diff --git a/src/PactNet/Drivers/MessagePactDriver.cs b/src/PactNet/Drivers/Message/MessagePactDriver.cs
similarity index 93%
rename from src/PactNet/Drivers/MessagePactDriver.cs
rename to src/PactNet/Drivers/Message/MessagePactDriver.cs
index 11b99c39..bca25125 100644
--- a/src/PactNet/Drivers/MessagePactDriver.cs
+++ b/src/PactNet/Drivers/Message/MessagePactDriver.cs
@@ -1,21 +1,18 @@
using PactNet.Interop;
-namespace PactNet.Drivers
+namespace PactNet.Drivers.Message
{
///
/// Driver for message pacts
///
internal class MessagePactDriver : AbstractPactDriver, IMessagePactDriver
{
- private readonly PactHandle pact;
-
///
/// Initialises a new instance of the class.
///
/// Pact handle
internal MessagePactDriver(PactHandle pact) : base(pact)
{
- this.pact = pact;
}
///
diff --git a/src/PactNet/Drivers/MockServerDriver.cs b/src/PactNet/Drivers/MockServerDriver.cs
index bc92485f..f146404d 100644
--- a/src/PactNet/Drivers/MockServerDriver.cs
+++ b/src/PactNet/Drivers/MockServerDriver.cs
@@ -9,7 +9,12 @@ namespace PactNet.Drivers
///
internal class MockServerDriver : IMockServerDriver
{
- private readonly int port;
+
+ ///
+ /// Mock server port
+ ///
+ public int Port { get; }
+
///
/// Mock server URI
@@ -26,7 +31,7 @@ internal MockServerDriver(string host, int port, bool tls)
{
string scheme = tls ? "https" : "http";
this.Uri = new Uri($"{scheme}://{host}:{port}");
- this.port = port;
+ this.Port = port;
}
///
@@ -35,7 +40,7 @@ internal MockServerDriver(string host, int port, bool tls)
/// Mismatch string
public string MockServerMismatches()
{
- IntPtr matchesPtr = NativeInterop.MockServerMismatches(this.port);
+ IntPtr matchesPtr = NativeInterop.MockServerMismatches(this.Port);
return matchesPtr == IntPtr.Zero
? string.Empty
@@ -48,12 +53,9 @@ public string MockServerMismatches()
/// Log string
public string MockServerLogs()
{
- IntPtr logsPtr = NativeInterop.MockServerLogs(this.port);
-
- return logsPtr == IntPtr.Zero
- ? "ERROR: Unable to retrieve mock server logs"
- : Marshal.PtrToStringAnsi(logsPtr);
+ return NativeInterop.FetchLogBuffer(null);
}
+
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
@@ -76,7 +78,7 @@ public void Dispose()
///
private void ReleaseUnmanagedResources()
{
- NativeInterop.CleanupMockServer(this.port);
+ NativeInterop.CleanupMockServer(this.Port);
}
}
}
diff --git a/src/PactNet/Drivers/PactDriver.cs b/src/PactNet/Drivers/PactDriver.cs
index bce35279..8cb8624c 100644
--- a/src/PactNet/Drivers/PactDriver.cs
+++ b/src/PactNet/Drivers/PactDriver.cs
@@ -1,4 +1,8 @@
-using PactNet.Interop;
+using PactNet.Drivers.Http;
+using PactNet.Drivers.Message;
+using PactNet.Drivers.Plugins;
+using PactNet.Exceptions;
+using PactNet.Interop;
namespace PactNet.Drivers
{
@@ -16,9 +20,7 @@ internal class PactDriver : IPactDriver
/// HTTP pact driver
public IHttpPactDriver NewHttpPact(string consumerName, string providerName, PactSpecification version)
{
- PactHandle pact = NativeInterop.NewPact(consumerName, providerName);
- NativeInterop.WithSpecification(pact, version).CheckInteropSuccess();
-
+ PactHandle pact = CreatePactHandle(consumerName, providerName, version);
return new HttpPactDriver(pact);
}
@@ -28,13 +30,55 @@ public IHttpPactDriver NewHttpPact(string consumerName, string providerName, Pac
/// Consumer name
/// Provider name
/// Specification version
- /// Message pact driver driver
+ /// Message pact driver
public IMessagePactDriver NewMessagePact(string consumerName, string providerName, PactSpecification version)
+ {
+ PactHandle pact = CreatePactHandle(consumerName, providerName, version);
+ return new MessagePactDriver(pact);
+ }
+
+ ///
+ /// Create a new plugin pact
+ ///
+ /// Consumer name
+ /// Provider name
+ /// Plugin name
+ /// Plugin version
+ /// Specification version
+ /// Plugin pact driver
+ public IPluginPactDriver NewPluginPact(string consumerName, string providerName, string pluginName, string pluginVersion, PactSpecification version)
{
PactHandle pact = NativeInterop.NewPact(consumerName, providerName);
NativeInterop.WithSpecification(pact, version).CheckInteropSuccess();
- return new MessagePactDriver(pact);
+ uint code = NativeInterop.UsingPlugin(pact, pluginName, pluginVersion);
+
+ if (code == 0)
+ {
+ return new PluginPactDriver(pact);
+ }
+
+ throw code switch
+ {
+ 1 => new PactFailureException("Unable to setup the plugin - general panic"),
+ 2 => new PactFailureException("Unable to setup the plugin - invalid plugin name or version"),
+ 3 => new PactFailureException("Unable to setup the plugin - invalid pact handle"),
+ _ => new PactFailureException($"Unable to setup the plugin - unknown error {code}")
+ };
+ }
+
+ ///
+ /// Create a new pact handle and set the specification version
+ ///
+ /// Consumer name
+ /// Provider name
+ /// Specification version
+ /// Pact handle
+ private static PactHandle CreatePactHandle(string consumerName, string providerName, PactSpecification version)
+ {
+ PactHandle pact = NativeInterop.NewPact(consumerName, providerName);
+ NativeInterop.WithSpecification(pact, version).CheckInteropSuccess();
+ return pact;
}
///
diff --git a/src/PactNet/Drivers/Plugins/IPluginInteractionDriver.cs b/src/PactNet/Drivers/Plugins/IPluginInteractionDriver.cs
new file mode 100644
index 00000000..7b6fa41f
--- /dev/null
+++ b/src/PactNet/Drivers/Plugins/IPluginInteractionDriver.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace PactNet.Drivers.Plugins
+{
+ ///
+ /// Driver for plugin interactions
+ ///
+ internal interface IPluginInteractionDriver : IProviderStateDriver
+ {
+ ///
+ /// Add a plugin interaction content
+ ///
+ /// Content type
+ /// A dictionary containing the plugin content.
+ void WithContent(string contentType, Dictionary content);
+ }
+}
diff --git a/src/PactNet/Drivers/Plugins/IPluginPactDriver.cs b/src/PactNet/Drivers/Plugins/IPluginPactDriver.cs
new file mode 100644
index 00000000..09c9b395
--- /dev/null
+++ b/src/PactNet/Drivers/Plugins/IPluginPactDriver.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace PactNet.Drivers.Plugins
+{
+ ///
+ /// Driver for plugin-based pacts
+ ///
+ internal interface IPluginPactDriver : ICompletedPactDriver, IDisposable
+ {
+ ///
+ /// Create a new sync interaction on the current pact
+ ///
+ /// Interaction description
+ /// Interaction driver
+ IPluginInteractionDriver NewSyncInteraction(string description);
+ }
+}
diff --git a/src/PactNet/Drivers/Plugins/PluginInteractionDriver.cs b/src/PactNet/Drivers/Plugins/PluginInteractionDriver.cs
new file mode 100644
index 00000000..e2cf360b
--- /dev/null
+++ b/src/PactNet/Drivers/Plugins/PluginInteractionDriver.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using PactNet.Exceptions;
+using PactNet.Interop;
+
+namespace PactNet.Drivers.Plugins
+{
+ ///
+ /// Driver for plugin interactions
+ ///
+ internal class PluginInteractionDriver : IPluginInteractionDriver
+ {
+ private readonly InteractionHandle interaction;
+
+ ///
+ /// Initialises a new instance of the class.
+ ///
+ /// Interaction handle
+ public PluginInteractionDriver(InteractionHandle interaction)
+ {
+ this.interaction = interaction;
+ }
+
+ ///
+ /// Add a provider state to the interaction
+ ///
+ /// Provider state description
+ public void Given(string description)
+ => NativeInterop.Given(this.interaction, description).CheckInteropSuccess();
+
+ ///
+ /// Add a provider state with a parameter to the interaction
+ ///
+ /// Provider state description
+ /// Parameter name
+ /// Parameter value
+ public void GivenWithParam(string description, string name, string value)
+ => NativeInterop.GivenWithParam(this.interaction, description, name, value).CheckInteropSuccess();
+
+ ///
+ /// Add plugin interaction content
+ ///
+ /// Content type
+ /// A dictionary containing the plugin content.
+ public void WithContent(string contentType, Dictionary content)
+ {
+ uint code = NativeInterop.InteractionContents(this.interaction, InteractionPart.Request, contentType, JsonSerializer.Serialize(content));
+
+ if (code != 0)
+ {
+ throw code switch
+ {
+ 1 => new PactFailureException("A general panic was caught"),
+ 2 => new PactFailureException("The mock server has already been started"),
+ 3 => new PactFailureException("The interaction handle is invalid"),
+ 4 => new PactFailureException("The content type is not valid"),
+ 5 => new PactFailureException("The contents JSON is not valid JSON"),
+ 6 => new PactFailureException("The plugin returned an error"),
+ _ => new PactFailureException($"An unknown error occurred when setting the interaction contents: {code}"),
+ };
+ }
+ }
+ }
+}
diff --git a/src/PactNet/Drivers/Plugins/PluginPactDriver.cs b/src/PactNet/Drivers/Plugins/PluginPactDriver.cs
new file mode 100644
index 00000000..45c28f1f
--- /dev/null
+++ b/src/PactNet/Drivers/Plugins/PluginPactDriver.cs
@@ -0,0 +1,56 @@
+using System;
+using PactNet.Interop;
+
+namespace PactNet.Drivers.Plugins
+{
+ ///
+ /// Driver for plugin-based pacts
+ ///
+ internal class PluginPactDriver : AbstractPactDriver, IPluginPactDriver, IDisposable
+ {
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ /// Pact handle
+ internal PluginPactDriver(PactHandle pact) : base(pact)
+ {
+ }
+
+ ///
+ /// Create a new sync interaction on the current pact
+ ///
+ /// Interaction description
+ /// Interaction driver
+ public IPluginInteractionDriver NewSyncInteraction(string description)
+ {
+ InteractionHandle interaction = NativeInterop.NewSyncMessageInteraction(this.pact, description);
+ return new PluginInteractionDriver(interaction);
+ }
+
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ this.ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Allows an object to try to free resources and perform other cleanup operations before it is reclaimed by garbage collection.
+ ///
+ ~PluginPactDriver()
+ {
+ this.ReleaseUnmanagedResources();
+ }
+
+ ///
+ /// Release unmanaged resources
+ ///
+ private void ReleaseUnmanagedResources()
+ {
+ NativeInterop.CleanupPlugins(pact);
+ }
+ }
+}
diff --git a/src/PactNet/Interop/LogLevelExtensions.cs b/src/PactNet/Interop/LogLevelExtensions.cs
new file mode 100644
index 00000000..acc77f38
--- /dev/null
+++ b/src/PactNet/Interop/LogLevelExtensions.cs
@@ -0,0 +1,42 @@
+using System;
+
+namespace PactNet.Interop;
+
+///
+/// PactLogLevel extension methods
+///
+internal static class LogLevelExtensions
+{
+ private static readonly object LogLocker = new object();
+ private static bool LogInitialised = false;
+
+ ///
+ /// Direct all logging in the native library to a task local memory buffer.
+ ///
+ /// Log level
+ /// Invalid log level
+ /// Logging can only be initialised **once**. Subsequent calls will have no effect
+ public static void LogToBuffer(this PactLogLevel level)
+ {
+ lock (LogLocker)
+ {
+ if (LogInitialised)
+ {
+ return;
+ }
+
+ NativeInterop.LogToBuffer(level switch
+ {
+ PactLogLevel.Trace => LevelFilter.Trace,
+ PactLogLevel.Debug => LevelFilter.Debug,
+ PactLogLevel.Information => LevelFilter.Info,
+ PactLogLevel.Warn => LevelFilter.Warn,
+ PactLogLevel.Error => LevelFilter.Error,
+ PactLogLevel.None => LevelFilter.Off,
+ _ => throw new ArgumentOutOfRangeException(nameof(level), level, "Invalid log level")
+ });
+
+ LogInitialised = true;
+ }
+ }
+}
diff --git a/src/PactNet/Interop/NativeInterop.cs b/src/PactNet/Interop/NativeInterop.cs
index 92bdc911..803b530a 100644
--- a/src/PactNet/Interop/NativeInterop.cs
+++ b/src/PactNet/Interop/NativeInterop.cs
@@ -21,15 +21,15 @@ internal static class NativeInterop
[DllImport(DllName, EntryPoint = "pactffi_mock_server_mismatches")]
public static extern IntPtr MockServerMismatches(int mockServerPort);
- [DllImport(DllName, EntryPoint = "pactffi_mock_server_logs")]
- public static extern IntPtr MockServerLogs(int mockServerPort);
-
[DllImport(DllName, EntryPoint = "pactffi_cleanup_mock_server")]
public static extern bool CleanupMockServer(int mockServerPort);
[DllImport(DllName, EntryPoint = "pactffi_pact_handle_write_file")]
public static extern int WritePactFile(PactHandle pact, string directory, bool overwrite);
+ [DllImport(DllName, EntryPoint = "pactffi_write_pact_file")]
+ public static extern int WritePactFileForPort(int port, string directory, bool overwrite);
+
[DllImport(DllName, EntryPoint = "pactffi_fetch_log_buffer")]
public static extern string FetchLogBuffer(string logId);
@@ -42,6 +42,9 @@ internal static class NativeInterop
[DllImport(DllName, EntryPoint = "pactffi_new_interaction")]
public static extern InteractionHandle NewInteraction(PactHandle pact, string description);
+ [DllImport(DllName, EntryPoint = "pactffi_new_sync_message_interaction")]
+ public static extern InteractionHandle NewSyncMessageInteraction(PactHandle pact, string description);
+
[DllImport(DllName, EntryPoint = "pactffi_given")]
public static extern bool Given(InteractionHandle interaction, string description);
@@ -168,5 +171,18 @@ public static extern void VerifierBrokerSourceWithSelectors(IntPtr handle,
public static extern IntPtr VerifierOutput(IntPtr handle, byte stripAnsi);
#endregion
+
+ #region Plugins
+
+ [DllImport(DllName, EntryPoint = "pactffi_using_plugin")]
+ public static extern uint UsingPlugin(PactHandle pact, string name, string version);
+
+ [DllImport(DllName, EntryPoint = "pactffi_interaction_contents")]
+ public static extern uint InteractionContents(InteractionHandle interaction, InteractionPart part, string contentType, string body);
+
+ [DllImport(DllName, EntryPoint = "pactffi_cleanup_plugins")]
+ public static extern void CleanupPlugins(PactHandle pact);
+
+ #endregion
}
}
diff --git a/src/PactNet/MessageBuilder.cs b/src/PactNet/MessageBuilder.cs
index 83c7cffe..571a9847 100644
--- a/src/PactNet/MessageBuilder.cs
+++ b/src/PactNet/MessageBuilder.cs
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
-using PactNet.Drivers;
+using PactNet.Drivers.Message;
using PactNet.Interop;
namespace PactNet
diff --git a/src/PactNet/MessagePactBuilder.cs b/src/PactNet/MessagePactBuilder.cs
index 0cd17567..c70e2349 100644
--- a/src/PactNet/MessagePactBuilder.cs
+++ b/src/PactNet/MessagePactBuilder.cs
@@ -1,5 +1,5 @@
using System;
-using PactNet.Drivers;
+using PactNet.Drivers.Message;
using PactNet.Interop;
namespace PactNet
diff --git a/src/PactNet/PactBuilder.cs b/src/PactNet/PactBuilder.cs
index fe311c88..aea4b1aa 100644
--- a/src/PactNet/PactBuilder.cs
+++ b/src/PactNet/PactBuilder.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Threading.Tasks;
-using PactNet.Drivers;
-using PactNet.Exceptions;
-using PactNet.Internal;
+using PactNet.Drivers.Http;
using PactNet.Models;
namespace PactNet
@@ -10,12 +6,10 @@ namespace PactNet
///
/// Pact builder for the native backend
///
- internal class PactBuilder : IPactBuilderV2, IPactBuilderV3, IPactBuilderV4
+ internal class PactBuilder : AbstractPactBuilder, IPactBuilderV2, IPactBuilderV3, IPactBuilderV4
{
private readonly IHttpPactDriver pact;
private readonly PactConfig config;
- private readonly int? port;
- private readonly IPAddress host;
///
/// Initialises a new instance of the class.
@@ -24,12 +18,11 @@ internal class PactBuilder : IPactBuilderV2, IPactBuilderV3, IPactBuilderV4
/// Pact config
/// Optional port, otherwise one is dynamically allocated
/// Optional host, otherwise loopback is used
- internal PactBuilder(IHttpPactDriver pact, PactConfig config, int? port = null, IPAddress host = IPAddress.Loopback)
+ internal PactBuilder(IHttpPactDriver pact, PactConfig config, int? port = null,
+ IPAddress host = IPAddress.Loopback) : base(pact, config, port, host, "http")
{
this.pact = pact;
this.config = config;
- this.port = port;
- this.host = host;
}
///
@@ -68,109 +61,5 @@ internal RequestBuilder UponReceiving(string description)
var requestBuilder = new RequestBuilder(interactions, this.config.DefaultJsonSettings);
return requestBuilder;
}
-
- ///
- /// Verify the configured interactions
- ///
- /// Action to perform the real interactions against the mock driver
- /// Failed to verify the interactions
- public void Verify(Action interact)
- {
- Guard.NotNull(interact, nameof(interact));
-
- using IMockServerDriver mockServer = this.StartMockServer();
-
- try
- {
- interact(new ConsumerContext
- {
- MockServerUri = mockServer.Uri
- });
-
- this.VerifyInternal(mockServer);
- }
- finally
- {
- this.PrintLogs(mockServer);
- }
- }
-
- ///
- /// Verify the configured interactions
- ///
- /// Action to perform the real interactions against the mock driver
- /// Failed to verify the interactions
- public async Task VerifyAsync(Func interact)
- {
- Guard.NotNull(interact, nameof(interact));
-
- using IMockServerDriver mockServer = this.StartMockServer();
-
- try
- {
- await interact(new ConsumerContext
- {
- MockServerUri = mockServer.Uri
- });
-
- this.VerifyInternal(mockServer);
- }
- finally
- {
- this.PrintLogs(mockServer);
- }
- }
-
- ///
- /// Start the mock driver
- ///
- /// Mock driver
- private IMockServerDriver StartMockServer()
- {
- string hostIp = this.host switch
- {
- IPAddress.Loopback => "127.0.0.1",
- IPAddress.Any => "0.0.0.0",
- _ => throw new ArgumentOutOfRangeException(nameof(this.host), this.host, "Unsupported IPAddress value")
- };
-
- // TODO: add TLS support
- return this.pact.CreateMockServer(hostIp, this.port, false);
- }
-
- ///
- /// Verify the interactions after the consumer client has been invoked
- ///
- /// Mock server
- private void VerifyInternal(IMockServerDriver mockServer)
- {
- string errors = mockServer.MockServerMismatches();
-
- if (string.IsNullOrWhiteSpace(errors) || errors == "[]")
- {
- this.pact.WritePactFile(this.config.PactDir);
- return;
- }
-
- this.config.WriteLine(string.Empty);
- this.config.WriteLine("Verification mismatches:");
- this.config.WriteLine(string.Empty);
- this.config.WriteLine(errors);
-
- throw new PactFailureException("Pact verification failed. See output for details");
- }
-
- ///
- /// Print logs to the configured outputs
- ///
- /// Mock server
- private void PrintLogs(IMockServerDriver mockServer)
- {
- string logs = mockServer.MockServerLogs();
-
- this.config.WriteLine("Mock driver logs:");
- this.config.WriteLine(string.Empty);
- this.config.WriteLine(logs);
- }
}
}
diff --git a/src/PactNet/PactExtensions.cs b/src/PactNet/PactExtensions.cs
index 32aa358f..b1432baf 100644
--- a/src/PactNet/PactExtensions.cs
+++ b/src/PactNet/PactExtensions.cs
@@ -1,5 +1,7 @@
-using System;
using PactNet.Drivers;
+using PactNet.Drivers.Http;
+using PactNet.Drivers.Message;
+using PactNet.Drivers.Plugins;
using PactNet.Interop;
using PactNet.Models;
@@ -10,9 +12,6 @@ namespace PactNet
///
public static class PactExtensions
{
- private static readonly object LogLocker = new object();
- private static bool LogInitialised = false;
-
///
/// Establish a new pact using the native backend
///
@@ -25,9 +24,10 @@ public static class PactExtensions
/// It is advised that the port is not specified whenever possible to allow PactNet to allocate a port dynamically
/// and ensure there are no port clashes
///
- public static IPactBuilderV2 WithHttpInteractions(this IPactV2 pact, int? port = null, IPAddress host = IPAddress.Loopback)
+ public static IPactBuilderV2 WithHttpInteractions(this IPactV2 pact, int? port = null,
+ IPAddress host = IPAddress.Loopback)
{
- InitialiseLogging(pact.Config.LogLevel);
+ pact.Config.LogLevel.LogToBuffer();
IPactDriver driver = new PactDriver();
IHttpPactDriver httpPact = driver.NewHttpPact(pact.Consumer, pact.Provider, PactSpecification.V2);
@@ -48,9 +48,10 @@ public static IPactBuilderV2 WithHttpInteractions(this IPactV2 pact, int? port =
/// It is advised that the port is not specified whenever possible to allow PactNet to allocate a port dynamically
/// and ensure there are no port clashes
///
- public static IPactBuilderV3 WithHttpInteractions(this IPactV3 pact, int? port = null, IPAddress host = IPAddress.Loopback)
+ public static IPactBuilderV3 WithHttpInteractions(this IPactV3 pact, int? port = null,
+ IPAddress host = IPAddress.Loopback)
{
- InitialiseLogging(pact.Config.LogLevel);
+ pact.Config.LogLevel.LogToBuffer();
IPactDriver driver = new PactDriver();
IHttpPactDriver httpPact = driver.NewHttpPact(pact.Consumer, pact.Provider, PactSpecification.V3);
@@ -71,9 +72,10 @@ public static IPactBuilderV3 WithHttpInteractions(this IPactV3 pact, int? port =
/// It is advised that the port is not specified whenever possible to allow PactNet to allocate a port dynamically
/// and ensure there are no port clashes
///
- public static IPactBuilderV4 WithHttpInteractions(this IPactV4 pact, int? port = null, IPAddress host = IPAddress.Loopback)
+ public static IPactBuilderV4 WithHttpInteractions(this IPactV4 pact, int? port = null,
+ IPAddress host = IPAddress.Loopback)
{
- InitialiseLogging(pact.Config.LogLevel);
+ pact.Config.LogLevel.LogToBuffer();
IPactDriver driver = new PactDriver();
IHttpPactDriver httpPact = driver.NewHttpPact(pact.Consumer, pact.Provider, PactSpecification.V4);
@@ -89,7 +91,7 @@ public static IPactBuilderV4 WithHttpInteractions(this IPactV4 pact, int? port =
/// Pact builder
public static IMessagePactBuilderV3 WithMessageInteractions(this IPactV3 pact)
{
- InitialiseLogging(pact.Config.LogLevel);
+ pact.Config.LogLevel.LogToBuffer();
IPactDriver driver = new PactDriver();
IMessagePactDriver messagePact = driver.NewMessagePact(pact.Consumer, pact.Provider, PactSpecification.V3);
@@ -105,7 +107,7 @@ public static IMessagePactBuilderV3 WithMessageInteractions(this IPactV3 pact)
/// Pact builder
public static IMessagePactBuilderV4 WithMessageInteractions(this IPactV4 pact)
{
- InitialiseLogging(pact.Config.LogLevel);
+ pact.Config.LogLevel.LogToBuffer();
IPactDriver driver = new PactDriver();
IMessagePactDriver messagePact = driver.NewMessagePact(pact.Consumer, pact.Provider, PactSpecification.V4);
@@ -115,33 +117,24 @@ public static IMessagePactBuilderV4 WithMessageInteractions(this IPactV4 pact)
}
///
- /// Initialise logging in the native library
+ /// Establish a new pact with synchronous plugin interactions.
///
- /// Log level
- /// Invalid log level
- /// Logging can only be initialised **once**. Subsequent calls will have no effect
- private static void InitialiseLogging(PactLogLevel level)
+ ///
+ /// Plugin name
+ /// Plugin version
+ /// The transport to use (i.e. http, https, grpc). Must be a valid UTF-8 NULL-terminated string, or NULL or empty, in which case http will be used.
+ /// Port for the mock server. If null, one will be assigned automatically
+ /// Host for the mock server
+ ///
+ public static ISynchronousPluginPactBuilderV4 WithSynchronousPluginInteractions(this IPactV4 pact,
+ string pluginName, string pluginVersion, string transport = null, int? port = null,
+ IPAddress host = IPAddress.Loopback)
{
- lock (LogLocker)
- {
- if (LogInitialised)
- {
- return;
- }
-
- NativeInterop.LogToBuffer(level switch
- {
- PactLogLevel.Trace => LevelFilter.Trace,
- PactLogLevel.Debug => LevelFilter.Debug,
- PactLogLevel.Information => LevelFilter.Info,
- PactLogLevel.Warn => LevelFilter.Warn,
- PactLogLevel.Error => LevelFilter.Error,
- PactLogLevel.None => LevelFilter.Off,
- _ => throw new ArgumentOutOfRangeException(nameof(level), level, "Invalid log level")
- });
-
- LogInitialised = true;
- }
+ pact.Config.LogLevel.LogToBuffer();
+ IPactDriver driver = new PactDriver();
+ var pluginDriver = driver.NewPluginPact(pact.Consumer, pact.Provider, pluginName, pluginVersion,
+ PactSpecification.V4);
+ return new SynchronousPluginPactBuilder(pluginDriver, pact.Config, port, host, transport);
}
}
}
diff --git a/src/PactNet/RequestBuilder.cs b/src/PactNet/RequestBuilder.cs
index d245cfbf..20204fbc 100644
--- a/src/PactNet/RequestBuilder.cs
+++ b/src/PactNet/RequestBuilder.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
-using PactNet.Drivers;
+using PactNet.Drivers.Http;
using PactNet.Matchers;
namespace PactNet
diff --git a/src/PactNet/ResponseBuilder.cs b/src/PactNet/ResponseBuilder.cs
index badb0e96..21abfd22 100644
--- a/src/PactNet/ResponseBuilder.cs
+++ b/src/PactNet/ResponseBuilder.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
-using PactNet.Drivers;
+using PactNet.Drivers.Http;
using PactNet.Matchers;
namespace PactNet
diff --git a/src/PactNet/SynchronousPluginPactBuilder.cs b/src/PactNet/SynchronousPluginPactBuilder.cs
new file mode 100644
index 00000000..0c3bc031
--- /dev/null
+++ b/src/PactNet/SynchronousPluginPactBuilder.cs
@@ -0,0 +1,26 @@
+using PactNet.Drivers;
+using PactNet.Drivers.Plugins;
+using PactNet.Models;
+
+namespace PactNet;
+
+internal class SynchronousPluginPactBuilder(
+ IPluginPactDriver pact,
+ PactConfig config,
+ int? port,
+ IPAddress host,
+ string transport = null)
+ : AbstractPactBuilder(pact, config, port, host, transport), ISynchronousPluginPactBuilderV4
+{
+ public ISynchronousPluginRequestBuilderV4 UponReceiving(string description)
+ {
+ return new SynchronousPluginRequestBuilder(pact.NewSyncInteraction(description));
+ }
+
+ ///
+ /// Driver for writing completed pact files containing interactions
+ ///
+ public ICompletedPactDriver CompletedPactDriver { get { return pact; } }
+
+ public void Dispose() => pact?.Dispose();
+}
diff --git a/src/PactNet/SynchronousPluginRequestBuilder.cs b/src/PactNet/SynchronousPluginRequestBuilder.cs
new file mode 100644
index 00000000..d3da9c41
--- /dev/null
+++ b/src/PactNet/SynchronousPluginRequestBuilder.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using PactNet.Drivers.Plugins;
+
+namespace PactNet;
+
+internal class SynchronousPluginRequestBuilder(IPluginInteractionDriver interactionDriver)
+ : ISynchronousPluginRequestBuilderV4
+{
+
+ ///
+ /// Add a provider state
+ ///
+ /// Provider state description
+ /// Fluent builder
+ public ISynchronousPluginRequestBuilderV4 Given(string description)
+ {
+ interactionDriver.Given(description);
+ return this;
+ }
+
+ ///
+ /// Add a provider state with a parameter to the interaction
+ ///
+ /// Provider state description
+ /// Parameter name
+ /// Parameter value
+ public ISynchronousPluginRequestBuilderV4 Given(string description, string name, string value)
+ {
+ interactionDriver.GivenWithParam(description, name, value);
+ return this;
+ }
+
+ ///
+ /// Add plugin interaction content
+ ///
+ /// Content type
+ /// A dictionary containing the plugin content.
+ public ISynchronousPluginRequestBuilderV4 WithContent(string contentType, Dictionary content)
+ {
+ interactionDriver.WithContent(contentType, content);
+ return this;
+ }
+}
diff --git a/src/PactNet/Verifier/InteropVerifierProvider.cs b/src/PactNet/Verifier/InteropVerifierProvider.cs
index c0245296..8de20a67 100644
--- a/src/PactNet/Verifier/InteropVerifierProvider.cs
+++ b/src/PactNet/Verifier/InteropVerifierProvider.cs
@@ -31,17 +31,7 @@ public InteropVerifierProvider(PactVerifierConfig config)
///
public void Initialise()
{
- NativeInterop.LogToBuffer(config.LogLevel switch
- {
- PactLogLevel.Trace => LevelFilter.Trace,
- PactLogLevel.Debug => LevelFilter.Debug,
- PactLogLevel.Information => LevelFilter.Info,
- PactLogLevel.Warn => LevelFilter.Warn,
- PactLogLevel.Error => LevelFilter.Error,
- PactLogLevel.None => LevelFilter.Off,
- _ => throw new ArgumentOutOfRangeException(nameof(config.LogLevel), config.LogLevel, "Invalid log level")
- });
-
+ this.config.LogLevel.LogToBuffer();
this.handle = NativeInterop.VerifierNewForApplication("pact-net", typeof(InteropVerifierProvider).Assembly.GetName().Version.ToString());
}
diff --git a/tests/PactNet.Extensions.Grpc.Tests/GrpcRequestBuilderTests.cs b/tests/PactNet.Extensions.Grpc.Tests/GrpcRequestBuilderTests.cs
new file mode 100644
index 00000000..e76da1d0
--- /dev/null
+++ b/tests/PactNet.Extensions.Grpc.Tests/GrpcRequestBuilderTests.cs
@@ -0,0 +1,38 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using Moq;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace PactNet.Extensions.Grpc.Tests;
+
+public class GrpcRequestBuilderTests(ITestOutputHelper testOutputHelper)
+{
+ [Fact]
+ public void ConfiguresRequestAndResponse()
+ {
+ var builder = new GrpcRequestBuilder(new Mock().Object);
+ string protoFilePath = Path.Combine("..", "..", "..", "..", "GrpcGreeterClient", "Protos", "greet.proto");
+ string serviceName = "Greeter";
+ string methodName = "SayHello";
+ var content = $@"{{
+ ""pact:proto"":""{protoFilePath.Replace("\\", "\\\\")}"",
+ ""pact:proto-service"": ""{serviceName}/{methodName}"",
+ ""pact:content-type"": ""application/protobuf"",
+ ""request"": {{
+ ""name"": ""matching(type, 'foo')""
+ }},
+ ""response"": {{
+ ""message"": ""matching(type, 'Hello foo')""
+ }}
+ }}".Replace(Environment.NewLine, "").Replace("'", "\\u0027");
+
+
+ builder.WithRequest(protoFilePath, serviceName, methodName, new { name = "matching(type, 'foo')" })
+ .WillRespond().WithBody(new { message = "matching(type, 'Hello foo')" });
+ var actual = JsonSerializer.Serialize(builder.InteractionContents);
+ testOutputHelper.WriteLine(actual);
+ Assert.Equal(content, actual, ignoreAllWhiteSpace: true);
+ }
+}
diff --git a/tests/PactNet.Extensions.Grpc.Tests/PactNet.Extensions.Grpc.Tests.csproj b/tests/PactNet.Extensions.Grpc.Tests/PactNet.Extensions.Grpc.Tests.csproj
new file mode 100644
index 00000000..6ed58ebe
--- /dev/null
+++ b/tests/PactNet.Extensions.Grpc.Tests/PactNet.Extensions.Grpc.Tests.csproj
@@ -0,0 +1,45 @@
+
+
+
+
+ net8.0
+
+
+ net8.0;net462
+
+ false
+ latest
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/tests/PactNet.Tests/ConfiguredMessageVerifierTests.cs b/tests/PactNet.Tests/ConfiguredMessageVerifierTests.cs
index 6b9bb0a7..577b2a27 100644
--- a/tests/PactNet.Tests/ConfiguredMessageVerifierTests.cs
+++ b/tests/PactNet.Tests/ConfiguredMessageVerifierTests.cs
@@ -4,7 +4,7 @@
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
-using PactNet.Drivers;
+using PactNet.Drivers.Message;
using PactNet.Exceptions;
using PactNet.Interop;
using PactNet.Models;
diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs
index 81ab072e..e8a6098f 100644
--- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs
+++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs
@@ -6,6 +6,8 @@
using System.Threading.Tasks;
using FluentAssertions;
using PactNet.Drivers;
+using PactNet.Drivers.Http;
+using PactNet.Drivers.Message;
using PactNet.Interop;
using Xunit;
using Xunit.Abstractions;
@@ -22,8 +24,7 @@ public class FfiIntegrationTests
public FfiIntegrationTests(ITestOutputHelper output)
{
this.output = output;
-
- NativeInterop.LogToBuffer(LevelFilter.Trace);
+ PactLogLevel.Trace.LogToBuffer();
}
[Fact]
@@ -36,7 +37,6 @@ public async Task HttpInteraction_v3_CreatesPactFile()
IHttpPactDriver pact = driver.NewHttpPact("NativeDriverTests-Consumer-V3",
"NativeDriverTests-Provider",
PactSpecification.V3);
-
IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction");
interaction.Given("provider state");
@@ -52,7 +52,7 @@ public async Task HttpInteraction_v3_CreatesPactFile()
interaction.WithResponseHeader("X-Response-Header", "value2", 1);
interaction.WithResponseBody("application/json", @"{""foo"":42}");
- using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false);
+ using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false, "http");
var client = new HttpClient { BaseAddress = mockServer.Uri };
client.DefaultRequestHeaders.Add("X-Request-Header", new[] { "request1", "request2" });
diff --git a/tests/PactNet.Tests/MessageBuilderTests.cs b/tests/PactNet.Tests/MessageBuilderTests.cs
index 4fa72c5c..34a7e922 100644
--- a/tests/PactNet.Tests/MessageBuilderTests.cs
+++ b/tests/PactNet.Tests/MessageBuilderTests.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text.Json;
using Moq;
-using PactNet.Drivers;
+using PactNet.Drivers.Message;
using PactNet.Interop;
using Xunit;
using Match = PactNet.Matchers.Match;
diff --git a/tests/PactNet.Tests/MessagePactBuilderTests.cs b/tests/PactNet.Tests/MessagePactBuilderTests.cs
index cff2f18c..daee234d 100644
--- a/tests/PactNet.Tests/MessagePactBuilderTests.cs
+++ b/tests/PactNet.Tests/MessagePactBuilderTests.cs
@@ -1,7 +1,7 @@
using System;
using FluentAssertions;
using Moq;
-using PactNet.Drivers;
+using PactNet.Drivers.Message;
using PactNet.Interop;
using Xunit;
diff --git a/tests/PactNet.Tests/PactBuilderTests.cs b/tests/PactNet.Tests/PactBuilderTests.cs
index fd645304..a152bf72 100644
--- a/tests/PactNet.Tests/PactBuilderTests.cs
+++ b/tests/PactNet.Tests/PactBuilderTests.cs
@@ -3,6 +3,7 @@
using FluentAssertions;
using Moq;
using PactNet.Drivers;
+using PactNet.Drivers.Http;
using PactNet.Exceptions;
using PactNet.Infrastructure.Outputters;
using Xunit;
@@ -11,6 +12,7 @@ namespace PactNet.Tests
{
public class PactBuilderTests
{
+ private const int MockServerPort = 5000;
private readonly PactBuilder builder;
private readonly Mock mockDriver;
@@ -27,12 +29,13 @@ public PactBuilderTests()
this.mockDriver = new Mock(MockBehavior.Strict);
this.mockInteractions = new Mock(MockBehavior.Strict);
this.mockServer = new Mock(MockBehavior.Strict);
+ this.mockServer.Setup(s => s.Port).Returns(MockServerPort);
this.mockOutput = new Mock();
this.fixture = new Fixture();
var customization = new SupportMutableValueTypesCustomization();
customization.Customize(this.fixture);
-
+
this.serverUri = this.fixture.Create();
this.config = new PactConfig
{
@@ -40,9 +43,9 @@ public PactBuilderTests()
};
// set some default mock setups
- this.mockDriver.Setup(s => s.CreateMockServer("127.0.0.1", null, false)).Returns(this.mockServer.Object);
+ this.mockDriver.Setup(s => s.CreateMockServer("127.0.0.1", null, false, "http")).Returns(this.mockServer.Object);
this.mockDriver.Setup(s => s.NewHttpInteraction(It.IsAny())).Returns(this.mockInteractions.Object);
- this.mockDriver.Setup(s => s.WritePactFile(this.config.PactDir));
+ this.mockDriver.Setup(s => s.WritePactFile(MockServerPort, this.config.PactDir));
this.mockServer.Setup(s => s.Uri).Returns(this.serverUri);
this.mockServer.Setup(s => s.MockServerLogs()).Returns(string.Empty);
@@ -67,14 +70,14 @@ public void Verify_WhenCalled_StartsMockServer()
{
this.builder.Verify(Success);
- this.mockDriver.Verify(d => d.CreateMockServer("127.0.0.1", null, false));
+ this.mockDriver.Verify(d => d.CreateMockServer("127.0.0.1", null, false, "http"));
}
[Fact]
public void Verify_ErrorStartingMockServer_ThrowsInvalidOperationException()
{
this.mockDriver
- .Setup(s => s.CreateMockServer(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Setup(s => s.CreateMockServer(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Throws();
Action action = () => this.builder.Verify(Success);
@@ -106,7 +109,7 @@ public void Verify_NoMismatches_WritesPactFile()
{
this.builder.Verify(Success);
- this.mockDriver.Verify(s => s.WritePactFile(this.config.PactDir));
+ this.mockDriver.Verify(s => s.WritePactFile(MockServerPort, this.config.PactDir));
}
[Fact]
@@ -121,7 +124,7 @@ public void Verify_NoMismatches_ShutsDownMockServer()
public void Verify_FailedToWritePactFile_ThrowsInvalidOperationException()
{
this.mockDriver
- .Setup(s => s.WritePactFile(It.IsAny()))
+ .Setup(s => s.WritePactFile(It.IsAny(), It.IsAny()))
.Throws();
Action action = () => this.builder.Verify(Success);
diff --git a/tests/PactNet.Tests/RequestBuilderTests.cs b/tests/PactNet.Tests/RequestBuilderTests.cs
index d4be9624..a55a204b 100644
--- a/tests/PactNet.Tests/RequestBuilderTests.cs
+++ b/tests/PactNet.Tests/RequestBuilderTests.cs
@@ -4,7 +4,7 @@
using System.Text.Json;
using FluentAssertions;
using Moq;
-using PactNet.Drivers;
+using PactNet.Drivers.Http;
using Xunit;
using Match = PactNet.Matchers.Match;
diff --git a/tests/PactNet.Tests/ResponseBuilderTests.cs b/tests/PactNet.Tests/ResponseBuilderTests.cs
index d92cee71..cd71e771 100644
--- a/tests/PactNet.Tests/ResponseBuilderTests.cs
+++ b/tests/PactNet.Tests/ResponseBuilderTests.cs
@@ -1,7 +1,7 @@
using System.Net;
using System.Text.Json;
using Moq;
-using PactNet.Drivers;
+using PactNet.Drivers.Http;
using Xunit;
using Match = PactNet.Matchers.Match;
diff --git a/tests/PactNet.Tests/data/v2-consumer-integration.json b/tests/PactNet.Tests/data/v2-consumer-integration.json
index 9484eef4..7c3a5702 100644
--- a/tests/PactNet.Tests/data/v2-consumer-integration.json
+++ b/tests/PactNet.Tests/data/v2-consumer-integration.json
@@ -75,6 +75,7 @@
"metadata": {
"pactRust": {
"ffi": "0.4.27",
+ "mockserver": "1.2.11",
"models": "1.2.8"
},
"pactSpecification": {
diff --git a/tests/PactNet.Tests/data/v3-consumer-integration.json b/tests/PactNet.Tests/data/v3-consumer-integration.json
index b220eb49..269da5e6 100644
--- a/tests/PactNet.Tests/data/v3-consumer-integration.json
+++ b/tests/PactNet.Tests/data/v3-consumer-integration.json
@@ -131,6 +131,7 @@
"metadata": {
"pactRust": {
"ffi": "0.4.27",
+ "mockserver": "1.2.11",
"models": "1.2.8"
},
"pactSpecification": {
diff --git a/tests/PactNet.Tests/data/v4-combined-integration.json b/tests/PactNet.Tests/data/v4-combined-integration.json
index 61808511..1a3483b2 100644
--- a/tests/PactNet.Tests/data/v4-combined-integration.json
+++ b/tests/PactNet.Tests/data/v4-combined-integration.json
@@ -145,6 +145,7 @@
},
"status": 201
},
+ "transport": "http",
"type": "Synchronous/HTTP"
},
{
diff --git a/tests/PactNet.Tests/data/v4-consumer-integration.json b/tests/PactNet.Tests/data/v4-consumer-integration.json
index 87ef562b..5c1ee161 100644
--- a/tests/PactNet.Tests/data/v4-consumer-integration.json
+++ b/tests/PactNet.Tests/data/v4-consumer-integration.json
@@ -145,12 +145,14 @@
},
"status": 201
},
+ "transport": "http",
"type": "Synchronous/HTTP"
}
],
"metadata": {
"pactRust": {
"ffi": "0.4.27",
+ "mockserver": "1.2.11",
"models": "1.2.8"
},
"pactSpecification": {