diff --git a/src/PactNet.Abstractions/Generators/Generate.cs b/src/PactNet.Abstractions/Generators/Generate.cs new file mode 100644 index 00000000..405a4b25 --- /dev/null +++ b/src/PactNet.Abstractions/Generators/Generate.cs @@ -0,0 +1,15 @@ +namespace PactNet.Generators; + +public static class Generate +{ + /// + /// Generates a value that is looked up from the provider state context using the given expression + /// + /// Example value + /// String expression + /// Generator + public static IGenerator ProviderState(string example, string expression) + { + return new ProviderStateGenerator(example, expression); + } +} diff --git a/src/PactNet.Abstractions/Generators/IGenerator.cs b/src/PactNet.Abstractions/Generators/IGenerator.cs new file mode 100644 index 00000000..454a41f4 --- /dev/null +++ b/src/PactNet.Abstractions/Generators/IGenerator.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using PactNet.Matchers; + +namespace PactNet.Generators; + +public interface IGenerator : IMatcher +{ + /// + /// Type of the generator + /// + [JsonProperty("pact:generator:type")] + string GeneratorType { get; } +} diff --git a/src/PactNet.Abstractions/Generators/ProviderStateGenerator.cs b/src/PactNet.Abstractions/Generators/ProviderStateGenerator.cs new file mode 100644 index 00000000..498575c7 --- /dev/null +++ b/src/PactNet.Abstractions/Generators/ProviderStateGenerator.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace PactNet.Generators; + +public class ProviderStateGenerator : IGenerator +{ + public string Type => "type"; + + public dynamic Value { get; } + + public string GeneratorType => "ProviderState"; + + /// + /// Expression to lookup in provider state context + /// + [JsonProperty("expression")] + public string Expression { get; } + + public ProviderStateGenerator(dynamic example, string expression) + { + this.Value = example; + this.Expression = expression; + } +} diff --git a/src/PactNet.Abstractions/IRequestBuilder.cs b/src/PactNet.Abstractions/IRequestBuilder.cs index c4350764..e5b349a4 100644 --- a/src/PactNet.Abstractions/IRequestBuilder.cs +++ b/src/PactNet.Abstractions/IRequestBuilder.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Net.Http; using Newtonsoft.Json; +using PactNet.Generators; using PactNet.Matchers; namespace PactNet @@ -116,6 +117,22 @@ public interface IRequestBuilderV3 /// Fluent builder IRequestBuilderV3 WithRequest(string method, string path); + /// + /// Set the request + /// + /// Request method + /// Path value generator + /// Fluent builder + IRequestBuilderV3 WithRequest(HttpMethod method, IGenerator generator); + + /// + /// Set the request + /// + /// Request method + /// Path value generator + /// Fluent builder + IRequestBuilderV3 WithRequest(string method, IGenerator generator); + /// /// Add a query string parameter /// @@ -125,6 +142,15 @@ public interface IRequestBuilderV3 /// You can add a query parameter with the same key multiple times IRequestBuilderV3 WithQuery(string key, string value); + /// + /// Add a query string parameter + /// + /// Query parameter key + /// Query parameter value generator + /// Fluent builder + /// You can add a query parameter with the same key multiple times + IRequestBuilderV3 WithQuery(string key, IGenerator generator); + /// /// Add a request header /// diff --git a/src/PactNet/RequestBuilder.cs b/src/PactNet/RequestBuilder.cs index 9716700b..5942fad6 100644 --- a/src/PactNet/RequestBuilder.cs +++ b/src/PactNet/RequestBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net.Http; using Newtonsoft.Json; +using PactNet.Generators; using PactNet.Interop; using PactNet.Matchers; @@ -145,6 +146,15 @@ IRequestBuilderV3 IRequestBuilderV3.Given(string providerState, IDictionary this.WithRequest(method, path); + /// + /// Set the request + /// + /// Request method + /// Path value generator + /// Fluent builder + IRequestBuilderV3 IRequestBuilderV3.WithRequest(HttpMethod method, IGenerator generator) + => this.WithRequest(method, generator); + /// /// Set the request /// @@ -154,6 +164,15 @@ IRequestBuilderV3 IRequestBuilderV3.WithRequest(HttpMethod method, string path) IRequestBuilderV3 IRequestBuilderV3.WithRequest(string method, string path) => this.WithRequest(method, path); + /// + /// Set the request + /// + /// Request method + /// Path value generator + /// Fluent builder + IRequestBuilderV3 IRequestBuilderV3.WithRequest(string method, IGenerator generator) + => this.WithRequest(method, generator); + /// /// Add a query string parameter /// @@ -164,6 +183,16 @@ IRequestBuilderV3 IRequestBuilderV3.WithRequest(string method, string path) IRequestBuilderV3 IRequestBuilderV3.WithQuery(string key, string value) => this.WithQuery(key, value); + /// + /// Add a query string parameter + /// + /// Query parameter key + /// Query parameter value generator + /// Fluent builder + /// You can add a query parameter with the same key multiple times + IRequestBuilderV3 IRequestBuilderV3.WithQuery(string key, IGenerator generator) + => this.WithQuery(key, generator); + /// /// Add a request header /// @@ -244,6 +273,15 @@ internal RequestBuilder Given(string providerState, IDictionary internal RequestBuilder WithRequest(HttpMethod method, string path) => this.WithRequest(method.Method, path); + /// + /// Set the request + /// + /// Request method + /// Path value generator + /// Fluent builder + internal RequestBuilder WithRequest(HttpMethod method, IGenerator generator) + => this.WithRequest(method.Method, generator); + /// /// Set the request /// @@ -258,6 +296,18 @@ internal RequestBuilder WithRequest(string method, string path) return this; } + /// + /// Set the request + /// + /// Request method + /// Path value generator + /// Fluent builder + internal RequestBuilder WithRequest(string method, IGenerator generator) + { + var serialised = JsonConvert.SerializeObject(generator, this.defaultSettings); + return this.WithRequest(method, serialised); + } + /// /// Add a query string parameter /// @@ -274,6 +324,20 @@ internal RequestBuilder WithQuery(string key, string value) return this; } + /// + /// Add a query string parameter + /// + /// Query parameter key + /// Query parameter value generator + /// Fluent builder + /// You can add a query parameter with the same key multiple times + internal RequestBuilder WithQuery(string key, IGenerator generator) + { + var serialised = JsonConvert.SerializeObject(generator, this.defaultSettings); + return this.WithQuery(key, serialised); + } + + /// /// Add a request header /// diff --git a/tests/PactNet.Abstractions.Tests/Generators/GenerateTests.cs b/tests/PactNet.Abstractions.Tests/Generators/GenerateTests.cs new file mode 100644 index 00000000..8e0c5ff4 --- /dev/null +++ b/tests/PactNet.Abstractions.Tests/Generators/GenerateTests.cs @@ -0,0 +1,20 @@ +using FluentAssertions; +using PactNet.Generators; +using Xunit; + +namespace PactNet.Abstractions.Tests.Generators +{ + public class GenerateTests + { + [Fact] + public void ProviderState_WhenCalled_ReturnsGenerator() + { + const string example = "/ticket/WO1FN"; + const string expression = @"/ticket/${pnr}"; + + var matcher = Generate.ProviderState(example, expression); + + matcher.Should().BeEquivalentTo(new ProviderStateGenerator(example, expression)); + } + } +} diff --git a/tests/PactNet.Abstractions.Tests/Generators/ProviderStateGeneratorTests.cs b/tests/PactNet.Abstractions.Tests/Generators/ProviderStateGeneratorTests.cs new file mode 100644 index 00000000..c0a3c393 --- /dev/null +++ b/tests/PactNet.Abstractions.Tests/Generators/ProviderStateGeneratorTests.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using Newtonsoft.Json; +using PactNet.Generators; +using PactNet.Matchers; +using Xunit; + +namespace PactNet.Abstractions.Tests.Generators +{ + public class ProviderStateGeneratorTests + { + [Fact] + public void Ctor_WhenCalled_SerialisesCorrectly() + { + const string example = "hello@tester.com"; + const string expression = "${email}"; + + var generator = new ProviderStateGenerator(example, expression); + + string actual = JsonConvert.SerializeObject(generator); + string expected = $@"{{""pact:matcher:type"":""type"",""value"":""{example}"",""pact:generator:type"":""ProviderState"",""expression"":""{expression}""}}"; + + actual.Should().BeEquivalentTo(expected); + } + } +} diff --git a/tests/PactNet.Tests/RequestBuilderTests.cs b/tests/PactNet.Tests/RequestBuilderTests.cs index 0a3851a7..f3357fea 100644 --- a/tests/PactNet.Tests/RequestBuilderTests.cs +++ b/tests/PactNet.Tests/RequestBuilderTests.cs @@ -6,6 +6,7 @@ using Moq; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; +using PactNet.Generators; using PactNet.Interop; using Xunit; using Match = PactNet.Matchers.Match; @@ -66,6 +67,18 @@ public void WithRequest_HttpMethod_AddsRequest() this.mockServer.Verify(s => s.WithRequest(this.handle, "POST", "/some/path")); } + [Fact] + public void WithRequest_HttpMethod_ProviderStateGenerator_AddsRequest() + { + const string example = "/some/example-path"; + const string expression = "/some/${path}"; + const string expectedValue = $@"{{""pact:matcher:type"":""type"",""value"":""{example}"",""pact:generator:type"":""ProviderState"",""expression"":""{expression}""}}"; + + this.builder.WithRequest(HttpMethod.Post, Generate.ProviderState(example, expression)); + + this.mockServer.Verify(s => s.WithRequest(this.handle, "POST", expectedValue)); + } + [Fact] public void WithRequest_String_AddsRequest() { @@ -74,6 +87,18 @@ public void WithRequest_String_AddsRequest() this.mockServer.Verify(s => s.WithRequest(this.handle, "POST", "/some/path")); } + [Fact] + public void WithRequest_String_ProviderStateGenerator_AddsRequest() + { + const string example = "/some/example-path"; + const string expression = "/some/${path}"; + const string expectedValue = $@"{{""pact:matcher:type"":""type"",""value"":""{example}"",""pact:generator:type"":""ProviderState"",""expression"":""{expression}""}}"; + + this.builder.WithRequest("POST", Generate.ProviderState(example, expression)); + + this.mockServer.Verify(s => s.WithRequest(this.handle, "POST", expectedValue)); + } + [Fact] public void WithQuery_WhenCalled_AddsQueryParam() { @@ -82,6 +107,15 @@ public void WithQuery_WhenCalled_AddsQueryParam() this.mockServer.Verify(s => s.WithQueryParameter(this.handle, "name", "value", 0)); } + [Fact] + public void WithQuery_Generator_WhenCalled_AddsQueryParam() + { + const string expectedValue = $@"{{""pact:matcher:type"":""type"",""value"":""example"",""pact:generator:type"":""ProviderState"",""expression"":""${{value}}""}}"; + + this.builder.WithQuery("name", Generate.ProviderState("example", "${value}")); + this.mockServer.Verify(s => s.WithQueryParameter(this.handle, "name", expectedValue, 0)); + } + [Fact] public void WithQuery_RepeatedQuery_SetsIndex() { @@ -94,6 +128,20 @@ public void WithQuery_RepeatedQuery_SetsIndex() this.mockServer.Verify(s => s.WithQueryParameter(this.handle, "other", "value", 0)); } + [Fact] + public void WithQuery_Generator_RepeatedQuery_SetsIndex() + { + const string expectedValue2 = $@"{{""pact:matcher:type"":""type"",""value"":""value2"",""pact:generator:type"":""ProviderState"",""expression"":""${{value}}""}}"; + + this.builder.WithQuery("name", "value1"); + this.builder.WithQuery("name", Generate.ProviderState("value2", "${value}")); + this.builder.WithQuery("other", "value"); + + this.mockServer.Verify(s => s.WithQueryParameter(this.handle, "name", "value1", 0)); + this.mockServer.Verify(s => s.WithQueryParameter(this.handle, "name", expectedValue2, 1)); + this.mockServer.Verify(s => s.WithQueryParameter(this.handle, "other", "value", 0)); + } + [Fact] public void WithHeader_Matcher_WhenCalled_AddsSerialisedHeaderParam() { @@ -104,6 +152,16 @@ public void WithHeader_Matcher_WhenCalled_AddsSerialisedHeaderParam() this.mockServer.Verify(s => s.WithRequestHeader(this.handle, "name", expectedValue, 0)); } + [Fact] + public void WithHeader_Generator_WhenCalled_AddsSerialisedHeaderParam() + { + var expectedValue = "{\"pact:matcher:type\":\"type\",\"value\":\"header\",\"pact:generator:type\":\"ProviderState\",\"expression\":\"${header}\"}"; + + this.builder.WithHeader("name", Generate.ProviderState("header", "${header}")); + + this.mockServer.Verify(s => s.WithRequestHeader(this.handle, "name", expectedValue, 0)); + } + [Fact] public void WithHeader_RepeatedMatcherHeader_SetsIndex() {