diff --git a/PactNet.sln b/PactNet.sln index 243019fa..a6392782 100644 --- a/PactNet.sln +++ b/PactNet.sln @@ -37,6 +37,21 @@ 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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pacts", "pacts", "{FB3A8B7F-7DA0-40A8-AFD1-FB0992FB9B2C}" + ProjectSection(SolutionItems) = preProject + samples\Grpc\pacts\grpc-greeter-client-grpc-greeter.json = samples\Grpc\pacts\grpc-greeter-client-grpc-greeter.json + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -155,6 +170,54 @@ 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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -170,6 +233,12 @@ 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} + {FB3A8B7F-7DA0-40A8-AFD1-FB0992FB9B2C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} 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..e51c7d07 --- /dev/null +++ b/samples/Grpc/GrpcGreeterClient.Tests/GrpcGreeterClient.Tests.csproj @@ -0,0 +1,23 @@ + + + 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..8e3bdf72 --- /dev/null +++ b/samples/Grpc/GrpcGreeterClient.Tests/GrpcGreeterClientTest.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +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.Output.Xunit; +using Xunit.Abstractions; + +namespace GrpcGreeterClient.Tests +{ + public class GrpcGreeterClientTests : IDisposable + { + private readonly ISynchronousPluginPactBuilderV4 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) + .WithSynchronousPluginInteractions("protobuf", "0.4.0", transport: "grpc"); + } + + [Fact] + public void ThrowsExceptionWhenNoGrpcClientRequestMade() + { + string protoFilePath = Path.Join(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "GrpcGreeterClient", "Protos", "greet.proto"); + var content = new Dictionary + { + { + "pact:proto", protoFilePath + }, + { "pact:proto-service", "Greeter/SayHello" }, + { "pact:content-type", "application/protobuf" }, + { "request", new { name = "matching(equalTo, 'foo')" } }, + { "response", new { message = "matching(equalTo, 'Hello foo')" } } + }; + + this.pact.UponReceiving("A greeting request to say hello.").WithContent("application/grpc", content); + + Assert.Throws(() => + this.pact.Verify(_ => + { + // No grpc call here results in failure. + })); + } + + [Fact] + public async Task WritesPactForGreeterSayHelloRequest() + { + string protoFilePath = Path.Join(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "GrpcGreeterClient", "Protos", "greet.proto"); + var content = new Dictionary + { + { + "pact:proto", protoFilePath + }, + { "pact:proto-service", "Greeter/SayHello" }, + { "pact:content-type", "application/protobuf" }, + { "request", new { name = "matching(equalTo, 'foo')" } }, + { "response", new { message = "matching(equalTo, 'Hello foo')" } } + }; + + + this.pact.UponReceiving("A greeting request to say hello.").WithContent("application/grpc", content); + + 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/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/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/Http/HttpInteractionDriver.cs b/src/PactNet/Drivers/Http/HttpInteractionDriver.cs index fd6520a9..6072ca6d 100644 --- a/src/PactNet/Drivers/Http/HttpInteractionDriver.cs +++ b/src/PactNet/Drivers/Http/HttpInteractionDriver.cs @@ -8,17 +8,14 @@ namespace PactNet.Drivers.Http /// 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 index 19572cac..7c8395ff 100644 --- a/src/PactNet/Drivers/Http/HttpPactDriver.cs +++ b/src/PactNet/Drivers/Http/HttpPactDriver.cs @@ -8,15 +8,12 @@ namespace PactNet.Drivers.Http /// 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; } /// @@ -27,35 +24,7 @@ internal HttpPactDriver(PactHandle pact) : base(pact) 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}") - }; + return new HttpInteractionDriver(interaction); } } } diff --git a/src/PactNet/Drivers/Http/IHttpPactDriver.cs b/src/PactNet/Drivers/Http/IHttpPactDriver.cs index 38895d25..0194f34b 100644 --- a/src/PactNet/Drivers/Http/IHttpPactDriver.cs +++ b/src/PactNet/Drivers/Http/IHttpPactDriver.cs @@ -13,15 +13,5 @@ internal interface IHttpPactDriver : ICompletedPactDriver /// 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/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/IPactDriver.cs b/src/PactNet/Drivers/IPactDriver.cs index a787052b..a3eaa1f7 100644 --- a/src/PactNet/Drivers/IPactDriver.cs +++ b/src/PactNet/Drivers/IPactDriver.cs @@ -1,11 +1,12 @@ 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 { @@ -32,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/Message/MessagePactDriver.cs b/src/PactNet/Drivers/Message/MessagePactDriver.cs index 91a43a62..bca25125 100644 --- a/src/PactNet/Drivers/Message/MessagePactDriver.cs +++ b/src/PactNet/Drivers/Message/MessagePactDriver.cs @@ -7,15 +7,12 @@ namespace PactNet.Drivers.Message /// 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/Plugins/IPluginInteractionDriver.cs b/src/PactNet/Drivers/Plugins/IPluginInteractionDriver.cs index 9cc90aa6..7b6fa41f 100644 --- a/src/PactNet/Drivers/Plugins/IPluginInteractionDriver.cs +++ b/src/PactNet/Drivers/Plugins/IPluginInteractionDriver.cs @@ -1,4 +1,6 @@ -namespace PactNet.Drivers.Plugins +using System.Collections.Generic; + +namespace PactNet.Drivers.Plugins { /// /// Driver for plugin interactions @@ -9,7 +11,7 @@ internal interface IPluginInteractionDriver : IProviderStateDriver /// Add a plugin interaction content /// /// Content type - /// Content - void WithContent(string contentType, string content); + /// 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 index 1e635f5a..09c9b395 100644 --- a/src/PactNet/Drivers/Plugins/IPluginPactDriver.cs +++ b/src/PactNet/Drivers/Plugins/IPluginPactDriver.cs @@ -5,7 +5,7 @@ namespace PactNet.Drivers.Plugins /// /// Driver for plugin-based pacts /// - internal interface IPluginPactDriver : ICompletedPactDriver + internal interface IPluginPactDriver : ICompletedPactDriver, IDisposable { /// /// Create a new sync interaction on the current pact @@ -13,16 +13,5 @@ internal interface IPluginPactDriver : ICompletedPactDriver /// Interaction description /// Interaction driver IPluginInteractionDriver NewSyncInteraction(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 - /// Transport - e.g. http, https, grpc - /// Transport config string - /// Mock server port - /// Failed to start mock server - IMockServerDriver CreateMockServer(string host, int? port, string transport, string transportConfig); } } diff --git a/src/PactNet/Drivers/Plugins/PluginInteractionDriver.cs b/src/PactNet/Drivers/Plugins/PluginInteractionDriver.cs index e4969c47..e2cf360b 100644 --- a/src/PactNet/Drivers/Plugins/PluginInteractionDriver.cs +++ b/src/PactNet/Drivers/Plugins/PluginInteractionDriver.cs @@ -1,4 +1,6 @@ -using PactNet.Exceptions; +using System.Collections.Generic; +using System.Text.Json; +using PactNet.Exceptions; using PactNet.Interop; namespace PactNet.Drivers.Plugins @@ -8,17 +10,14 @@ namespace PactNet.Drivers.Plugins /// internal class PluginInteractionDriver : IPluginInteractionDriver { - private readonly PactHandle pact; private readonly InteractionHandle interaction; /// /// Initialises a new instance of the class. /// - /// Pact handle /// Interaction handle - public PluginInteractionDriver(PactHandle pact, InteractionHandle interaction) + public PluginInteractionDriver(InteractionHandle interaction) { - this.pact = pact; this.interaction = interaction; } @@ -39,13 +38,13 @@ public void GivenWithParam(string description, string name, string value) => NativeInterop.GivenWithParam(this.interaction, description, name, value).CheckInteropSuccess(); /// - /// Add a plugin interaction content + /// Add plugin interaction content /// /// Content type - /// Content - public void WithContent(string contentType, string content) + /// A dictionary containing the plugin content. + public void WithContent(string contentType, Dictionary content) { - uint code = NativeInterop.InteractionContents(this.interaction, InteractionPart.Request, contentType, content); + uint code = NativeInterop.InteractionContents(this.interaction, InteractionPart.Request, contentType, JsonSerializer.Serialize(content)); if (code != 0) { diff --git a/src/PactNet/Drivers/Plugins/PluginPactDriver.cs b/src/PactNet/Drivers/Plugins/PluginPactDriver.cs index bfa4cd9c..45c28f1f 100644 --- a/src/PactNet/Drivers/Plugins/PluginPactDriver.cs +++ b/src/PactNet/Drivers/Plugins/PluginPactDriver.cs @@ -6,17 +6,14 @@ namespace PactNet.Drivers.Plugins /// /// Driver for plugin-based pacts /// - internal class PluginPactDriver : AbstractPactDriver, IPluginPactDriver + internal class PluginPactDriver : AbstractPactDriver, IPluginPactDriver, IDisposable { - private readonly PactHandle pact; - /// /// Initialize a new instance of the class. /// /// Pact handle internal PluginPactDriver(PactHandle pact) : base(pact) { - this.pact = pact; } /// @@ -27,36 +24,33 @@ internal PluginPactDriver(PactHandle pact) : base(pact) public IPluginInteractionDriver NewSyncInteraction(string description) { InteractionHandle interaction = NativeInterop.NewSyncMessageInteraction(this.pact, description); - return new PluginInteractionDriver(this.pact, interaction); + return new PluginInteractionDriver(interaction); } + /// - /// Create the mock server for the current pact + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// - /// Host for the mock server - /// Port for the mock server, or null to allocate one automatically - /// Transport - e.g. http, https, grpc - /// Transport config string - /// Mock server port - /// Failed to start mock server - public IMockServerDriver CreateMockServer(string host, int? port, string transport, string transportConfig) + public void Dispose() { - int result = NativeInterop.CreateMockServerForTransport(this.pact, host, (ushort)port.GetValueOrDefault(0), transport, transportConfig); + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } - if (result > 0) - { - return new MockServerDriver(host, result, false); - } + /// + /// Allows an object to try to free resources and perform other cleanup operations before it is reclaimed by garbage collection. + /// + ~PluginPactDriver() + { + this.ReleaseUnmanagedResources(); + } - throw result switch - { - -1 => new InvalidOperationException("Invalid handle when starting mock server"), - -2 => new InvalidOperationException("The transport config is not valid JSON"), - -3 => new InvalidOperationException("Unable to start mock server"), - -4 => new InvalidOperationException("The pact reference library panicked"), - -5 => new InvalidOperationException("The IPAddress is invalid"), - _ => new InvalidOperationException($"Unknown mock server error: {result}") - }; + /// + /// 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 b9ba21dc..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); diff --git a/src/PactNet/PactBuilder.cs b/src/PactNet/PactBuilder.cs index 0f18d3bf..aea4b1aa 100644 --- a/src/PactNet/PactBuilder.cs +++ b/src/PactNet/PactBuilder.cs @@ -1,9 +1,4 @@ -using System; -using System.Threading.Tasks; -using PactNet.Drivers; using PactNet.Drivers.Http; -using PactNet.Exceptions; -using PactNet.Internal; using PactNet.Models; namespace PactNet @@ -11,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. @@ -25,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; } /// @@ -69,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 7999b227..b1432baf 100644 --- a/src/PactNet/PactExtensions.cs +++ b/src/PactNet/PactExtensions.cs @@ -1,7 +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; @@ -12,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 /// @@ -27,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); @@ -50,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); @@ -73,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); @@ -91,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); @@ -107,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); @@ -117,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/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.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index 3edef219..e8a6098f 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -24,8 +24,7 @@ public class FfiIntegrationTests public FfiIntegrationTests(ITestOutputHelper output) { this.output = output; - - NativeInterop.LogToBuffer(LevelFilter.Trace); + PactLogLevel.Trace.LogToBuffer(); } [Fact] @@ -38,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"); @@ -54,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/PactBuilderTests.cs b/tests/PactNet.Tests/PactBuilderTests.cs index ce86c34d..a152bf72 100644 --- a/tests/PactNet.Tests/PactBuilderTests.cs +++ b/tests/PactNet.Tests/PactBuilderTests.cs @@ -12,6 +12,7 @@ namespace PactNet.Tests { public class PactBuilderTests { + private const int MockServerPort = 5000; private readonly PactBuilder builder; private readonly Mock mockDriver; @@ -28,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 { @@ -41,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); @@ -68,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); @@ -107,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] @@ -122,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/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": {