Skip to content

Commit 0085b05

Browse files
committed
feat(grpc): Add PactNet.Extensions.Grpc library
1 parent 9a94134 commit 0085b05

19 files changed

+611
-91
lines changed

PactNet.sln

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrpcGreeter.Tests", "sample
4949
EndProject
5050
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PactNet.Interop", "src\PactNet.Interop\PactNet.Interop.csproj", "{D1D174A6-0027-49C4-AC6B-62CDF0F8511B}"
5151
EndProject
52+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PactNet.Extensions.Grpc", "src\PactNet.Extensions.Grpc\PactNet.Extensions.Grpc.csproj", "{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}"
53+
EndProject
54+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pacts", "pacts", "{AF3752A7-877C-4958-8438-222D2C842D45}"
55+
ProjectSection(SolutionItems) = preProject
56+
samples\Grpc\pacts\grpc-greeter-client-grpc-greeter.json = samples\Grpc\pacts\grpc-greeter-client-grpc-greeter.json
57+
EndProjectSection
58+
EndProject
59+
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}"
60+
EndProject
5261
Global
5362
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5463
Debug|Any CPU = Debug|Any CPU
@@ -227,6 +236,30 @@ Global
227236
{D1D174A6-0027-49C4-AC6B-62CDF0F8511B}.Release|x64.Build.0 = Release|Any CPU
228237
{D1D174A6-0027-49C4-AC6B-62CDF0F8511B}.Release|x86.ActiveCfg = Release|Any CPU
229238
{D1D174A6-0027-49C4-AC6B-62CDF0F8511B}.Release|x86.Build.0 = Release|Any CPU
239+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
240+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Debug|Any CPU.Build.0 = Debug|Any CPU
241+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Debug|x64.ActiveCfg = Debug|Any CPU
242+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Debug|x64.Build.0 = Debug|Any CPU
243+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Debug|x86.ActiveCfg = Debug|Any CPU
244+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Debug|x86.Build.0 = Debug|Any CPU
245+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Release|Any CPU.ActiveCfg = Release|Any CPU
246+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Release|Any CPU.Build.0 = Release|Any CPU
247+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Release|x64.ActiveCfg = Release|Any CPU
248+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Release|x64.Build.0 = Release|Any CPU
249+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Release|x86.ActiveCfg = Release|Any CPU
250+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10}.Release|x86.Build.0 = Release|Any CPU
251+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
252+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
253+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Debug|x64.ActiveCfg = Debug|Any CPU
254+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Debug|x64.Build.0 = Debug|Any CPU
255+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Debug|x86.ActiveCfg = Debug|Any CPU
256+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Debug|x86.Build.0 = Debug|Any CPU
257+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
258+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Release|Any CPU.Build.0 = Release|Any CPU
259+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Release|x64.ActiveCfg = Release|Any CPU
260+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Release|x64.Build.0 = Release|Any CPU
261+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Release|x86.ActiveCfg = Release|Any CPU
262+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA}.Release|x86.Build.0 = Release|Any CPU
230263
EndGlobalSection
231264
GlobalSection(SolutionProperties) = preSolution
232265
HideSolutionNode = FALSE
@@ -248,6 +281,9 @@ Global
248281
{13756BC3-0750-E2AF-E1F0-565855A3E636} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
249282
{DC5371A0-7DE2-4CC5-D0E1-1DF6CB567FA6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
250283
{D1D174A6-0027-49C4-AC6B-62CDF0F8511B} = {CF67D7A1-AE96-420B-9971-65E535B903E8}
284+
{30EF8B05-ACE6-482B-97D3-B8EE45F1DE10} = {CF67D7A1-AE96-420B-9971-65E535B903E8}
285+
{AF3752A7-877C-4958-8438-222D2C842D45} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
286+
{F4C6BFE1-003A-4BFB-B855-6F5E72AB98CA} = {A117BBC6-60BB-4282-BF10-E616DE0AFAD0}
251287
EndGlobalSection
252288
GlobalSection(ExtensibilityGlobals) = postSolution
253289
SolutionGuid = {C2CBC30C-92D4-4E3A-A5B8-1E5D4E938DFC}

samples/Grpc/GrpcGreeterClient.Tests/GrpcGreeterClient.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
</PackageReference>
1414
</ItemGroup>
1515
<ItemGroup>
16+
<ProjectReference Include="..\..\..\src\PactNet.Extensions.Grpc\PactNet.Extensions.Grpc.csproj" />
1617
<ProjectReference Include="..\..\..\src\PactNet.Interop\PactNet.Interop.csproj" />
18+
<ProjectReference Include="..\..\..\src\PactNet.Output.Xunit\PactNet.Output.Xunit.csproj" />
1719
<ProjectReference Include="..\GrpcGreeterClient\GrpcGreeterClient.csproj" />
1820
</ItemGroup>
1921
<ItemGroup>
Lines changed: 63 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,87 @@
1+
using System;
12
using System.Text.Json;
23
using System.Threading.Tasks;
34
using FluentAssertions;
45
using Xunit;
5-
using PactNet.Interop;
66
using System.IO;
7+
using System.Text.Json.Serialization;
78
using PactNet;
9+
using PactNet.Exceptions;
10+
using PactNet.Extensions.Grpc;
11+
using PactNet.Output.Xunit;
812
using Xunit.Abstractions;
913

1014
namespace GrpcGreeterClient.Tests
1115
{
12-
public class GrpcGreeterClientTests
16+
public class GrpcGreeterClientTests : IDisposable
1317
{
14-
private readonly ITestOutputHelper testOutputHelper;
18+
private readonly IGrpcPactBuilderV4 pact;
1519

16-
public GrpcGreeterClientTests(ITestOutputHelper testOutputHelper)
20+
public GrpcGreeterClientTests(ITestOutputHelper output)
1721
{
18-
this.testOutputHelper = testOutputHelper;
19-
PactLogLevel.Information.InitialiseLogging();
22+
var config = new PactConfig
23+
{
24+
PactDir = "../../../../pacts/",
25+
Outputters = new[]
26+
{
27+
new XunitOutput(output)
28+
},
29+
DefaultJsonSettings = new JsonSerializerOptions
30+
{
31+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
32+
PropertyNameCaseInsensitive = true,
33+
Converters = { new JsonStringEnumConverter() }
34+
},
35+
LogLevel = PactLogLevel.Debug
36+
};
37+
38+
this.pact = Pact.V4("grpc-greeter-client", "grpc-greeter", config).WithGrpcInteractions();
2039
}
2140

2241
[Fact]
23-
public async Task ReturnsMismatchWhenNoGrpcClientRequestMade()
42+
public void ThrowsExceptionWhenNoGrpcClientRequestMade()
2443
{
25-
// arrange
26-
var host = "0.0.0.0";
27-
var pact = NativeInterop.NewPact("grpc-greeter-client", "grpc-greeter");
28-
var interaction = PluginInterop.NewSyncMessageInteraction(pact, "a request to a plugin");
29-
NativeInterop.WithSpecification(pact, PactSpecification.V4);
30-
var content = $@"{{
31-
""pact:proto"":""{Path.Join(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "GrpcGreeterClient", "Protos", "greet.proto").Replace("\\", "\\\\")}"",
32-
""pact:proto-service"": ""Greeter/SayHello"",
33-
""pact:content-type"": ""application/protobuf"",
34-
""request"": {{
35-
""name"": ""matching(type, 'foo')""
36-
}},
37-
""response"": {{
38-
""message"": ""matching(type, 'Hello foo')""
39-
}}
40-
}}";
41-
42-
using var pluginDriver = pact.UsePlugin("protobuf", "0.4.0");
43-
PluginInterop.PluginInteractionContents(interaction, 0, "application/grpc", content);
44-
45-
using var driver = pact.CreateMockServer(host, 0, "grpc", false);
46-
var port = driver.Port;
47-
testOutputHelper.WriteLine("Port: " + port);
48-
49-
var matched = driver.MockServerMatched();
50-
testOutputHelper.WriteLine("Matched: " + matched);
51-
matched.Should().BeFalse();
52-
53-
var MismatchesString = driver.MockServerMismatches();
54-
testOutputHelper.WriteLine("Mismatches: " + MismatchesString);
55-
var MismatchesJson = JsonSerializer.Deserialize<JsonElement>(MismatchesString);
56-
var ErrorString = MismatchesJson[0].GetProperty("error").GetString();
57-
var ExpectedPath = MismatchesJson[0].GetProperty("path").GetString();
58-
59-
ErrorString.Should().Be("Did not receive any requests for path 'Greeter/SayHello'");
60-
ExpectedPath.Should().Be("Greeter/SayHello");
61-
62-
await Task.Delay(1);
44+
string protoFilePath = Path.Join(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "GrpcGreeterClient", "Protos", "greet.proto");
45+
this.pact
46+
.UponReceiving("A greeting request to say hello.")
47+
.WithRequest(protoFilePath, nameof(Greeter), "SayHello",
48+
new { name = "matching(type, 'foo')" })
49+
.WillRespond()
50+
.WithBody(new { message = "matching(type, 'Hello foo')" });
51+
52+
Assert.Throws<PactFailureException>(() =>
53+
this.pact.Verify(ctx =>
54+
{
55+
// No grpc call here results in failure.
56+
}));
6357
}
58+
6459
[Fact]
65-
public async Task WritesPactWhenGrpcClientRequestMade()
60+
public async Task WritesPactForGreeterSayHelloRequest()
6661
{
67-
// arrange
68-
var host = "0.0.0.0";
69-
var pact = NativeInterop.NewPact("grpc-greeter-client", "grpc-greeter");
70-
var interaction = PluginInterop.NewSyncMessageInteraction(pact, "a request to a plugin");
71-
NativeInterop.WithSpecification(pact, PactSpecification.V4);
72-
var content = $@"{{
73-
""pact:proto"":""{Path.Join(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "GrpcGreeterClient", "Protos", "greet.proto").Replace("\\", "\\\\")}"",
74-
""pact:proto-service"": ""Greeter/SayHello"",
75-
""pact:content-type"": ""application/protobuf"",
76-
""request"": {{
77-
""name"": ""matching(type, 'foo')""
78-
}},
79-
""response"": {{
80-
""message"": ""matching(type, 'Hello foo')""
81-
}}
82-
}}";
83-
84-
using var pluginDriver = pact.UsePlugin("protobuf", "0.4.0");
85-
PluginInterop.PluginInteractionContents(interaction, 0, "application/grpc", content);
86-
87-
using var driver = pact.CreateMockServer(host, 0, "grpc", false);
88-
var port = driver.Port;
89-
testOutputHelper.WriteLine("Port: " + port);
90-
91-
// act
92-
var client = new GreeterClientWrapper("http://localhost:" + port);
93-
var result = await client.SayHello("foo");
94-
testOutputHelper.WriteLine("Result: " + result);
95-
96-
// assert
97-
result.Should().Be("Hello foo");
98-
var matched = driver.MockServerMatched();
99-
testOutputHelper.WriteLine("Matched: " + matched);
100-
matched.Should().BeTrue();
101-
102-
var MismatchesString = driver.MockServerMismatches();
103-
testOutputHelper.WriteLine("Mismatches: " + MismatchesString);
104-
105-
MismatchesString.Should().Be("[]");
106-
107-
PactFileWriter.WritePactFileForPort(port, "../../../../pacts");
62+
// Arrange
63+
string protoFilePath = Path.Join(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "GrpcGreeterClient", "Protos", "greet.proto");
64+
this.pact
65+
.UponReceiving("A greeting request to say hello.")
66+
.WithRequest(protoFilePath, nameof(Greeter), "SayHello",
67+
new { name = "matching(type, 'foo')" })
68+
.WillRespond()
69+
.WithBody(new { message = "matching(type, 'Hello foo')" });
70+
71+
await this.pact.VerifyAsync(async ctx =>
72+
{
73+
74+
// Arrange
75+
var client = new GreeterClientWrapper(ctx.MockServerUri.AbsoluteUri);
76+
77+
// Act
78+
var greeting = await client.SayHello("foo");
79+
80+
// Assert
81+
greeting.Should().Be("Hello foo");
82+
});
10883
}
10984

85+
public void Dispose() => this.pact?.Dispose();
11086
}
11187
}

samples/Grpc/pacts/grpc-greeter-client-grpc-greeter.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
},
55
"interactions": [
66
{
7-
"description": "a request to a plugin",
7+
"description": "A greeting request to say hello.",
88
"interactionMarkup": {
99
"markup": "```protobuf\nmessage HelloReply {\n string message = 1;\n}\n```\n",
1010
"markupType": "COMMON_MARK"

src/PactNet/ConsumerContext.cs renamed to src/PactNet.Abstractions/ConsumerContext.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ namespace PactNet
55
/// <summary>
66
/// Context for consumer interaction verification
77
/// </summary>
8-
internal class ConsumerContext : IConsumerContext
8+
public class ConsumerContext : IConsumerContext
99
{
1010
/// <summary>
1111
/// URI for the mock server
1212
/// </summary>
13-
public Uri MockServerUri { get; internal set; }
13+
public Uri MockServerUri { get; set; }
1414
}
1515
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
using System;
2+
using PactNet.Interop;
23

34
namespace PactNet.Drivers;
45

56
public interface IPluginDriver : IDisposable
67
{
8+
/// <summary>
9+
/// Add interaction contents for a plugin interaction.
10+
/// </summary>
11+
/// <param name="interaction"></param>
12+
/// <param name="contentType"></param>
13+
/// <param name="content"></param>
14+
/// <param name="part"></param>
15+
/// <exception cref="InvalidOperationException"></exception>
16+
void WithInteractionContents(InteractionHandle interaction, string contentType, string content, InteractionPart part = InteractionPart.Request);
717
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("PactNet.Extensions.Grpc.Tests")]
2+
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DynamicProxyGenAssembly2")]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using PactNet.Interop;
2+
using PactNet.Models;
3+
4+
5+
namespace PactNet.Extensions.Grpc;
6+
7+
/// <summary>
8+
/// Grpc extensions for Pact V4
9+
/// </summary>
10+
public static class GrpcExtensions
11+
{
12+
/// <summary>
13+
/// Add asynchronous message (i.e. consumer/producer) interactions to the pact
14+
/// </summary>
15+
/// <param name="pact">Pact details</param>
16+
/// <param name="port">Port for the mock server. If null, one will be assigned automatically</param>
17+
/// <param name="host">Host for the mock server</param>
18+
/// <returns>Pact builder</returns>
19+
public static IGrpcPactBuilderV4 WithGrpcInteractions(this IPactV4 pact, int? port = null, IPAddress host = IPAddress.Loopback)
20+
{
21+
pact.Config.LogLevel.InitialiseLogging();
22+
PactHandle grpcPact = NewGrpcPact(pact.Consumer, pact.Provider);
23+
24+
var builder = new GrpcPactBuilder(grpcPact, pact.Config);
25+
return builder;
26+
}
27+
28+
private static PactHandle NewGrpcPact(string consumerName, string providerName)
29+
{
30+
PactHandle pact = NativeInterop.NewPact(consumerName, providerName);
31+
NativeInterop.WithSpecification(pact, PactSpecification.V4).ThrowExceptionOnFailure();
32+
return pact;
33+
}
34+
}

0 commit comments

Comments
 (0)