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": {