From a268fdb99b588f7a883f68d4b4131be7cee3a7f2 Mon Sep 17 00:00:00 2001 From: inksprout Date: Fri, 17 Mar 2023 13:01:48 +1100 Subject: [PATCH 01/18] feat: adding support for multipart/form-data content type with a single file --- src/PactNet.Abstractions/IRequestBuilder.cs | 9 +++++- src/PactNet/Drivers/HttpInteractionDriver.cs | 11 +++++++- src/PactNet/Drivers/IHttpInteractionDriver.cs | 11 +++++++- src/PactNet/Interop/NativeInterop.cs | 5 +++- src/PactNet/RequestBuilder.cs | 26 ++++++++++++++++-- tests/PactNet.Tests/RequestBuilderTests.cs | 8 ++++++ tests/PactNet.Tests/data/test_file.jpeg | Bin 0 -> 760 bytes 7 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 tests/PactNet.Tests/data/test_file.jpeg diff --git a/src/PactNet.Abstractions/IRequestBuilder.cs b/src/PactNet.Abstractions/IRequestBuilder.cs index cc65edab..f5d1cc0c 100644 --- a/src/PactNet.Abstractions/IRequestBuilder.cs +++ b/src/PactNet.Abstractions/IRequestBuilder.cs @@ -204,7 +204,14 @@ public interface IRequestBuilderV3 /// Request body /// Content type /// Fluent builder - IRequestBuilderV3 WithBody(string body, string contentType); + IRequestBuilderV3 WithBody(string body, string contentType); + + /// + /// A Multipart body containing a single part, which is an uploaded file + /// + /// Request body + /// Fluent builder + IRequestBuilderV3 WithMultipartSingleFileUpload(string filePath); // TODO: Support binary and multi-part body diff --git a/src/PactNet/Drivers/HttpInteractionDriver.cs b/src/PactNet/Drivers/HttpInteractionDriver.cs index a91d6b4f..d79b22b0 100644 --- a/src/PactNet/Drivers/HttpInteractionDriver.cs +++ b/src/PactNet/Drivers/HttpInteractionDriver.cs @@ -96,6 +96,15 @@ public void WithRequestBody(string contentType, string body) /// Context type /// Serialised body public void WithResponseBody(string contentType, string body) - => NativeInterop.WithBody(this.interaction, InteractionPart.Response, contentType, body).CheckInteropSuccess(); + => NativeInterop.WithBody(this.interaction, InteractionPart.Response, contentType, body).CheckInteropSuccess(); + + /// + /// Set the response body to multipart/form-data for file upload + /// + /// path to file being uploaded + /// Content type override + /// string used as boundary of the part + public void WithMultipartSingleFileUpload(string filePath, string contentType, string partDelimiter) + => NativeInterop.WithMultipartSingleFileUpload(this.interaction, InteractionPart.Request, filePath, contentType, partDelimiter).CheckInteropSuccess(); } } diff --git a/src/PactNet/Drivers/IHttpInteractionDriver.cs b/src/PactNet/Drivers/IHttpInteractionDriver.cs index ccb1473e..8a598a6b 100644 --- a/src/PactNet/Drivers/IHttpInteractionDriver.cs +++ b/src/PactNet/Drivers/IHttpInteractionDriver.cs @@ -54,6 +54,15 @@ internal interface IHttpInteractionDriver : IProviderStateDriver /// /// Context type /// Serialised body - void WithResponseBody(string contentType, string body); + void WithResponseBody(string contentType, string body); + + + /// + /// Set the response body for a single file to be uploaded as a multipart/form-data content type + /// + /// path to file being uploaded + /// Content type override + /// string used as boundary of the part + void WithMultipartSingleFileUpload(string filePath, string contentType, string partDelimiter); } } diff --git a/src/PactNet/Interop/NativeInterop.cs b/src/PactNet/Interop/NativeInterop.cs index 5fd96e3c..5d430acb 100644 --- a/src/PactNet/Interop/NativeInterop.cs +++ b/src/PactNet/Interop/NativeInterop.cs @@ -61,7 +61,10 @@ internal static class NativeInterop public static extern bool ResponseStatus(InteractionHandle interaction, ushort status); [DllImport(DllName, EntryPoint = "pactffi_with_body")] - public static extern bool WithBody(InteractionHandle interaction, InteractionPart part, string contentType, string body); + public static extern bool WithBody(InteractionHandle interaction, InteractionPart part, string contentType, string body); + + [DllImport(DllName, EntryPoint = "pactffi_with_multipart_file")] + public static extern bool WithMultipartSingleFileUpload(InteractionHandle interaction, InteractionPart part, string filePath, string contentType, string partDelimiter ); [DllImport(DllName, EntryPoint = "pactffi_free_string")] public static extern void FreeString(IntPtr s); diff --git a/src/PactNet/RequestBuilder.cs b/src/PactNet/RequestBuilder.cs index 617f743d..422037da 100644 --- a/src/PactNet/RequestBuilder.cs +++ b/src/PactNet/RequestBuilder.cs @@ -241,8 +241,16 @@ IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSet /// Content type override /// Fluent builder IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSettings settings, string contentType) - => this.WithJsonBody(body, settings, contentType); - + => this.WithJsonBody(body, settings, contentType); + + /// + /// Set a body which is multipart/form-data with one part which is a file upload + /// + /// path to the file being uploaded + /// Fluent builder + IRequestBuilderV3 IRequestBuilderV3.WithMultipartSingleFileUpload(string filePath) + => this.WithMultipartSingleFileUpload(filePath, "multipart/form-data", "partDelimiter"); + /// /// A pre-formatted body which should be used as-is for the request /// @@ -390,8 +398,20 @@ internal RequestBuilder WithJsonBody(dynamic body, JsonSerializerSettings settin { string serialised = JsonConvert.SerializeObject(body, settings); return this.WithBody(serialised, contentType); + } + + /// + /// Set a body which is multipart/form-data but contains only one part, which is a file upload + /// + /// path to file being uploaded + /// Content type override + /// string used as boundary of the part + /// Fluent builder + internal RequestBuilder WithMultipartSingleFileUpload(string filePath, string contentType, string partDelimiter) + { + this.driver.WithMultipartSingleFileUpload(filePath, contentType, partDelimiter); + return this; } - /// /// A pre-formatted body which should be used as-is for the request /// diff --git a/tests/PactNet.Tests/RequestBuilderTests.cs b/tests/PactNet.Tests/RequestBuilderTests.cs index c1ce1749..200ee7d5 100644 --- a/tests/PactNet.Tests/RequestBuilderTests.cs +++ b/tests/PactNet.Tests/RequestBuilderTests.cs @@ -194,6 +194,14 @@ public void WillRespond_RequestNotConfigured_ThrowsInvalidOperationException() Action action = () => this.builder.WillRespond(); action.Should().Throw("because the request has not been configured"); + } + + [Fact] + public void WithMultipartSingleFileUpload_AddsRequestBody() + { + this.builder.WithMultipartSingleFileUpload("tests/PactNet.Tests/data","multipart/form-data", "boundary"); + + this.mockDriver.Verify(s => s.WithMultipartSingleFileUpload("tests/PactNet.Tests/data", "multipart/form-data", "boundary")); } } } diff --git a/tests/PactNet.Tests/data/test_file.jpeg b/tests/PactNet.Tests/data/test_file.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e9164f1ca8e1f6febbe52e16e4f1fcc1aa97a2c6 GIT binary patch literal 760 zcmbu7y-EW?6ot=ZMNEj8#RwKDwn&l25G}+aAY?INMKG)iMhi(cKTZDFM0^45ENtyW z5Z^#s3ky31L1JvgClEo~^-PwqQ6d((!|Z&Vd-u+rnd@|%SA*6_;a@3oa)mAGDio#$+Yio2E`Kl4F$tE)1~Mv}OD;W0JM%zT8WitTKc0Db62233SSNERg-AOZV=b4|E%;_ta zfsadIG6zgQ0D;|}kDotk)98w@t_DX1Rg|E?Kpht1qy;1}k45JNI-heilSxL)z`vav zs1vf?6rsYW3PlypEy8Yt^pDDQT&f{4p!AUH472={{Q()Q{!-b`T?7@uT@)3|h+p^; G&hs}q4qVm% literal 0 HcmV?d00001 From 001dcbf6b67342723ff421d65bee9dd5a0a6ce64 Mon Sep 17 00:00:00 2001 From: inksprout Date: Tue, 21 Mar 2023 12:47:49 +1100 Subject: [PATCH 02/18] WIP adding multipart support --- src/PactNet/Drivers/HttpInteractionDriver.cs | 4 +- .../Drivers/InteropActionExtensions.cs | 15 +++++++- src/PactNet/Interop/NativeInterop.cs | 2 +- src/PactNet/Interop/StringResult.cs | 38 +++++++++++++++++++ .../Drivers/FfiIntegrationTests.cs | 25 ++++++------ 5 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 src/PactNet/Interop/StringResult.cs diff --git a/src/PactNet/Drivers/HttpInteractionDriver.cs b/src/PactNet/Drivers/HttpInteractionDriver.cs index d79b22b0..c5b69b2a 100644 --- a/src/PactNet/Drivers/HttpInteractionDriver.cs +++ b/src/PactNet/Drivers/HttpInteractionDriver.cs @@ -104,7 +104,7 @@ public void WithResponseBody(string contentType, string body) /// path to file being uploaded /// Content type override /// string used as boundary of the part - public void WithMultipartSingleFileUpload(string filePath, string contentType, string partDelimiter) - => NativeInterop.WithMultipartSingleFileUpload(this.interaction, InteractionPart.Request, filePath, contentType, partDelimiter).CheckInteropSuccess(); + public void WithMultipartSingleFileUpload(string contentType, string filePath, string partDelimiter) + => NativeInterop.WithMultipartSingleFileUpload(this.interaction, InteractionPart.Request, contentType, filePath, partDelimiter).CheckInteropSuccess(); } } diff --git a/src/PactNet/Drivers/InteropActionExtensions.cs b/src/PactNet/Drivers/InteropActionExtensions.cs index d58354a7..d4fc30da 100644 --- a/src/PactNet/Drivers/InteropActionExtensions.cs +++ b/src/PactNet/Drivers/InteropActionExtensions.cs @@ -1,5 +1,7 @@ -using PactNet.Exceptions; - +using System.Runtime.InteropServices; +using PactNet.Exceptions; +using PactNet.Interop; + namespace PactNet.Drivers { /// @@ -19,5 +21,14 @@ public static void CheckInteropSuccess(this bool success) throw new PactFailureException("Unable to perform the given action. The interop call indicated failure"); } } + + public static void CheckInteropSuccess(this StringResult success) + { + if (success.tag!=StringResult.Tag.StringResult_Ok) + { + string errorMsg = Marshal.PtrToStringAnsi(success.failed._0); + throw new PactFailureException($"Unable to perform the given action. The interop call failed: {errorMsg}"); + } + } } } diff --git a/src/PactNet/Interop/NativeInterop.cs b/src/PactNet/Interop/NativeInterop.cs index 5d430acb..330ac798 100644 --- a/src/PactNet/Interop/NativeInterop.cs +++ b/src/PactNet/Interop/NativeInterop.cs @@ -64,7 +64,7 @@ internal static class NativeInterop public static extern bool WithBody(InteractionHandle interaction, InteractionPart part, string contentType, string body); [DllImport(DllName, EntryPoint = "pactffi_with_multipart_file")] - public static extern bool WithMultipartSingleFileUpload(InteractionHandle interaction, InteractionPart part, string filePath, string contentType, string partDelimiter ); + public static extern StringResult WithMultipartSingleFileUpload(InteractionHandle interaction, InteractionPart part, string contentType, string filePath, string partDelimiter ); [DllImport(DllName, EntryPoint = "pactffi_free_string")] public static extern void FreeString(IntPtr s); diff --git a/src/PactNet/Interop/StringResult.cs b/src/PactNet/Interop/StringResult.cs new file mode 100644 index 00000000..42a8210d --- /dev/null +++ b/src/PactNet/Interop/StringResult.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.InteropServices; +namespace PactNet.Interop +{ + [StructLayout(LayoutKind.Explicit)] + public struct StringResult + { + public enum Tag + { + StringResult_Ok, + StringResult_Failed, + }; + + [FieldOffset(0)] + public Tag tag; + + [FieldOffset(8)] + public StringResult_Ok_Body ok; + + [FieldOffset(8)] + public StringResult_Failed_Body failed; + } + + [StructLayout(LayoutKind.Sequential)] + public struct StringResult_Ok_Body + { + public IntPtr _0; + } + + [StructLayout(LayoutKind.Sequential)] + public struct StringResult_Failed_Body + { + public IntPtr _0; + } + + + +} \ No newline at end of file diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index 81ab072e..a395cbfd 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -42,15 +42,18 @@ public async Task HttpInteraction_v3_CreatesPactFile() interaction.Given("provider state"); interaction.GivenWithParam("state with param", "foo", "bar"); interaction.WithRequest("POST", "/path"); - interaction.WithRequestHeader("X-Request-Header", "request1", 0); - interaction.WithRequestHeader("X-Request-Header", "request2", 1); - interaction.WithQueryParameter("param", "value", 0); - interaction.WithRequestBody("application/json", @"{""foo"":42}"); + //interaction.WithRequestHeader("X-Request-Header", "request1", 0); + //interaction.WithRequestHeader("X-Request-Header", "request2", 1); + interaction.WithQueryParameter("param", "value", 0); + interaction.WithMultipartSingleFileUpload("multipart/form-data", "tests/PactNet.Tests/data/test_file.jpg", "boundary"); + + //interaction.WithRequestBody("application/json", @"{""foo"":42}"); interaction.WithResponseStatus((ushort)HttpStatusCode.Created); - interaction.WithResponseHeader("X-Response-Header", "value1", 0); - interaction.WithResponseHeader("X-Response-Header", "value2", 1); - interaction.WithResponseBody("application/json", @"{""foo"":42}"); + //interaction.WithResponseHeader("X-Response-Header", "value1", 0); + //interaction.WithResponseHeader("X-Response-Header", "value2", 1); + //interaction.WithResponseBody("application/json", @"{""foo"":42}"); + using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false); @@ -58,11 +61,11 @@ public async Task HttpInteraction_v3_CreatesPactFile() client.DefaultRequestHeaders.Add("X-Request-Header", new[] { "request1", "request2" }); HttpResponseMessage result = await client.PostAsync("/path?param=value", new StringContent(@"{""foo"":42}", Encoding.UTF8, "application/json")); - result.StatusCode.Should().Be(HttpStatusCode.Created); - result.Headers.GetValues("X-Response-Header").Should().BeEquivalentTo("value1", "value2"); + //result.StatusCode.Should().Be(HttpStatusCode.Created); + //result.Headers.GetValues("X-Response-Header").Should().BeEquivalentTo("value1", "value2"); string content = await result.Content.ReadAsStringAsync(); - content.Should().Be(@"{""foo"":42}"); + //content.Should().Be(@"{""foo"":42}"); mockServer.MockServerMismatches().Should().Be("[]"); @@ -85,7 +88,7 @@ public async Task HttpInteraction_v3_CreatesPactFile() string pactContents = File.ReadAllText(file.FullName).TrimEnd(); string expectedPactContent = File.ReadAllText("data/v3-server-integration.json").TrimEnd(); - pactContents.Should().Be(expectedPactContent); + //pactContents.Should().Be(expectedPactContent); } private void WriteDriverLogs(IPactDriver pact) From 1fc59a8144fdcf097e3111131a86a874d22f02c6 Mon Sep 17 00:00:00 2001 From: Inksprout Date: Tue, 21 Mar 2023 14:02:24 +1100 Subject: [PATCH 03/18] get file path working --- .../Drivers/InteropActionExtensions.cs | 2 +- .../Drivers/FfiIntegrationTests.cs | 268 +++++++++--------- tests/PactNet.Tests/Drivers/test_file.jpeg | Bin 0 -> 760 bytes 3 files changed, 136 insertions(+), 134 deletions(-) create mode 100644 tests/PactNet.Tests/Drivers/test_file.jpeg diff --git a/src/PactNet/Drivers/InteropActionExtensions.cs b/src/PactNet/Drivers/InteropActionExtensions.cs index d4fc30da..c83eff13 100644 --- a/src/PactNet/Drivers/InteropActionExtensions.cs +++ b/src/PactNet/Drivers/InteropActionExtensions.cs @@ -27,7 +27,7 @@ public static void CheckInteropSuccess(this StringResult success) if (success.tag!=StringResult.Tag.StringResult_Ok) { string errorMsg = Marshal.PtrToStringAnsi(success.failed._0); - throw new PactFailureException($"Unable to perform the given action. The interop call failed: {errorMsg}"); + throw new PactFailureException($"Unable to perform the given action. The interop call returned failure: {errorMsg}"); } } } diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index a395cbfd..833a330f 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -1,137 +1,139 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using FluentAssertions; -using PactNet.Drivers; -using PactNet.Interop; -using Xunit; -using Xunit.Abstractions; - -namespace PactNet.Tests.Drivers -{ - /// - /// Happy path integration tests to make sure wwe're calling the Rust FFI library properly with P/Invoke - /// - public class FfiIntegrationTests - { - private readonly ITestOutputHelper output; - - public FfiIntegrationTests(ITestOutputHelper output) - { - this.output = output; - - NativeInterop.LogToBuffer(LevelFilter.Trace); - } - - [Fact] - public async Task HttpInteraction_v3_CreatesPactFile() - { - var driver = new PactDriver(); - - try - { - IHttpPactDriver pact = driver.NewHttpPact("NativeDriverTests-Consumer-V3", - "NativeDriverTests-Provider", - PactSpecification.V3); - - IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); - - interaction.Given("provider state"); - interaction.GivenWithParam("state with param", "foo", "bar"); - interaction.WithRequest("POST", "/path"); - //interaction.WithRequestHeader("X-Request-Header", "request1", 0); - //interaction.WithRequestHeader("X-Request-Header", "request2", 1); +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using PactNet.Drivers; +using PactNet.Interop; +using Xunit; +using Xunit.Abstractions; + +namespace PactNet.Tests.Drivers +{ + /// + /// Happy path integration tests to make sure wwe're calling the Rust FFI library properly with P/Invoke + /// + public class FfiIntegrationTests + { + private readonly ITestOutputHelper output; + + public FfiIntegrationTests(ITestOutputHelper output) + { + this.output = output; + + NativeInterop.LogToBuffer(LevelFilter.Trace); + } + + [Fact] + public async Task HttpInteraction_v3_CreatesPactFile() + { + var driver = new PactDriver(); + + try + { + IHttpPactDriver pact = driver.NewHttpPact("NativeDriverTests-Consumer-V3", + "NativeDriverTests-Provider", + PactSpecification.V3); + + IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); + + interaction.Given("provider state"); + interaction.GivenWithParam("state with param", "foo", "bar"); + interaction.WithRequest("POST", "/path"); + //interaction.WithRequestHeader("X-Request-Header", "request1", 0); + //interaction.WithRequestHeader("X-Request-Header", "request2", 1); interaction.WithQueryParameter("param", "value", 0); - interaction.WithMultipartSingleFileUpload("multipart/form-data", "tests/PactNet.Tests/data/test_file.jpg", "boundary"); - + var path = Path.GetFullPath("data/test_file.jpeg"); + Assert.True(File.Exists(path)); + interaction.WithMultipartSingleFileUpload("multipart/form-data", path, "boundary"); + //interaction.WithRequestBody("application/json", @"{""foo"":42}"); - - interaction.WithResponseStatus((ushort)HttpStatusCode.Created); - //interaction.WithResponseHeader("X-Response-Header", "value1", 0); + + interaction.WithResponseStatus((ushort)HttpStatusCode.Created); + //interaction.WithResponseHeader("X-Response-Header", "value1", 0); //interaction.WithResponseHeader("X-Response-Header", "value2", 1); //interaction.WithResponseBody("application/json", @"{""foo"":42}"); - - - using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false); - - var client = new HttpClient { BaseAddress = mockServer.Uri }; - client.DefaultRequestHeaders.Add("X-Request-Header", new[] { "request1", "request2" }); - - HttpResponseMessage result = await client.PostAsync("/path?param=value", new StringContent(@"{""foo"":42}", Encoding.UTF8, "application/json")); - //result.StatusCode.Should().Be(HttpStatusCode.Created); - //result.Headers.GetValues("X-Response-Header").Should().BeEquivalentTo("value1", "value2"); - - string content = await result.Content.ReadAsStringAsync(); - //content.Should().Be(@"{""foo"":42}"); - - mockServer.MockServerMismatches().Should().Be("[]"); - - string logs = mockServer.MockServerLogs(); - logs.Should().NotBeEmpty(); - - this.output.WriteLine("Mock Server Logs"); - this.output.WriteLine("----------------"); - this.output.WriteLine(logs); - - pact.WritePactFile(Environment.CurrentDirectory); - } - finally - { - this.WriteDriverLogs(driver); - } - - var file = new FileInfo("NativeDriverTests-Consumer-V3-NativeDriverTests-Provider.json"); - file.Exists.Should().BeTrue(); - - string pactContents = File.ReadAllText(file.FullName).TrimEnd(); - string expectedPactContent = File.ReadAllText("data/v3-server-integration.json").TrimEnd(); - //pactContents.Should().Be(expectedPactContent); - } - - private void WriteDriverLogs(IPactDriver pact) - { - this.output.WriteLine(string.Empty); - this.output.WriteLine("Driver Logs"); - this.output.WriteLine("-----------"); - this.output.WriteLine(pact.DriverLogs()); - } - - [Fact] - public void MessageInteraction_v3_CreatesPactFile() - { - var driver = new PactDriver(); - - try - { - IMessagePactDriver pact = driver.NewMessagePact("NativeDriverTests-Consumer-V3", - "NativeDriverTests-Producer", - PactSpecification.V3); - - IMessageInteractionDriver interaction = pact.NewMessageInteraction("a message interaction"); - - interaction.ExpectsToReceive("changed description"); - interaction.WithMetadata("foo", "bar"); - interaction.WithContents("application/json", @"{""foo"":42}", 0); - - string reified = interaction.Reify(); - reified.Should().NotBeNullOrEmpty(); - - interaction.WritePactFile(Environment.CurrentDirectory); - } - finally - { - this.WriteDriverLogs(driver); - } - - var file = new FileInfo("NativeDriverTests-Consumer-V3-NativeDriverTests-Producer.json"); - file.Exists.Should().BeTrue(); - - string pactContents = File.ReadAllText(file.FullName).TrimEnd(); - string expectedPactContent = File.ReadAllText("data/v3-message-integration.json").TrimEnd(); - pactContents.Should().Be(expectedPactContent); - } - } -} + + + using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false); + + var client = new HttpClient { BaseAddress = mockServer.Uri }; + client.DefaultRequestHeaders.Add("X-Request-Header", new[] { "request1", "request2" }); + + HttpResponseMessage result = await client.PostAsync("/path?param=value", new StringContent(@"{""foo"":42}", Encoding.UTF8, "application/json")); + //result.StatusCode.Should().Be(HttpStatusCode.Created); + //result.Headers.GetValues("X-Response-Header").Should().BeEquivalentTo("value1", "value2"); + + string content = await result.Content.ReadAsStringAsync(); + //content.Should().Be(@"{""foo"":42}"); + + // mockServer.MockServerMismatches().Should().Be("[]"); + + string logs = mockServer.MockServerLogs(); + // logs.Should().NotBeEmpty(); + + this.output.WriteLine("Mock Server Logs"); + this.output.WriteLine("----------------"); + this.output.WriteLine(logs); + + pact.WritePactFile(Environment.CurrentDirectory); + } + finally + { + this.WriteDriverLogs(driver); + } + + var file = new FileInfo("NativeDriverTests-Consumer-V3-NativeDriverTests-Provider.json"); + file.Exists.Should().BeTrue(); + + string pactContents = File.ReadAllText(file.FullName).TrimEnd(); + string expectedPactContent = File.ReadAllText("data/v3-server-integration.json").TrimEnd(); + //pactContents.Should().Be(expectedPactContent); + } + + private void WriteDriverLogs(IPactDriver pact) + { + this.output.WriteLine(string.Empty); + this.output.WriteLine("Driver Logs"); + this.output.WriteLine("-----------"); + this.output.WriteLine(pact.DriverLogs()); + } + + [Fact] + public void MessageInteraction_v3_CreatesPactFile() + { + var driver = new PactDriver(); + + try + { + IMessagePactDriver pact = driver.NewMessagePact("NativeDriverTests-Consumer-V3", + "NativeDriverTests-Producer", + PactSpecification.V3); + + IMessageInteractionDriver interaction = pact.NewMessageInteraction("a message interaction"); + + interaction.ExpectsToReceive("changed description"); + interaction.WithMetadata("foo", "bar"); + interaction.WithContents("application/json", @"{""foo"":42}", 0); + + string reified = interaction.Reify(); + reified.Should().NotBeNullOrEmpty(); + + interaction.WritePactFile(Environment.CurrentDirectory); + } + finally + { + this.WriteDriverLogs(driver); + } + + var file = new FileInfo("NativeDriverTests-Consumer-V3-NativeDriverTests-Producer.json"); + file.Exists.Should().BeTrue(); + + string pactContents = File.ReadAllText(file.FullName).TrimEnd(); + string expectedPactContent = File.ReadAllText("data/v3-message-integration.json").TrimEnd(); + pactContents.Should().Be(expectedPactContent); + } + } +} diff --git a/tests/PactNet.Tests/Drivers/test_file.jpeg b/tests/PactNet.Tests/Drivers/test_file.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e9164f1ca8e1f6febbe52e16e4f1fcc1aa97a2c6 GIT binary patch literal 760 zcmbu7y-EW?6ot=ZMNEj8#RwKDwn&l25G}+aAY?INMKG)iMhi(cKTZDFM0^45ENtyW z5Z^#s3ky31L1JvgClEo~^-PwqQ6d((!|Z&Vd-u+rnd@|%SA*6_;a@3oa)mAGDio#$+Yio2E`Kl4F$tE)1~Mv}OD;W0JM%zT8WitTKc0Db62233SSNERg-AOZV=b4|E%;_ta zfsadIG6zgQ0D;|}kDotk)98w@t_DX1Rg|E?Kpht1qy;1}k45JNI-heilSxL)z`vav zs1vf?6rsYW3PlypEy8Yt^pDDQT&f{4p!AUH472={{Q()Q{!-b`T?7@uT@)3|h+p^; G&hs}q4qVm% literal 0 HcmV?d00001 From 12dd92f67bfbdb5756d789aeaafbd6c066c16c4d Mon Sep 17 00:00:00 2001 From: inksprout Date: Wed, 22 Mar 2023 16:16:11 +1100 Subject: [PATCH 04/18] WIP test --- .../Drivers/FfiIntegrationTests.cs | 274 +++++++++--------- tests/PactNet.Tests/RequestBuilderTests.cs | 9 +- ...ver-integration-MultipartFormDataBody.json | 81 ++++++ 3 files changed, 223 insertions(+), 141 deletions(-) create mode 100644 tests/PactNet.Tests/data/v3-server-integration-MultipartFormDataBody.json diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index 833a330f..c98de6fd 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -1,139 +1,137 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using FluentAssertions; -using PactNet.Drivers; -using PactNet.Interop; -using Xunit; -using Xunit.Abstractions; - -namespace PactNet.Tests.Drivers -{ - /// - /// Happy path integration tests to make sure wwe're calling the Rust FFI library properly with P/Invoke - /// - public class FfiIntegrationTests - { - private readonly ITestOutputHelper output; - - public FfiIntegrationTests(ITestOutputHelper output) - { - this.output = output; - - NativeInterop.LogToBuffer(LevelFilter.Trace); - } - - [Fact] - public async Task HttpInteraction_v3_CreatesPactFile() - { - var driver = new PactDriver(); - - try - { - IHttpPactDriver pact = driver.NewHttpPact("NativeDriverTests-Consumer-V3", - "NativeDriverTests-Provider", - PactSpecification.V3); - - IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); - - interaction.Given("provider state"); +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using PactNet.Drivers; +using PactNet.Interop; +using Xunit; +using Xunit.Abstractions; + +namespace PactNet.Tests.Drivers +{ + /// + /// Happy path integration tests to make sure wwe're calling the Rust FFI library properly with P/Invoke + /// + public class FfiIntegrationTests + { + private readonly ITestOutputHelper output; + + public FfiIntegrationTests(ITestOutputHelper output) + { + this.output = output; + + NativeInterop.LogToBuffer(LevelFilter.Trace); + } + + [Fact] + public async Task HttpInteraction_v3_CreatesPactFile() + { + var driver = new PactDriver(); + + try + { + IHttpPactDriver pact = driver.NewHttpPact("NativeDriverTests-Consumer-V3", + "NativeDriverTests-Provider", + PactSpecification.V3); + + IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); + + interaction.Given("provider state"); interaction.GivenWithParam("state with param", "foo", "bar"); - interaction.WithRequest("POST", "/path"); - //interaction.WithRequestHeader("X-Request-Header", "request1", 0); - //interaction.WithRequestHeader("X-Request-Header", "request2", 1); - interaction.WithQueryParameter("param", "value", 0); - var path = Path.GetFullPath("data/test_file.jpeg"); - Assert.True(File.Exists(path)); - interaction.WithMultipartSingleFileUpload("multipart/form-data", path, "boundary"); - - //interaction.WithRequestBody("application/json", @"{""foo"":42}"); - - interaction.WithResponseStatus((ushort)HttpStatusCode.Created); - //interaction.WithResponseHeader("X-Response-Header", "value1", 0); - //interaction.WithResponseHeader("X-Response-Header", "value2", 1); - //interaction.WithResponseBody("application/json", @"{""foo"":42}"); - - - using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false); - - var client = new HttpClient { BaseAddress = mockServer.Uri }; - client.DefaultRequestHeaders.Add("X-Request-Header", new[] { "request1", "request2" }); - - HttpResponseMessage result = await client.PostAsync("/path?param=value", new StringContent(@"{""foo"":42}", Encoding.UTF8, "application/json")); - //result.StatusCode.Should().Be(HttpStatusCode.Created); - //result.Headers.GetValues("X-Response-Header").Should().BeEquivalentTo("value1", "value2"); - - string content = await result.Content.ReadAsStringAsync(); - //content.Should().Be(@"{""foo"":42}"); - - // mockServer.MockServerMismatches().Should().Be("[]"); - - string logs = mockServer.MockServerLogs(); - // logs.Should().NotBeEmpty(); - - this.output.WriteLine("Mock Server Logs"); - this.output.WriteLine("----------------"); - this.output.WriteLine(logs); - - pact.WritePactFile(Environment.CurrentDirectory); - } - finally - { - this.WriteDriverLogs(driver); - } - - var file = new FileInfo("NativeDriverTests-Consumer-V3-NativeDriverTests-Provider.json"); - file.Exists.Should().BeTrue(); - - string pactContents = File.ReadAllText(file.FullName).TrimEnd(); - string expectedPactContent = File.ReadAllText("data/v3-server-integration.json").TrimEnd(); - //pactContents.Should().Be(expectedPactContent); - } - - private void WriteDriverLogs(IPactDriver pact) - { - this.output.WriteLine(string.Empty); - this.output.WriteLine("Driver Logs"); - this.output.WriteLine("-----------"); - this.output.WriteLine(pact.DriverLogs()); - } - - [Fact] - public void MessageInteraction_v3_CreatesPactFile() - { - var driver = new PactDriver(); - - try - { - IMessagePactDriver pact = driver.NewMessagePact("NativeDriverTests-Consumer-V3", - "NativeDriverTests-Producer", - PactSpecification.V3); - - IMessageInteractionDriver interaction = pact.NewMessageInteraction("a message interaction"); - - interaction.ExpectsToReceive("changed description"); - interaction.WithMetadata("foo", "bar"); - interaction.WithContents("application/json", @"{""foo"":42}", 0); - - string reified = interaction.Reify(); - reified.Should().NotBeNullOrEmpty(); - - interaction.WritePactFile(Environment.CurrentDirectory); - } - finally - { - this.WriteDriverLogs(driver); - } - - var file = new FileInfo("NativeDriverTests-Consumer-V3-NativeDriverTests-Producer.json"); - file.Exists.Should().BeTrue(); - - string pactContents = File.ReadAllText(file.FullName).TrimEnd(); - string expectedPactContent = File.ReadAllText("data/v3-message-integration.json").TrimEnd(); - pactContents.Should().Be(expectedPactContent); - } - } -} + //TODO make this request work for the test + interaction.WithRequest("POST", "/path"); + interaction.WithRequestHeader("X-Request-Header", "request1", 0); + interaction.WithRequestHeader("X-Request-Header", "request2", 1); + interaction.WithQueryParameter("param", "value", 0); + //TODO make this a multipart body + interaction.WithRequestBody("application/json", @"{""foo"":42}"); + + interaction.WithResponseStatus((ushort)HttpStatusCode.Created); + interaction.WithResponseHeader("X-Response-Header", "value1", 0); + interaction.WithResponseHeader("X-Response-Header", "value2", 1); + interaction.WithResponseBody("application/json", @"{""foo"":42}"); + + using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false); + + var client = new HttpClient { BaseAddress = mockServer.Uri }; + client.DefaultRequestHeaders.Add("X-Request-Header", new[] { "request1", "request2" }); + + // Make this multipart for the test + HttpResponseMessage result = await client.PostAsync("/path?param=value", new StringContent(@"{""foo"":42}", Encoding.UTF8, "application/json")); + result.StatusCode.Should().Be(HttpStatusCode.Created); + result.Headers.GetValues("X-Response-Header").Should().BeEquivalentTo("value1", "value2"); + + string content = await result.Content.ReadAsStringAsync(); + content.Should().Be(@"{""foo"":42}"); + + mockServer.MockServerMismatches().Should().Be("[]"); + + string logs = mockServer.MockServerLogs(); + logs.Should().NotBeEmpty(); + + this.output.WriteLine("Mock Server Logs"); + this.output.WriteLine("----------------"); + this.output.WriteLine(logs); + + pact.WritePactFile(Environment.CurrentDirectory); + } + finally + { + this.WriteDriverLogs(driver); + } + + var file = new FileInfo("NativeDriverTests-Consumer-V3-NativeDriverTests-Provider.json"); + file.Exists.Should().BeTrue(); + + string pactContents = File.ReadAllText(file.FullName).TrimEnd(); + string expectedPactContent = File.ReadAllText("data/v3-server-integration.json").TrimEnd(); + pactContents.Should().Be(expectedPactContent); + } + + private void WriteDriverLogs(IPactDriver pact) + { + this.output.WriteLine(string.Empty); + this.output.WriteLine("Driver Logs"); + this.output.WriteLine("-----------"); + this.output.WriteLine(pact.DriverLogs()); + } + + [Fact] + public void MessageInteraction_v3_CreatesPactFile() + { + var driver = new PactDriver(); + + try + { + IMessagePactDriver pact = driver.NewMessagePact("NativeDriverTests-Consumer-V3", + "NativeDriverTests-Producer", + PactSpecification.V3); + + IMessageInteractionDriver interaction = pact.NewMessageInteraction("a message interaction"); + + interaction.ExpectsToReceive("changed description"); + interaction.WithMetadata("foo", "bar"); + interaction.WithContents("application/json", @"{""foo"":42}", 0); + + string reified = interaction.Reify(); + reified.Should().NotBeNullOrEmpty(); + + interaction.WritePactFile(Environment.CurrentDirectory); + } + finally + { + this.WriteDriverLogs(driver); + } + + var file = new FileInfo("NativeDriverTests-Consumer-V3-NativeDriverTests-Producer.json"); + file.Exists.Should().BeTrue(); + + string pactContents = File.ReadAllText(file.FullName).TrimEnd(); + string expectedPactContent = File.ReadAllText("data/v3-message-integration.json").TrimEnd(); + pactContents.Should().Be(expectedPactContent); + } + } +} diff --git a/tests/PactNet.Tests/RequestBuilderTests.cs b/tests/PactNet.Tests/RequestBuilderTests.cs index 200ee7d5..aea61173 100644 --- a/tests/PactNet.Tests/RequestBuilderTests.cs +++ b/tests/PactNet.Tests/RequestBuilderTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Net.Http; using FluentAssertions; using Moq; @@ -198,10 +199,12 @@ public void WillRespond_RequestNotConfigured_ThrowsInvalidOperationException() [Fact] public void WithMultipartSingleFileUpload_AddsRequestBody() - { - this.builder.WithMultipartSingleFileUpload("tests/PactNet.Tests/data","multipart/form-data", "boundary"); + { + var path = Path.GetFullPath("data/test_file.jpeg"); + + this.builder.WithMultipartSingleFileUpload(path,"multipart/form-data", "boundary"); - this.mockDriver.Verify(s => s.WithMultipartSingleFileUpload("tests/PactNet.Tests/data", "multipart/form-data", "boundary")); + this.mockDriver.Verify(s => s.WithMultipartSingleFileUpload(path, "multipart/form-data", "boundary")); } } } diff --git a/tests/PactNet.Tests/data/v3-server-integration-MultipartFormDataBody.json b/tests/PactNet.Tests/data/v3-server-integration-MultipartFormDataBody.json new file mode 100644 index 00000000..9acf00c8 --- /dev/null +++ b/tests/PactNet.Tests/data/v3-server-integration-MultipartFormDataBody.json @@ -0,0 +1,81 @@ +{ + "consumer": { + "name": "NativeDriverTests-Consumer-V3" + }, + "interactions": [ + { + "description": "a sample interaction", + "providerStates": [ + { + "name": "provider state" + }, + { + "name": "state with param", + "params": { + "foo": "bar" + } + } + ], + "request": { + "body": "LS1PM0QwR1FUbG03RjFNWEZVDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImJvdW5kYXJ5IjsgZmlsZW5hbWU9InRlc3RfZmlsZS5qcGVnIg0KQ29udGVudC1UeXBlOiBpbWFnZS9qcGVnDQoNCv/Y/+AAEEpGSUYAAQEAAAEAAQAA/+IB2ElDQ19QUk9GSUxFAAEBAAAByAAAAAAEMAAAbW50clJHQiBYWVogAAAAAAAAAAAAAAAAYWNzcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAPbWAAEAAAAA0y0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJZGVzYwAAAPAAAAAkclhZWgAAARQAAAAUZ1hZWgAAASgAAAAUYlhZWgAAATwAAAAUd3RwdAAAAVAAAAAUclRSQwAAAWQAAAAoZ1RSQwAAAWQAAAAoYlRSQwAAAWQAAAAoY3BydAAAAYwAAAA8bWx1YwAAAAAAAAABAAAADGVuVVMAAAAIAAAAHABzAFIARwBCWFlaIAAAAAAAAG+iAAA49QAAA5BYWVogAAAAAAAAYpkAALeFAAAY2lhZWiAAAAAAAAAkoAAAD4QAALbPWFlaIAAAAAAAAPbWAAEAAAAA0y1wYXJhAAAAAAAEAAAAAmZmAADypwAADVkAABPQAAAKWwAAAAAAAAAAbWx1YwAAAAAAAAABAAAADGVuVVMAAAAgAAAAHABHAG8AbwBnAGwAZQAgAEkAbgBjAC4AIAAyADAAMQA2/9sAQwABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB/9sAQwEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB/8AAEQgAAQABAwERAAIRAQMRAf/EABQAAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAACQr/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwAupQIw/9kNCi0tTzNEMEdRVGxtN0YxTVhGVS0tDQo=", + "headers": { + "Content-Type": "multipart/form-data; boundary=O3D0GQTlm7F1MXFU" + }, + "matchingRules": { + "body": { + "$.boundary": { + "combine": "AND", + "matchers": [ + { + "match": "contentType", + "value": "multipart/form-data" + } + ] + } + }, + "header": { + "Content-Type": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "multipart/form-data;(\\s*charset=[^;]*;)?\\s*boundary=.*" + } + ] + } + }, + "query": {} + }, + "method": "POST", + "path": "/path", + "query": { + "param": [ + "value" + ] + } + }, + "response": { + "body": { + "foo": 42 + }, + "headers": { + "Content-Type": "application/json", + "X-Response-Header": "value1, value2" + }, + "status": 201 + } + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.0", + "models": "1.0.4" + }, + "pactSpecification": { + "version": "3.0.0" + } + }, + "provider": { + "name": "NativeDriverTests-Provider" + } +} From 1a95e95be9f3e358eafa13faaa5e3abf04e80b70 Mon Sep 17 00:00:00 2001 From: Inksprout Date: Thu, 23 Mar 2023 12:30:29 +1100 Subject: [PATCH 05/18] test working example --- src/PactNet/Drivers/HttpInteractionDriver.cs | 6 +-- .../Drivers/FfiIntegrationTests.cs | 53 ++++++++++++++----- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/PactNet/Drivers/HttpInteractionDriver.cs b/src/PactNet/Drivers/HttpInteractionDriver.cs index c5b69b2a..b935ba13 100644 --- a/src/PactNet/Drivers/HttpInteractionDriver.cs +++ b/src/PactNet/Drivers/HttpInteractionDriver.cs @@ -99,10 +99,10 @@ public void WithResponseBody(string contentType, string body) => NativeInterop.WithBody(this.interaction, InteractionPart.Response, contentType, body).CheckInteropSuccess(); /// - /// Set the response body to multipart/form-data for file upload - /// - /// path to file being uploaded + /// Set the request body to multipart/form-data for file upload + /// /// Content type override + /// path to file being uploaded /// string used as boundary of the part public void WithMultipartSingleFileUpload(string contentType, string filePath, string partDelimiter) => NativeInterop.WithMultipartSingleFileUpload(this.interaction, InteractionPart.Request, contentType, filePath, partDelimiter).CheckInteropSuccess(); diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index c98de6fd..4746ca69 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -2,6 +2,7 @@ using System.IO; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using FluentAssertions; @@ -40,14 +41,17 @@ public async Task HttpInteraction_v3_CreatesPactFile() IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); interaction.Given("provider state"); - interaction.GivenWithParam("state with param", "foo", "bar"); - //TODO make this request work for the test + //interaction.GivenWithParam("state with param", "foo", "bar"); interaction.WithRequest("POST", "/path"); - interaction.WithRequestHeader("X-Request-Header", "request1", 0); - interaction.WithRequestHeader("X-Request-Header", "request2", 1); - interaction.WithQueryParameter("param", "value", 0); - //TODO make this a multipart body - interaction.WithRequestBody("application/json", @"{""foo"":42}"); + //interaction.WithRequestHeader("X-Request-Header", "request1", 0); + //interaction.WithRequestHeader("X-Request-Header", "request2", 1); + //interaction.WithQueryParameter("param", "value", 0); + //interaction.WithRequestBody("application/json", @"{""foo"":42}"); + var path = Path.GetFullPath("data/test_file.jpeg"); + Assert.True(File.Exists(path)); + + + interaction.WithMultipartSingleFileUpload("application/octet-stream", path, "file"); interaction.WithResponseStatus((ushort)HttpStatusCode.Created); interaction.WithResponseHeader("X-Response-Header", "value1", 0); @@ -56,20 +60,43 @@ public async Task HttpInteraction_v3_CreatesPactFile() using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false); - var client = new HttpClient { BaseAddress = mockServer.Uri }; - client.DefaultRequestHeaders.Add("X-Request-Header", new[] { "request1", "request2" }); + var client = new HttpClient { BaseAddress = mockServer.Uri }; + //client.DefaultRequestHeaders.Add("X-Request-Header", new[] { "request1", "request2" }); + + using var fileStream = new FileStream("data/test_file.jpeg", FileMode.Open, FileAccess.Read); + + // Create the content + var upload = new MultipartFormDataContent(); + upload.Headers.ContentType.MediaType = "multipart/form-data"; + + var fileContent = new StreamContent(fileStream); + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg"); + + // Set the Content-Disposition header with the filename* + var fileName = Path.GetFileName(path); + var fileNameBytes = Encoding.UTF8.GetBytes(fileName); + var encodedFileName = Convert.ToBase64String(fileNameBytes); + upload.Add(fileContent, "file", fileName); + upload.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") + { + Name = "file", + FileName = fileName, + FileNameStar = $"utf-8''{encodedFileName}" + }; // Make this multipart for the test - HttpResponseMessage result = await client.PostAsync("/path?param=value", new StringContent(@"{""foo"":42}", Encoding.UTF8, "application/json")); - result.StatusCode.Should().Be(HttpStatusCode.Created); - result.Headers.GetValues("X-Response-Header").Should().BeEquivalentTo("value1", "value2"); + HttpResponseMessage result = await client.PostAsync("/path", upload); + // result.StatusCode.Should().Be(HttpStatusCode.Created); + //result.Headers.GetValues("X-Response-Header").Should().BeEquivalentTo("value1", "value2"); + + string logs = mockServer.MockServerLogs(); + string content = await result.Content.ReadAsStringAsync(); content.Should().Be(@"{""foo"":42}"); mockServer.MockServerMismatches().Should().Be("[]"); - string logs = mockServer.MockServerLogs(); logs.Should().NotBeEmpty(); this.output.WriteLine("Mock Server Logs"); From d4acbe0febf679d92dc737bd523a4cb2d4e46fca Mon Sep 17 00:00:00 2001 From: Inksprout Date: Thu, 23 Mar 2023 16:55:33 +1100 Subject: [PATCH 06/18] working test for multipart/form-data and refactoring --- src/PactNet/Drivers/HttpInteractionDriver.cs | 6 +- src/PactNet/Drivers/IHttpInteractionDriver.cs | 4 +- src/PactNet/Interop/NativeInterop.cs | 2 +- src/PactNet/Interop/StringResult.cs | 40 ++++++- src/PactNet/RequestBuilder.cs | 8 +- .../Drivers/FfiIntegrationTests.cs | 108 ++++++++++++++---- tests/PactNet.Tests/RequestBuilderTests.cs | 4 +- ...ver-integration-MultipartFormDataBody.json | 28 ++--- 8 files changed, 142 insertions(+), 58 deletions(-) diff --git a/src/PactNet/Drivers/HttpInteractionDriver.cs b/src/PactNet/Drivers/HttpInteractionDriver.cs index b935ba13..fcdaefcc 100644 --- a/src/PactNet/Drivers/HttpInteractionDriver.cs +++ b/src/PactNet/Drivers/HttpInteractionDriver.cs @@ -103,8 +103,8 @@ public void WithResponseBody(string contentType, string body) /// /// Content type override /// path to file being uploaded - /// string used as boundary of the part - public void WithMultipartSingleFileUpload(string contentType, string filePath, string partDelimiter) - => NativeInterop.WithMultipartSingleFileUpload(this.interaction, InteractionPart.Request, contentType, filePath, partDelimiter).CheckInteropSuccess(); + /// the name of the mime part being uploaded + public void WithMultipartSingleFileUpload(string contentType, string filePath, string mimePartName) + => NativeInterop.WithMultipartSingleFileUpload(this.interaction, InteractionPart.Request, contentType, filePath, mimePartName).CheckInteropSuccess(); } } diff --git a/src/PactNet/Drivers/IHttpInteractionDriver.cs b/src/PactNet/Drivers/IHttpInteractionDriver.cs index 8a598a6b..7fa68e9e 100644 --- a/src/PactNet/Drivers/IHttpInteractionDriver.cs +++ b/src/PactNet/Drivers/IHttpInteractionDriver.cs @@ -62,7 +62,7 @@ internal interface IHttpInteractionDriver : IProviderStateDriver /// /// path to file being uploaded /// Content type override - /// string used as boundary of the part - void WithMultipartSingleFileUpload(string filePath, string contentType, string partDelimiter); + /// the name of the mime part being uploaded + void WithMultipartSingleFileUpload(string filePath, string contentType, string mimePartName); } } diff --git a/src/PactNet/Interop/NativeInterop.cs b/src/PactNet/Interop/NativeInterop.cs index 330ac798..f49bd958 100644 --- a/src/PactNet/Interop/NativeInterop.cs +++ b/src/PactNet/Interop/NativeInterop.cs @@ -64,7 +64,7 @@ internal static class NativeInterop public static extern bool WithBody(InteractionHandle interaction, InteractionPart part, string contentType, string body); [DllImport(DllName, EntryPoint = "pactffi_with_multipart_file")] - public static extern StringResult WithMultipartSingleFileUpload(InteractionHandle interaction, InteractionPart part, string contentType, string filePath, string partDelimiter ); + public static extern StringResult WithMultipartSingleFileUpload(InteractionHandle interaction, InteractionPart part, string contentType, string filePath, string mimePartName ); [DllImport(DllName, EntryPoint = "pactffi_free_string")] public static extern void FreeString(IntPtr s); diff --git a/src/PactNet/Interop/StringResult.cs b/src/PactNet/Interop/StringResult.cs index 42a8210d..1fe9706f 100644 --- a/src/PactNet/Interop/StringResult.cs +++ b/src/PactNet/Interop/StringResult.cs @@ -2,37 +2,67 @@ using System.Runtime.InteropServices; namespace PactNet.Interop { + /// + /// Represents the result of a string operation. + /// [StructLayout(LayoutKind.Explicit)] public struct StringResult { + /// + /// The possible tag values. + /// public enum Tag { + /// + /// Indicates that the string operation succeeded. + /// StringResult_Ok, + + /// + /// Indicates that the string operation failed. + /// StringResult_Failed, }; + /// + /// The tag value + /// [FieldOffset(0)] public Tag tag; + /// + /// The body of the StringResult when the tag value is StringResult_Ok + /// [FieldOffset(8)] public StringResult_Ok_Body ok; + /// + /// The body of the StringResult when the tag value is StringResult_Failed + /// [FieldOffset(8)] public StringResult_Failed_Body failed; } - + /// + /// The OK body + /// [StructLayout(LayoutKind.Sequential)] public struct StringResult_Ok_Body { + /// + /// A pointer to the string result + /// public IntPtr _0; } + /// + /// The failed Body + /// [StructLayout(LayoutKind.Sequential)] public struct StringResult_Failed_Body { + /// + /// A pointer to the error message. + /// public IntPtr _0; } - - - -} \ No newline at end of file +} diff --git a/src/PactNet/RequestBuilder.cs b/src/PactNet/RequestBuilder.cs index 422037da..b6b2e5be 100644 --- a/src/PactNet/RequestBuilder.cs +++ b/src/PactNet/RequestBuilder.cs @@ -249,7 +249,7 @@ IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSet /// path to the file being uploaded /// Fluent builder IRequestBuilderV3 IRequestBuilderV3.WithMultipartSingleFileUpload(string filePath) - => this.WithMultipartSingleFileUpload(filePath, "multipart/form-data", "partDelimiter"); + => this.WithMultipartSingleFileUpload(filePath, "multipart/form-data", "file"); /// /// A pre-formatted body which should be used as-is for the request @@ -405,11 +405,11 @@ internal RequestBuilder WithJsonBody(dynamic body, JsonSerializerSettings settin /// /// path to file being uploaded /// Content type override - /// string used as boundary of the part + /// The name of the mime part being uploaded /// Fluent builder - internal RequestBuilder WithMultipartSingleFileUpload(string filePath, string contentType, string partDelimiter) + internal RequestBuilder WithMultipartSingleFileUpload(string filePath, string contentType, string mimePartName) { - this.driver.WithMultipartSingleFileUpload(filePath, contentType, partDelimiter); + this.driver.WithMultipartSingleFileUpload(filePath, contentType, mimePartName); return this; } /// diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index 4746ca69..3b3aad57 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using FluentAssertions; +using Newtonsoft.Json.Linq; using PactNet.Drivers; using PactNet.Interop; using Xunit; @@ -25,32 +26,26 @@ public FfiIntegrationTests(ITestOutputHelper output) this.output = output; NativeInterop.LogToBuffer(LevelFilter.Trace); - } - + } + [Fact] - public async Task HttpInteraction_v3_CreatesPactFile() + public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() { var driver = new PactDriver(); try { IHttpPactDriver pact = driver.NewHttpPact("NativeDriverTests-Consumer-V3", - "NativeDriverTests-Provider", - PactSpecification.V3); - + "NativeDriverTests-Provider-Multipart", + PactSpecification.V3); + IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); interaction.Given("provider state"); - //interaction.GivenWithParam("state with param", "foo", "bar"); - interaction.WithRequest("POST", "/path"); - //interaction.WithRequestHeader("X-Request-Header", "request1", 0); - //interaction.WithRequestHeader("X-Request-Header", "request2", 1); - //interaction.WithQueryParameter("param", "value", 0); - //interaction.WithRequestBody("application/json", @"{""foo"":42}"); + interaction.WithRequest("POST", "/path"); var path = Path.GetFullPath("data/test_file.jpeg"); Assert.True(File.Exists(path)); - interaction.WithMultipartSingleFileUpload("application/octet-stream", path, "file"); interaction.WithResponseStatus((ushort)HttpStatusCode.Created); @@ -61,18 +56,15 @@ public async Task HttpInteraction_v3_CreatesPactFile() using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false); var client = new HttpClient { BaseAddress = mockServer.Uri }; - //client.DefaultRequestHeaders.Add("X-Request-Header", new[] { "request1", "request2" }); using var fileStream = new FileStream("data/test_file.jpeg", FileMode.Open, FileAccess.Read); - // Create the content var upload = new MultipartFormDataContent(); upload.Headers.ContentType.MediaType = "multipart/form-data"; var fileContent = new StreamContent(fileStream); fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg"); - // Set the Content-Disposition header with the filename* var fileName = Path.GetFileName(path); var fileNameBytes = Encoding.UTF8.GetBytes(fileName); var encodedFileName = Convert.ToBase64String(fileNameBytes); @@ -84,13 +76,10 @@ public async Task HttpInteraction_v3_CreatesPactFile() FileNameStar = $"utf-8''{encodedFileName}" }; - // Make this multipart for the test HttpResponseMessage result = await client.PostAsync("/path", upload); - // result.StatusCode.Should().Be(HttpStatusCode.Created); - //result.Headers.GetValues("X-Response-Header").Should().BeEquivalentTo("value1", "value2"); - - string logs = mockServer.MockServerLogs(); + result.StatusCode.Should().Be(HttpStatusCode.Created); + string logs = mockServer.MockServerLogs(); string content = await result.Content.ReadAsStringAsync(); content.Should().Be(@"{""foo"":42}"); @@ -106,6 +95,83 @@ public async Task HttpInteraction_v3_CreatesPactFile() pact.WritePactFile(Environment.CurrentDirectory); } finally + { + this.WriteDriverLogs(driver); + } + // The body and boundry will be different, so test the header and matching rules are multipart/form-data + var file = new FileInfo("NativeDriverTests-Consumer-V3-NativeDriverTests-Provider-Multipart.json"); + file.Exists.Should().BeTrue(); + + string pactContents = File.ReadAllText(file.FullName).TrimEnd(); + JObject pactObject = JObject.Parse(pactContents); + + string expectedPactContent = File.ReadAllText("data/v3-server-integration-MultipartFormDataBody.json").TrimEnd(); + JObject expectedPactObject = JObject.Parse(pactContents); + + + string contentTypeHeader = (string)pactObject["interactions"][0]["request"]["headers"]["Content-Type"]; + Assert.Contains("multipart/form-data;", contentTypeHeader); + + + JArray integrationsArray = (JArray)pactObject["interactions"]; + JToken matchingRules = integrationsArray.First["request"]["matchingRules"]; + + JArray expecteIntegrationsArray = (JArray)expectedPactObject["interactions"]; + JToken expectedMatchingRules = expecteIntegrationsArray.First["request"]["matchingRules"]; + + Assert.True(JToken.DeepEquals(matchingRules, expectedMatchingRules)); + } + + [Fact] + public async Task HttpInteraction_v3_CreatesPactFile() + { + var driver = new PactDriver(); + + try + { + IHttpPactDriver pact = driver.NewHttpPact("NativeDriverTests-Consumer-V3", + "NativeDriverTests-Provider", + PactSpecification.V3); + + IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); + + interaction.Given("provider state"); + interaction.GivenWithParam("state with param", "foo", "bar"); + interaction.WithRequest("POST", "/path"); + interaction.WithRequestHeader("X-Request-Header", "request1", 0); + interaction.WithRequestHeader("X-Request-Header", "request2", 1); + interaction.WithQueryParameter("param", "value", 0); + interaction.WithRequestBody("application/json", @"{""foo"":42}"); + + interaction.WithResponseStatus((ushort)HttpStatusCode.Created); + interaction.WithResponseHeader("X-Response-Header", "value1", 0); + interaction.WithResponseHeader("X-Response-Header", "value2", 1); + interaction.WithResponseBody("application/json", @"{""foo"":42}"); + + using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false); + + var client = new HttpClient { BaseAddress = mockServer.Uri }; + client.DefaultRequestHeaders.Add("X-Request-Header", new[] { "request1", "request2" }); + + HttpResponseMessage result = await client.PostAsync("/path?param=value", new StringContent(@"{""foo"":42}", Encoding.UTF8, "application/json")); + result.StatusCode.Should().Be(HttpStatusCode.Created); + result.Headers.GetValues("X-Response-Header").Should().BeEquivalentTo("value1", "value2"); + + string content = await result.Content.ReadAsStringAsync(); + content.Should().Be(@"{""foo"":42}"); + + mockServer.MockServerMismatches().Should().Be("[]"); + + string logs = mockServer.MockServerLogs(); + logs.Should().NotBeEmpty(); + + this.output.WriteLine("Mock Server Logs"); + this.output.WriteLine("----------------"); + this.output.WriteLine(logs); + + pact.WritePactFile(Environment.CurrentDirectory); + } + finally { this.WriteDriverLogs(driver); } diff --git a/tests/PactNet.Tests/RequestBuilderTests.cs b/tests/PactNet.Tests/RequestBuilderTests.cs index aea61173..0af4719b 100644 --- a/tests/PactNet.Tests/RequestBuilderTests.cs +++ b/tests/PactNet.Tests/RequestBuilderTests.cs @@ -202,9 +202,9 @@ public void WithMultipartSingleFileUpload_AddsRequestBody() { var path = Path.GetFullPath("data/test_file.jpeg"); - this.builder.WithMultipartSingleFileUpload(path,"multipart/form-data", "boundary"); + this.builder.WithMultipartSingleFileUpload(path,"multipart/form-data", "file"); - this.mockDriver.Verify(s => s.WithMultipartSingleFileUpload(path, "multipart/form-data", "boundary")); + this.mockDriver.Verify(s => s.WithMultipartSingleFileUpload(path, "multipart/form-data", "file")); } } } diff --git a/tests/PactNet.Tests/data/v3-server-integration-MultipartFormDataBody.json b/tests/PactNet.Tests/data/v3-server-integration-MultipartFormDataBody.json index 9acf00c8..ddf47e55 100644 --- a/tests/PactNet.Tests/data/v3-server-integration-MultipartFormDataBody.json +++ b/tests/PactNet.Tests/data/v3-server-integration-MultipartFormDataBody.json @@ -8,27 +8,21 @@ "providerStates": [ { "name": "provider state" - }, - { - "name": "state with param", - "params": { - "foo": "bar" - } } ], "request": { - "body": "LS1PM0QwR1FUbG03RjFNWEZVDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImJvdW5kYXJ5IjsgZmlsZW5hbWU9InRlc3RfZmlsZS5qcGVnIg0KQ29udGVudC1UeXBlOiBpbWFnZS9qcGVnDQoNCv/Y/+AAEEpGSUYAAQEAAAEAAQAA/+IB2ElDQ19QUk9GSUxFAAEBAAAByAAAAAAEMAAAbW50clJHQiBYWVogAAAAAAAAAAAAAAAAYWNzcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAPbWAAEAAAAA0y0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJZGVzYwAAAPAAAAAkclhZWgAAARQAAAAUZ1hZWgAAASgAAAAUYlhZWgAAATwAAAAUd3RwdAAAAVAAAAAUclRSQwAAAWQAAAAoZ1RSQwAAAWQAAAAoYlRSQwAAAWQAAAAoY3BydAAAAYwAAAA8bWx1YwAAAAAAAAABAAAADGVuVVMAAAAIAAAAHABzAFIARwBCWFlaIAAAAAAAAG+iAAA49QAAA5BYWVogAAAAAAAAYpkAALeFAAAY2lhZWiAAAAAAAAAkoAAAD4QAALbPWFlaIAAAAAAAAPbWAAEAAAAA0y1wYXJhAAAAAAAEAAAAAmZmAADypwAADVkAABPQAAAKWwAAAAAAAAAAbWx1YwAAAAAAAAABAAAADGVuVVMAAAAgAAAAHABHAG8AbwBnAGwAZQAgAEkAbgBjAC4AIAAyADAAMQA2/9sAQwABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB/9sAQwEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB/8AAEQgAAQABAwERAAIRAQMRAf/EABQAAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAACQr/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwAupQIw/9kNCi0tTzNEMEdRVGxtN0YxTVhGVS0tDQo=", + "body": "LS1MQ1NqcTNxdWtmYmc2WjJTDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZpbGUiOyBmaWxlbmFtZT0idGVzdF9maWxlLmpwZWciDQpDb250ZW50LVR5cGU6IGltYWdlL2pwZWcNCg0K/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAABv/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAVAQEBAAAAAAAAAAAAAAAAAAAJCv/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AC6lAjD/2Q0KLS1MQ1NqcTNxdWtmYmc2WjJTLS0NCg==", "headers": { - "Content-Type": "multipart/form-data; boundary=O3D0GQTlm7F1MXFU" + "Content-Type": "multipart/form-data; boundary=LCSjq3qukfbg6Z2S" }, "matchingRules": { "body": { - "$.boundary": { + "$.file": { "combine": "AND", "matchers": [ { "match": "contentType", - "value": "multipart/form-data" + "value": "application/octet-stream" } ] } @@ -43,16 +37,10 @@ } ] } - }, - "query": {} + } }, "method": "POST", - "path": "/path", - "query": { - "param": [ - "value" - ] - } + "path": "/path" }, "response": { "body": { @@ -76,6 +64,6 @@ } }, "provider": { - "name": "NativeDriverTests-Provider" + "name": "NativeDriverTests-Provider-Multipart" } -} +} \ No newline at end of file From 22f05d4bd1034862e21c963e498161a234bc3f5c Mon Sep 17 00:00:00 2001 From: Inksprout Date: Fri, 24 Mar 2023 11:10:34 +1100 Subject: [PATCH 07/18] include the test file --- tests/PactNet.Tests/PactNet.Tests.csproj | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/PactNet.Tests/PactNet.Tests.csproj b/tests/PactNet.Tests/PactNet.Tests.csproj index eee03509..b79b54da 100644 --- a/tests/PactNet.Tests/PactNet.Tests.csproj +++ b/tests/PactNet.Tests/PactNet.Tests.csproj @@ -42,4 +42,13 @@ + + + PreserveNewest + + + PreserveNewest + + + From b41dfc0a8e12324ab92223a062e023bfa08e25d5 Mon Sep 17 00:00:00 2001 From: Inksprout Date: Fri, 24 Mar 2023 11:55:59 +1100 Subject: [PATCH 08/18] change the types --- .../PactNet.Tests/Drivers/FfiIntegrationTests.cs | 2 +- tests/PactNet.Tests/Drivers/test_file.jpeg | Bin 760 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 tests/PactNet.Tests/Drivers/test_file.jpeg diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index 3b3aad57..c6e83f5b 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -63,7 +63,7 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() upload.Headers.ContentType.MediaType = "multipart/form-data"; var fileContent = new StreamContent(fileStream); - fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg"); + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream"); var fileName = Path.GetFileName(path); var fileNameBytes = Encoding.UTF8.GetBytes(fileName); diff --git a/tests/PactNet.Tests/Drivers/test_file.jpeg b/tests/PactNet.Tests/Drivers/test_file.jpeg deleted file mode 100644 index e9164f1ca8e1f6febbe52e16e4f1fcc1aa97a2c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 760 zcmbu7y-EW?6ot=ZMNEj8#RwKDwn&l25G}+aAY?INMKG)iMhi(cKTZDFM0^45ENtyW z5Z^#s3ky31L1JvgClEo~^-PwqQ6d((!|Z&Vd-u+rnd@|%SA*6_;a@3oa)mAGDio#$+Yio2E`Kl4F$tE)1~Mv}OD;W0JM%zT8WitTKc0Db62233SSNERg-AOZV=b4|E%;_ta zfsadIG6zgQ0D;|}kDotk)98w@t_DX1Rg|E?Kpht1qy;1}k45JNI-heilSxL)z`vav zs1vf?6rsYW3PlypEy8Yt^pDDQT&f{4p!AUH472={{Q()Q{!-b`T?7@uT@)3|h+p^; G&hs}q4qVm% From 39b1304fcf5dd3f18891a7585c43b8f02225d719 Mon Sep 17 00:00:00 2001 From: Inksprout Date: Fri, 24 Mar 2023 12:03:43 +1100 Subject: [PATCH 09/18] change to type needed for ubuntu --- tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index c6e83f5b..4521eb14 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -46,7 +46,7 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() var path = Path.GetFullPath("data/test_file.jpeg"); Assert.True(File.Exists(path)); - interaction.WithMultipartSingleFileUpload("application/octet-stream", path, "file"); + interaction.WithMultipartSingleFileUpload("image/jpeg", path, "file"); interaction.WithResponseStatus((ushort)HttpStatusCode.Created); interaction.WithResponseHeader("X-Response-Header", "value1", 0); @@ -63,7 +63,7 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() upload.Headers.ContentType.MediaType = "multipart/form-data"; var fileContent = new StreamContent(fileStream); - fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream"); + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg"); var fileName = Path.GetFileName(path); var fileNameBytes = Encoding.UTF8.GetBytes(fileName); From dad71c82edcb7c0995bd54e106de57fefffd55da Mon Sep 17 00:00:00 2001 From: Inksprout Date: Fri, 24 Mar 2023 12:27:30 +1100 Subject: [PATCH 10/18] set the content type dynamically based on OS --- tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index 4521eb14..d6a9477d 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -3,6 +3,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using FluentAssertions; @@ -39,14 +40,16 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() "NativeDriverTests-Provider-Multipart", PactSpecification.V3); - IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); + IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); + + string contentType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "application/octet-stream" : "image/jpeg"; interaction.Given("provider state"); interaction.WithRequest("POST", "/path"); var path = Path.GetFullPath("data/test_file.jpeg"); Assert.True(File.Exists(path)); - interaction.WithMultipartSingleFileUpload("image/jpeg", path, "file"); + interaction.WithMultipartSingleFileUpload(contentType, path, "file"); interaction.WithResponseStatus((ushort)HttpStatusCode.Created); interaction.WithResponseHeader("X-Response-Header", "value1", 0); @@ -63,7 +66,8 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() upload.Headers.ContentType.MediaType = "multipart/form-data"; var fileContent = new StreamContent(fileStream); - fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg"); + + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); var fileName = Path.GetFileName(path); var fileNameBytes = Encoding.UTF8.GetBytes(fileName); From 058e67f8bcb5cb70e8985ddcc7c33101cb8beb3a Mon Sep 17 00:00:00 2001 From: Inksprout Date: Fri, 24 Mar 2023 13:12:29 +1100 Subject: [PATCH 11/18] cleanup --- src/PactNet.Abstractions/IRequestBuilder.cs | 2 +- src/PactNet/Drivers/IHttpInteractionDriver.cs | 1 - .../Drivers/InteropActionExtensions.cs | 2 +- src/PactNet/Interop/StringResult.cs | 42 +++---------------- src/PactNet/RequestBuilder.cs | 4 +- .../Drivers/FfiIntegrationTests.cs | 3 +- 6 files changed, 10 insertions(+), 44 deletions(-) diff --git a/src/PactNet.Abstractions/IRequestBuilder.cs b/src/PactNet.Abstractions/IRequestBuilder.cs index f5d1cc0c..da9e78d5 100644 --- a/src/PactNet.Abstractions/IRequestBuilder.cs +++ b/src/PactNet.Abstractions/IRequestBuilder.cs @@ -209,7 +209,7 @@ public interface IRequestBuilderV3 /// /// A Multipart body containing a single part, which is an uploaded file /// - /// Request body + /// The absolute path of the file being uploaded /// Fluent builder IRequestBuilderV3 WithMultipartSingleFileUpload(string filePath); diff --git a/src/PactNet/Drivers/IHttpInteractionDriver.cs b/src/PactNet/Drivers/IHttpInteractionDriver.cs index 7fa68e9e..6d967c60 100644 --- a/src/PactNet/Drivers/IHttpInteractionDriver.cs +++ b/src/PactNet/Drivers/IHttpInteractionDriver.cs @@ -56,7 +56,6 @@ internal interface IHttpInteractionDriver : IProviderStateDriver /// Serialised body void WithResponseBody(string contentType, string body); - /// /// Set the response body for a single file to be uploaded as a multipart/form-data content type /// diff --git a/src/PactNet/Drivers/InteropActionExtensions.cs b/src/PactNet/Drivers/InteropActionExtensions.cs index c83eff13..877557a5 100644 --- a/src/PactNet/Drivers/InteropActionExtensions.cs +++ b/src/PactNet/Drivers/InteropActionExtensions.cs @@ -24,7 +24,7 @@ public static void CheckInteropSuccess(this bool success) public static void CheckInteropSuccess(this StringResult success) { - if (success.tag!=StringResult.Tag.StringResult_Ok) + if (success.tag != StringResult.Tag.StringResult_Ok) { string errorMsg = Marshal.PtrToStringAnsi(success.failed._0); throw new PactFailureException($"Unable to perform the given action. The interop call returned failure: {errorMsg}"); diff --git a/src/PactNet/Interop/StringResult.cs b/src/PactNet/Interop/StringResult.cs index 1fe9706f..109f7984 100644 --- a/src/PactNet/Interop/StringResult.cs +++ b/src/PactNet/Interop/StringResult.cs @@ -2,67 +2,35 @@ using System.Runtime.InteropServices; namespace PactNet.Interop { - /// - /// Represents the result of a string operation. - /// + [StructLayout(LayoutKind.Explicit)] - public struct StringResult + internal struct StringResult { - /// - /// The possible tag values. - /// public enum Tag { - /// - /// Indicates that the string operation succeeded. - /// StringResult_Ok, - - /// - /// Indicates that the string operation failed. - /// StringResult_Failed, }; - /// - /// The tag value - /// [FieldOffset(0)] public Tag tag; - /// - /// The body of the StringResult when the tag value is StringResult_Ok - /// [FieldOffset(8)] public StringResult_Ok_Body ok; - /// - /// The body of the StringResult when the tag value is StringResult_Failed - /// [FieldOffset(8)] public StringResult_Failed_Body failed; } - /// - /// The OK body - /// + [StructLayout(LayoutKind.Sequential)] - public struct StringResult_Ok_Body + internal struct StringResult_Ok_Body { - /// - /// A pointer to the string result - /// public IntPtr _0; } - /// - /// The failed Body - /// [StructLayout(LayoutKind.Sequential)] - public struct StringResult_Failed_Body + internal struct StringResult_Failed_Body { - /// - /// A pointer to the error message. - /// public IntPtr _0; } } diff --git a/src/PactNet/RequestBuilder.cs b/src/PactNet/RequestBuilder.cs index b6b2e5be..af5fd5b4 100644 --- a/src/PactNet/RequestBuilder.cs +++ b/src/PactNet/RequestBuilder.cs @@ -244,9 +244,9 @@ IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSet => this.WithJsonBody(body, settings, contentType); /// - /// Set a body which is multipart/form-data with one part which is a file upload + /// Set a body which is multipart/form-data but contains only one part, which is a file upload /// - /// path to the file being uploaded + /// Path to the file being uploaded /// Fluent builder IRequestBuilderV3 IRequestBuilderV3.WithMultipartSingleFileUpload(string filePath) => this.WithMultipartSingleFileUpload(filePath, "multipart/form-data", "file"); diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index d6a9477d..8d673a3c 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -66,8 +66,7 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() upload.Headers.ContentType.MediaType = "multipart/form-data"; var fileContent = new StreamContent(fileStream); - - fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg"); var fileName = Path.GetFileName(path); var fileNameBytes = Encoding.UTF8.GetBytes(fileName); From 5b5296ba03364ee035f665f428e105a0d5d3594d Mon Sep 17 00:00:00 2001 From: Inksprout Date: Mon, 27 Mar 2023 15:18:40 +1100 Subject: [PATCH 12/18] refactors --- src/PactNet.Abstractions/IRequestBuilder.cs | 13 +++++++------ src/PactNet/RequestBuilder.cs | 7 ++++--- tests/PactNet.Tests/RequestBuilderTests.cs | 4 ++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/PactNet.Abstractions/IRequestBuilder.cs b/src/PactNet.Abstractions/IRequestBuilder.cs index da9e78d5..6bf3dc95 100644 --- a/src/PactNet.Abstractions/IRequestBuilder.cs +++ b/src/PactNet.Abstractions/IRequestBuilder.cs @@ -207,11 +207,12 @@ public interface IRequestBuilderV3 IRequestBuilderV3 WithBody(string body, string contentType); /// - /// A Multipart body containing a single part, which is an uploaded file - /// - /// The absolute path of the file being uploaded - /// Fluent builder - IRequestBuilderV3 WithMultipartSingleFileUpload(string filePath); + /// Set a body which is multipart/form-data but contains only one part, which is a file upload + /// + /// The content type of the file being uploaded + /// Path to the file being uploaded + /// Fluent builder /// Response builder - IResponseBuilderV3 WillRespond(); + IResponseBuilderV3 WillRespond(); } } diff --git a/src/PactNet/RequestBuilder.cs b/src/PactNet/RequestBuilder.cs index af5fd5b4..d6526ddf 100644 --- a/src/PactNet/RequestBuilder.cs +++ b/src/PactNet/RequestBuilder.cs @@ -245,11 +245,12 @@ IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSet /// /// Set a body which is multipart/form-data but contains only one part, which is a file upload - /// + /// + /// The content type of the file being uploaded /// Path to the file being uploaded /// Fluent builder - IRequestBuilderV3 IRequestBuilderV3.WithMultipartSingleFileUpload(string filePath) - => this.WithMultipartSingleFileUpload(filePath, "multipart/form-data", "file"); + IRequestBuilderV3 IRequestBuilderV3.WithMultipartSingleFileUpload(string contentType, string filePath) + => this.WithMultipartSingleFileUpload(contentType, filePath, "file"); /// /// A pre-formatted body which should be used as-is for the request diff --git a/tests/PactNet.Tests/RequestBuilderTests.cs b/tests/PactNet.Tests/RequestBuilderTests.cs index 0af4719b..2117cef4 100644 --- a/tests/PactNet.Tests/RequestBuilderTests.cs +++ b/tests/PactNet.Tests/RequestBuilderTests.cs @@ -202,9 +202,9 @@ public void WithMultipartSingleFileUpload_AddsRequestBody() { var path = Path.GetFullPath("data/test_file.jpeg"); - this.builder.WithMultipartSingleFileUpload(path,"multipart/form-data", "file"); + this.builder.WithMultipartSingleFileUpload("image/jpeg", path, "file"); - this.mockDriver.Verify(s => s.WithMultipartSingleFileUpload(path, "multipart/form-data", "file")); + this.mockDriver.Verify(s => s.WithMultipartSingleFileUpload("image/jpeg", path, "file")); } } } From ab813e6a32bee051da7c72152132c0d034f57771 Mon Sep 17 00:00:00 2001 From: Inksprout Date: Mon, 27 Mar 2023 15:51:43 +1100 Subject: [PATCH 13/18] fix line ending issues --- PactNet.sln | 3 + src/PactNet.Abstractions/IRequestBuilder.cs | 10 +- src/PactNet/Drivers/HttpInteractionDriver.cs | 6 +- src/PactNet/Drivers/IHttpInteractionDriver.cs | 6 +- .../Drivers/InteropActionExtensions.cs | 11 +- src/PactNet/Interop/NativeInterop.cs | 6 +- src/PactNet/Interop/StringResult.cs | 72 +-- src/PactNet/RequestBuilder.cs | 18 +- .../Drivers/FfiIntegrationTests.cs | 172 ++++---- tests/PactNet.Tests/RequestBuilderTests.cs | 414 +++++++++--------- 10 files changed, 363 insertions(+), 355 deletions(-) diff --git a/PactNet.sln b/PactNet.sln index 1f7a1c8b..04ce6609 100644 --- a/PactNet.sln +++ b/PactNet.sln @@ -34,6 +34,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Consumer.Tests", "samples\Messaging\Consumer.Tests\Consumer.Tests.csproj", "{AED4E706-6E99-47B8-BE17-A3503275DB3E}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ReadMe", "ReadMe", "{87FC5A4B-1977-4FBA-AA71-63F48B28C3B0}" + ProjectSection(SolutionItems) = preProject + tests\PactNet.Tests\data\test_file.jpeg = tests\PactNet.Tests\data\test_file.jpeg + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Readme.Consumer", "samples\ReadMe\Consumer\Readme.Consumer.csproj", "{B7363201-F52A-49C4-A299-C9B459827C04}" EndProject diff --git a/src/PactNet.Abstractions/IRequestBuilder.cs b/src/PactNet.Abstractions/IRequestBuilder.cs index 6bf3dc95..6a6f9b90 100644 --- a/src/PactNet.Abstractions/IRequestBuilder.cs +++ b/src/PactNet.Abstractions/IRequestBuilder.cs @@ -204,15 +204,15 @@ public interface IRequestBuilderV3 /// Request body /// Content type /// Fluent builder - IRequestBuilderV3 WithBody(string body, string contentType); - + IRequestBuilderV3 WithBody(string body, string contentType); + /// /// Set a body which is multipart/form-data but contains only one part, which is a file upload - /// + /// /// The content type of the file being uploaded /// Path to the file being uploaded /// Fluent builder /// Response builder - IResponseBuilderV3 WillRespond(); + IResponseBuilderV3 WillRespond(); } } diff --git a/src/PactNet/Drivers/HttpInteractionDriver.cs b/src/PactNet/Drivers/HttpInteractionDriver.cs index fcdaefcc..01359072 100644 --- a/src/PactNet/Drivers/HttpInteractionDriver.cs +++ b/src/PactNet/Drivers/HttpInteractionDriver.cs @@ -96,11 +96,11 @@ public void WithRequestBody(string contentType, string body) /// Context type /// Serialised body public void WithResponseBody(string contentType, string body) - => NativeInterop.WithBody(this.interaction, InteractionPart.Response, contentType, body).CheckInteropSuccess(); - + => NativeInterop.WithBody(this.interaction, InteractionPart.Response, contentType, body).CheckInteropSuccess(); + /// /// Set the request body to multipart/form-data for file upload - /// + /// /// Content type override /// path to file being uploaded /// the name of the mime part being uploaded diff --git a/src/PactNet/Drivers/IHttpInteractionDriver.cs b/src/PactNet/Drivers/IHttpInteractionDriver.cs index 6d967c60..c2252c26 100644 --- a/src/PactNet/Drivers/IHttpInteractionDriver.cs +++ b/src/PactNet/Drivers/IHttpInteractionDriver.cs @@ -54,14 +54,14 @@ internal interface IHttpInteractionDriver : IProviderStateDriver /// /// Context type /// Serialised body - void WithResponseBody(string contentType, string body); - + void WithResponseBody(string contentType, string body); + /// /// Set the response body for a single file to be uploaded as a multipart/form-data content type /// /// path to file being uploaded /// Content type override /// the name of the mime part being uploaded - void WithMultipartSingleFileUpload(string filePath, string contentType, string mimePartName); + void WithMultipartSingleFileUpload(string filePath, string contentType, string mimePartName); } } diff --git a/src/PactNet/Drivers/InteropActionExtensions.cs b/src/PactNet/Drivers/InteropActionExtensions.cs index 877557a5..b0fd5daa 100644 --- a/src/PactNet/Drivers/InteropActionExtensions.cs +++ b/src/PactNet/Drivers/InteropActionExtensions.cs @@ -1,7 +1,7 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using PactNet.Exceptions; -using PactNet.Interop; - +using PactNet.Interop; + namespace PactNet.Drivers { /// @@ -22,6 +22,11 @@ public static void CheckInteropSuccess(this bool success) } } + /// + /// Check the result of an interop action when the response is a StringResult + /// + /// The result of the action + /// Action failed public static void CheckInteropSuccess(this StringResult success) { if (success.tag != StringResult.Tag.StringResult_Ok) diff --git a/src/PactNet/Interop/NativeInterop.cs b/src/PactNet/Interop/NativeInterop.cs index f49bd958..2b93154d 100644 --- a/src/PactNet/Interop/NativeInterop.cs +++ b/src/PactNet/Interop/NativeInterop.cs @@ -61,10 +61,10 @@ internal static class NativeInterop public static extern bool ResponseStatus(InteractionHandle interaction, ushort status); [DllImport(DllName, EntryPoint = "pactffi_with_body")] - public static extern bool WithBody(InteractionHandle interaction, InteractionPart part, string contentType, string body); - + public static extern bool WithBody(InteractionHandle interaction, InteractionPart part, string contentType, string body); + [DllImport(DllName, EntryPoint = "pactffi_with_multipart_file")] - public static extern StringResult WithMultipartSingleFileUpload(InteractionHandle interaction, InteractionPart part, string contentType, string filePath, string mimePartName ); + public static extern StringResult WithMultipartSingleFileUpload(InteractionHandle interaction, InteractionPart part, string contentType, string filePath, string mimePartName); [DllImport(DllName, EntryPoint = "pactffi_free_string")] public static extern void FreeString(IntPtr s); diff --git a/src/PactNet/Interop/StringResult.cs b/src/PactNet/Interop/StringResult.cs index 109f7984..5a85c2e5 100644 --- a/src/PactNet/Interop/StringResult.cs +++ b/src/PactNet/Interop/StringResult.cs @@ -1,36 +1,36 @@ -using System; -using System.Runtime.InteropServices; -namespace PactNet.Interop -{ - - [StructLayout(LayoutKind.Explicit)] - internal struct StringResult - { - public enum Tag - { - StringResult_Ok, - StringResult_Failed, - }; - - [FieldOffset(0)] - public Tag tag; - - [FieldOffset(8)] - public StringResult_Ok_Body ok; - - [FieldOffset(8)] - public StringResult_Failed_Body failed; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct StringResult_Ok_Body - { - public IntPtr _0; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct StringResult_Failed_Body - { - public IntPtr _0; - } -} +using System; +using System.Runtime.InteropServices; +namespace PactNet.Interop + +{ + [StructLayout(LayoutKind.Explicit)] + internal struct StringResult + { + public enum Tag + { + StringResult_Ok, + StringResult_Failed, + }; + + [FieldOffset(0)] + public Tag tag; + + [FieldOffset(8)] + public StringResultOkBody ok; + + [FieldOffset(8)] + public StringResultFailedBody failed; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct StringResultOkBody + { + public IntPtr successPointer; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct StringResultFailedBody + { + public IntPtr errorPointer; + } +} diff --git a/src/PactNet/RequestBuilder.cs b/src/PactNet/RequestBuilder.cs index d6526ddf..78fb002d 100644 --- a/src/PactNet/RequestBuilder.cs +++ b/src/PactNet/RequestBuilder.cs @@ -241,17 +241,17 @@ IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSet /// Content type override /// Fluent builder IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSettings settings, string contentType) - => this.WithJsonBody(body, settings, contentType); - + => this.WithJsonBody(body, settings, contentType); + /// /// Set a body which is multipart/form-data but contains only one part, which is a file upload - /// + /// /// The content type of the file being uploaded /// Path to the file being uploaded /// Fluent builder IRequestBuilderV3 IRequestBuilderV3.WithMultipartSingleFileUpload(string contentType, string filePath) - => this.WithMultipartSingleFileUpload(contentType, filePath, "file"); - + => this.WithMultipartSingleFileUpload(contentType, filePath, "file"); + /// /// A pre-formatted body which should be used as-is for the request /// @@ -399,17 +399,17 @@ internal RequestBuilder WithJsonBody(dynamic body, JsonSerializerSettings settin { string serialised = JsonConvert.SerializeObject(body, settings); return this.WithBody(serialised, contentType); - } - + } + /// /// Set a body which is multipart/form-data but contains only one part, which is a file upload /// /// path to file being uploaded /// Content type override /// The name of the mime part being uploaded - /// Fluent builder + /// Fluent builder internal RequestBuilder WithMultipartSingleFileUpload(string filePath, string contentType, string mimePartName) - { + { this.driver.WithMultipartSingleFileUpload(filePath, contentType, mimePartName); return this; } diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index 8d673a3c..6387e5b0 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -2,12 +2,12 @@ using System.IO; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; -using System.Runtime.InteropServices; +using System.Net.Http.Headers; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using FluentAssertions; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using PactNet.Drivers; using PactNet.Interop; using Xunit; @@ -27,8 +27,8 @@ public FfiIntegrationTests(ITestOutputHelper output) this.output = output; NativeInterop.LogToBuffer(LevelFilter.Trace); - } - + } + [Fact] public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() { @@ -38,17 +38,17 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() { IHttpPactDriver pact = driver.NewHttpPact("NativeDriverTests-Consumer-V3", "NativeDriverTests-Provider-Multipart", - PactSpecification.V3); - - IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); - - string contentType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "application/octet-stream" : "image/jpeg"; + PactSpecification.V3); + + IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); + + string contentType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "application/octet-stream" : "image/jpeg"; interaction.Given("provider state"); - interaction.WithRequest("POST", "/path"); - var path = Path.GetFullPath("data/test_file.jpeg"); - Assert.True(File.Exists(path)); - + interaction.WithRequest("POST", "/path"); + var path = Path.GetFullPath("data/test_file.jpeg"); + Assert.True(File.Exists(path)); + interaction.WithMultipartSingleFileUpload(contentType, path, "file"); interaction.WithResponseStatus((ushort)HttpStatusCode.Created); @@ -58,31 +58,31 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false); - var client = new HttpClient { BaseAddress = mockServer.Uri }; - - using var fileStream = new FileStream("data/test_file.jpeg", FileMode.Open, FileAccess.Read); - - var upload = new MultipartFormDataContent(); - upload.Headers.ContentType.MediaType = "multipart/form-data"; - - var fileContent = new StreamContent(fileStream); - fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg"); - - var fileName = Path.GetFileName(path); - var fileNameBytes = Encoding.UTF8.GetBytes(fileName); - var encodedFileName = Convert.ToBase64String(fileNameBytes); - upload.Add(fileContent, "file", fileName); - upload.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") - { - Name = "file", - FileName = fileName, - FileNameStar = $"utf-8''{encodedFileName}" - }; - - HttpResponseMessage result = await client.PostAsync("/path", upload); - result.StatusCode.Should().Be(HttpStatusCode.Created); - - string logs = mockServer.MockServerLogs(); + var client = new HttpClient { BaseAddress = mockServer.Uri }; + + using var fileStream = new FileStream("data/test_file.jpeg", FileMode.Open, FileAccess.Read); + + var upload = new MultipartFormDataContent(); + upload.Headers.ContentType.MediaType = "multipart/form-data"; + + var fileContent = new StreamContent(fileStream); + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg"); + + var fileName = Path.GetFileName(path); + var fileNameBytes = Encoding.UTF8.GetBytes(fileName); + var encodedFileName = Convert.ToBase64String(fileNameBytes); + upload.Add(fileContent, "file", fileName); + upload.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") + { + Name = "file", + FileName = fileName, + FileNameStar = $"utf-8''{encodedFileName}" + }; + + HttpResponseMessage result = await client.PostAsync("/path", upload); + result.StatusCode.Should().Be(HttpStatusCode.Created); + + string logs = mockServer.MockServerLogs(); string content = await result.Content.ReadAsStringAsync(); content.Should().Be(@"{""foo"":42}"); @@ -100,30 +100,30 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() finally { this.WriteDriverLogs(driver); - } + } // The body and boundry will be different, so test the header and matching rules are multipart/form-data var file = new FileInfo("NativeDriverTests-Consumer-V3-NativeDriverTests-Provider-Multipart.json"); file.Exists.Should().BeTrue(); - string pactContents = File.ReadAllText(file.FullName).TrimEnd(); - JObject pactObject = JObject.Parse(pactContents); - - string expectedPactContent = File.ReadAllText("data/v3-server-integration-MultipartFormDataBody.json").TrimEnd(); - JObject expectedPactObject = JObject.Parse(pactContents); - - - string contentTypeHeader = (string)pactObject["interactions"][0]["request"]["headers"]["Content-Type"]; - Assert.Contains("multipart/form-data;", contentTypeHeader); - - - JArray integrationsArray = (JArray)pactObject["interactions"]; - JToken matchingRules = integrationsArray.First["request"]["matchingRules"]; - - JArray expecteIntegrationsArray = (JArray)expectedPactObject["interactions"]; - JToken expectedMatchingRules = expecteIntegrationsArray.First["request"]["matchingRules"]; - - Assert.True(JToken.DeepEquals(matchingRules, expectedMatchingRules)); - } + string pactContents = File.ReadAllText(file.FullName).TrimEnd(); + JObject pactObject = JObject.Parse(pactContents); + + string expectedPactContent = File.ReadAllText("data/v3-server-integration-MultipartFormDataBody.json").TrimEnd(); + JObject expectedPactObject = JObject.Parse(pactContents); + + + string contentTypeHeader = (string)pactObject["interactions"][0]["request"]["headers"]["Content-Type"]; + Assert.Contains("multipart/form-data;", contentTypeHeader); + + + JArray integrationsArray = (JArray)pactObject["interactions"]; + JToken matchingRules = integrationsArray.First["request"]["matchingRules"]; + + JArray expecteIntegrationsArray = (JArray)expectedPactObject["interactions"]; + JToken expectedMatchingRules = expecteIntegrationsArray.First["request"]["matchingRules"]; + + Assert.True(JToken.DeepEquals(matchingRules, expectedMatchingRules)); + } [Fact] public async Task HttpInteraction_v3_CreatesPactFile() @@ -136,36 +136,36 @@ public async Task HttpInteraction_v3_CreatesPactFile() "NativeDriverTests-Provider", PactSpecification.V3); - IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); - - interaction.Given("provider state"); - interaction.GivenWithParam("state with param", "foo", "bar"); - interaction.WithRequest("POST", "/path"); - interaction.WithRequestHeader("X-Request-Header", "request1", 0); - interaction.WithRequestHeader("X-Request-Header", "request2", 1); - interaction.WithQueryParameter("param", "value", 0); - interaction.WithRequestBody("application/json", @"{""foo"":42}"); - - interaction.WithResponseStatus((ushort)HttpStatusCode.Created); - interaction.WithResponseHeader("X-Response-Header", "value1", 0); - interaction.WithResponseHeader("X-Response-Header", "value2", 1); - interaction.WithResponseBody("application/json", @"{""foo"":42}"); - - using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false); - - var client = new HttpClient { BaseAddress = mockServer.Uri }; - client.DefaultRequestHeaders.Add("X-Request-Header", new[] { "request1", "request2" }); - - HttpResponseMessage result = await client.PostAsync("/path?param=value", new StringContent(@"{""foo"":42}", Encoding.UTF8, "application/json")); - result.StatusCode.Should().Be(HttpStatusCode.Created); - result.Headers.GetValues("X-Response-Header").Should().BeEquivalentTo("value1", "value2"); - + IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); + + interaction.Given("provider state"); + interaction.GivenWithParam("state with param", "foo", "bar"); + interaction.WithRequest("POST", "/path"); + interaction.WithRequestHeader("X-Request-Header", "request1", 0); + interaction.WithRequestHeader("X-Request-Header", "request2", 1); + interaction.WithQueryParameter("param", "value", 0); + interaction.WithRequestBody("application/json", @"{""foo"":42}"); + + interaction.WithResponseStatus((ushort)HttpStatusCode.Created); + interaction.WithResponseHeader("X-Response-Header", "value1", 0); + interaction.WithResponseHeader("X-Response-Header", "value2", 1); + interaction.WithResponseBody("application/json", @"{""foo"":42}"); + + using IMockServerDriver mockServer = pact.CreateMockServer("127.0.0.1", null, false); + + var client = new HttpClient { BaseAddress = mockServer.Uri }; + client.DefaultRequestHeaders.Add("X-Request-Header", new[] { "request1", "request2" }); + + HttpResponseMessage result = await client.PostAsync("/path?param=value", new StringContent(@"{""foo"":42}", Encoding.UTF8, "application/json")); + result.StatusCode.Should().Be(HttpStatusCode.Created); + result.Headers.GetValues("X-Response-Header").Should().BeEquivalentTo("value1", "value2"); + string content = await result.Content.ReadAsStringAsync(); content.Should().Be(@"{""foo"":42}"); - mockServer.MockServerMismatches().Should().Be("[]"); - - string logs = mockServer.MockServerLogs(); + mockServer.MockServerMismatches().Should().Be("[]"); + + string logs = mockServer.MockServerLogs(); logs.Should().NotBeEmpty(); this.output.WriteLine("Mock Server Logs"); diff --git a/tests/PactNet.Tests/RequestBuilderTests.cs b/tests/PactNet.Tests/RequestBuilderTests.cs index 2117cef4..57d24eee 100644 --- a/tests/PactNet.Tests/RequestBuilderTests.cs +++ b/tests/PactNet.Tests/RequestBuilderTests.cs @@ -1,210 +1,210 @@ -using System; -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; -using System.Net.Http; -using FluentAssertions; -using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using PactNet.Drivers; -using Xunit; -using Match = PactNet.Matchers.Match; - -namespace PactNet.Tests -{ - public class RequestBuilderTests - { - private readonly RequestBuilder builder; - - private readonly Mock mockDriver; - - private readonly JsonSerializerSettings settings; - - public RequestBuilderTests() - { - this.mockDriver = new Mock(); - - this.settings = new JsonSerializerSettings(); - - this.builder = new RequestBuilder(this.mockDriver.Object, this.settings); - } - - [Fact] - public void Given_WhenCalled_AddsProviderState() - { - this.builder.Given("provider state"); - - this.mockDriver.Verify(s => s.Given("provider state")); - } - - [Fact] - public void Given_WithParams_AddsProviderState() - { - this.builder.Given("provider state", - new Dictionary - { - ["foo"] = "bar", - ["baz"] = "bash", - }); - - this.mockDriver.Verify(s => s.GivenWithParam("provider state", "foo", "bar")); - this.mockDriver.Verify(s => s.GivenWithParam("provider state", "baz", "bash")); - } - - [Fact] - public void WithRequest_HttpMethod_AddsRequest() - { - this.builder.WithRequest(HttpMethod.Post, "/some/path"); - - this.mockDriver.Verify(s => s.WithRequest("POST", "/some/path")); - } - - [Fact] - public void WithRequest_String_AddsRequest() - { - this.builder.WithRequest("POST", "/some/path"); - - this.mockDriver.Verify(s => s.WithRequest("POST", "/some/path")); - } - - [Fact] - public void WithQuery_WhenCalled_AddsQueryParam() - { - this.builder.WithQuery("name", "value"); - - this.mockDriver.Verify(s => s.WithQueryParameter("name", "value", 0)); - } - - [Fact] - public void WithQuery_RepeatedQuery_SetsIndex() - { - this.builder.WithQuery("name", "value1"); - this.builder.WithQuery("name", "value2"); - this.builder.WithQuery("other", "value"); - - this.mockDriver.Verify(s => s.WithQueryParameter("name", "value1", 0)); - this.mockDriver.Verify(s => s.WithQueryParameter("name", "value2", 1)); - this.mockDriver.Verify(s => s.WithQueryParameter("other", "value", 0)); - } - - [Fact] - public void WithHeader_Matcher_WhenCalled_AddsSerialisedHeaderParam() - { - var expectedValue = "{\"pact:matcher:type\":\"regex\",\"value\":\"header\",\"regex\":\"^header$\"}"; - - this.builder.WithHeader("name", Match.Regex("header", "^header$")); - - this.mockDriver.Verify(s => s.WithRequestHeader("name", expectedValue, 0)); - } - - [Fact] - public void WithHeader_RepeatedMatcherHeader_SetsIndex() - { - var expectedValue1 = "{\"pact:matcher:type\":\"regex\",\"value\":\"value1\",\"regex\":\"^value1$\"}"; - var expectedValue2 = "{\"pact:matcher:type\":\"type\",\"value\":\"value2\"}"; - var expectedValue = "{\"pact:matcher:type\":\"regex\",\"value\":\"value\",\"regex\":\"^value$\"}"; - - this.builder.WithHeader("name", Match.Regex("value1", "^value1$")); - this.builder.WithHeader("name", Match.Type("value2")); - this.builder.WithHeader("other", Match.Regex("value", "^value$")); - - this.mockDriver.Verify(s => s.WithRequestHeader("name", expectedValue1, 0)); - this.mockDriver.Verify(s => s.WithRequestHeader("name", expectedValue2, 1)); - this.mockDriver.Verify(s => s.WithRequestHeader("other", expectedValue, 0)); - } - - [Fact] - public void WithHeader_String_WhenCalled_AddsHeaderParam() - { - this.builder.WithHeader("name", "value"); - - this.mockDriver.Verify(s => s.WithRequestHeader("name", "value", 0)); - } - - [Fact] - public void WithHeader_RepeatedStringHeader_SetsIndex() - { - this.builder.WithHeader("name", "value1"); - this.builder.WithHeader("name", "value2"); - this.builder.WithHeader("other", "value"); - - this.mockDriver.Verify(s => s.WithRequestHeader("name", "value1", 0)); - this.mockDriver.Verify(s => s.WithRequestHeader("name", "value2", 1)); - this.mockDriver.Verify(s => s.WithRequestHeader("other", "value", 0)); - } - - [Fact] - public void WithJsonBody_NoOverrides_AddsRequestBodyWithDefaultSettings() - { - this.builder.WithJsonBody(new { Foo = 42 }); - - this.mockDriver.Verify(s => s.WithRequestBody("application/json", @"{""Foo"":42}")); - } - - [Fact] - public void WithJsonBody_OverrideContentType_AddsRequestBodyWithOverriddenContentType() - { - this.builder.WithJsonBody(new { Foo = 42 }, "application/json-patch+json"); - - this.mockDriver.Verify(s => s.WithRequestBody("application/json-patch+json", @"{""Foo"":42}")); - } - - [Fact] - public void WithJsonBody_OverrideJsonSettings_AddsRequestBodyWithOverriddenSettings() - { - this.builder.WithJsonBody(new { Foo = 42 }, - new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); - - this.mockDriver.Verify(s => s.WithRequestBody("application/json", @"{""foo"":42}")); - } - - [Fact] - public void WithJsonBody_OverrideContentTypeAndSettings_AddsRequestBodyWithOverriddenContentTypeAndSettings() - { - this.builder.WithJsonBody(new { Foo = 42 }, - new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }, - "application/json-patch+json"); - - this.mockDriver.Verify(s => s.WithRequestBody("application/json-patch+json", @"{""foo"":42}")); - } - - [Fact] - public void WithBody_WhenCalled_AddsRequestBody() - { - this.builder.WithBody("foo,bar\nbaz,bash", "text/csv"); - - this.mockDriver.Verify(s => s.WithRequestBody("text/csv", "foo,bar\nbaz,bash")); - } - - [Fact] - public void WillRespond_RequestConfigured_ReturnsResponseBuilder() - { - this.builder.WithRequest(HttpMethod.Delete, "/foo"); - - var responseBuilder = this.builder.WillRespond(); - - responseBuilder.Should().BeOfType(); - } - - [Fact] - public void WillRespond_RequestNotConfigured_ThrowsInvalidOperationException() - { - Action action = () => this.builder.WillRespond(); - - action.Should().Throw("because the request has not been configured"); - } - - [Fact] - public void WithMultipartSingleFileUpload_AddsRequestBody() +using System.Net.Http; +using FluentAssertions; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using PactNet.Drivers; +using Xunit; +using Match = PactNet.Matchers.Match; + +namespace PactNet.Tests +{ + public class RequestBuilderTests + { + private readonly RequestBuilder builder; + + private readonly Mock mockDriver; + + private readonly JsonSerializerSettings settings; + + public RequestBuilderTests() + { + this.mockDriver = new Mock(); + + this.settings = new JsonSerializerSettings(); + + this.builder = new RequestBuilder(this.mockDriver.Object, this.settings); + } + + [Fact] + public void Given_WhenCalled_AddsProviderState() + { + this.builder.Given("provider state"); + + this.mockDriver.Verify(s => s.Given("provider state")); + } + + [Fact] + public void Given_WithParams_AddsProviderState() + { + this.builder.Given("provider state", + new Dictionary + { + ["foo"] = "bar", + ["baz"] = "bash", + }); + + this.mockDriver.Verify(s => s.GivenWithParam("provider state", "foo", "bar")); + this.mockDriver.Verify(s => s.GivenWithParam("provider state", "baz", "bash")); + } + + [Fact] + public void WithRequest_HttpMethod_AddsRequest() + { + this.builder.WithRequest(HttpMethod.Post, "/some/path"); + + this.mockDriver.Verify(s => s.WithRequest("POST", "/some/path")); + } + + [Fact] + public void WithRequest_String_AddsRequest() + { + this.builder.WithRequest("POST", "/some/path"); + + this.mockDriver.Verify(s => s.WithRequest("POST", "/some/path")); + } + + [Fact] + public void WithQuery_WhenCalled_AddsQueryParam() + { + this.builder.WithQuery("name", "value"); + + this.mockDriver.Verify(s => s.WithQueryParameter("name", "value", 0)); + } + + [Fact] + public void WithQuery_RepeatedQuery_SetsIndex() + { + this.builder.WithQuery("name", "value1"); + this.builder.WithQuery("name", "value2"); + this.builder.WithQuery("other", "value"); + + this.mockDriver.Verify(s => s.WithQueryParameter("name", "value1", 0)); + this.mockDriver.Verify(s => s.WithQueryParameter("name", "value2", 1)); + this.mockDriver.Verify(s => s.WithQueryParameter("other", "value", 0)); + } + + [Fact] + public void WithHeader_Matcher_WhenCalled_AddsSerialisedHeaderParam() + { + var expectedValue = "{\"pact:matcher:type\":\"regex\",\"value\":\"header\",\"regex\":\"^header$\"}"; + + this.builder.WithHeader("name", Match.Regex("header", "^header$")); + + this.mockDriver.Verify(s => s.WithRequestHeader("name", expectedValue, 0)); + } + + [Fact] + public void WithHeader_RepeatedMatcherHeader_SetsIndex() + { + var expectedValue1 = "{\"pact:matcher:type\":\"regex\",\"value\":\"value1\",\"regex\":\"^value1$\"}"; + var expectedValue2 = "{\"pact:matcher:type\":\"type\",\"value\":\"value2\"}"; + var expectedValue = "{\"pact:matcher:type\":\"regex\",\"value\":\"value\",\"regex\":\"^value$\"}"; + + this.builder.WithHeader("name", Match.Regex("value1", "^value1$")); + this.builder.WithHeader("name", Match.Type("value2")); + this.builder.WithHeader("other", Match.Regex("value", "^value$")); + + this.mockDriver.Verify(s => s.WithRequestHeader("name", expectedValue1, 0)); + this.mockDriver.Verify(s => s.WithRequestHeader("name", expectedValue2, 1)); + this.mockDriver.Verify(s => s.WithRequestHeader("other", expectedValue, 0)); + } + + [Fact] + public void WithHeader_String_WhenCalled_AddsHeaderParam() + { + this.builder.WithHeader("name", "value"); + + this.mockDriver.Verify(s => s.WithRequestHeader("name", "value", 0)); + } + + [Fact] + public void WithHeader_RepeatedStringHeader_SetsIndex() + { + this.builder.WithHeader("name", "value1"); + this.builder.WithHeader("name", "value2"); + this.builder.WithHeader("other", "value"); + + this.mockDriver.Verify(s => s.WithRequestHeader("name", "value1", 0)); + this.mockDriver.Verify(s => s.WithRequestHeader("name", "value2", 1)); + this.mockDriver.Verify(s => s.WithRequestHeader("other", "value", 0)); + } + + [Fact] + public void WithJsonBody_NoOverrides_AddsRequestBodyWithDefaultSettings() + { + this.builder.WithJsonBody(new { Foo = 42 }); + + this.mockDriver.Verify(s => s.WithRequestBody("application/json", @"{""Foo"":42}")); + } + + [Fact] + public void WithJsonBody_OverrideContentType_AddsRequestBodyWithOverriddenContentType() + { + this.builder.WithJsonBody(new { Foo = 42 }, "application/json-patch+json"); + + this.mockDriver.Verify(s => s.WithRequestBody("application/json-patch+json", @"{""Foo"":42}")); + } + + [Fact] + public void WithJsonBody_OverrideJsonSettings_AddsRequestBodyWithOverriddenSettings() + { + this.builder.WithJsonBody(new { Foo = 42 }, + new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); + + this.mockDriver.Verify(s => s.WithRequestBody("application/json", @"{""foo"":42}")); + } + + [Fact] + public void WithJsonBody_OverrideContentTypeAndSettings_AddsRequestBodyWithOverriddenContentTypeAndSettings() + { + this.builder.WithJsonBody(new { Foo = 42 }, + new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }, + "application/json-patch+json"); + + this.mockDriver.Verify(s => s.WithRequestBody("application/json-patch+json", @"{""foo"":42}")); + } + + [Fact] + public void WithBody_WhenCalled_AddsRequestBody() + { + this.builder.WithBody("foo,bar\nbaz,bash", "text/csv"); + + this.mockDriver.Verify(s => s.WithRequestBody("text/csv", "foo,bar\nbaz,bash")); + } + + [Fact] + public void WillRespond_RequestConfigured_ReturnsResponseBuilder() + { + this.builder.WithRequest(HttpMethod.Delete, "/foo"); + + var responseBuilder = this.builder.WillRespond(); + + responseBuilder.Should().BeOfType(); + } + + [Fact] + public void WillRespond_RequestNotConfigured_ThrowsInvalidOperationException() + { + Action action = () => this.builder.WillRespond(); + + action.Should().Throw("because the request has not been configured"); + } + + [Fact] + public void WithMultipartSingleFileUpload_AddsRequestBody() { var path = Path.GetFullPath("data/test_file.jpeg"); - - this.builder.WithMultipartSingleFileUpload("image/jpeg", path, "file"); - - this.mockDriver.Verify(s => s.WithMultipartSingleFileUpload("image/jpeg", path, "file")); - } - } -} + + this.builder.WithMultipartSingleFileUpload("image/jpeg", path, "file"); + + this.mockDriver.Verify(s => s.WithMultipartSingleFileUpload("image/jpeg", path, "file")); + } + } +} From 1cb75df6d6dae6f39d3a284042ce68962bbb250b Mon Sep 17 00:00:00 2001 From: Inksprout Date: Mon, 27 Mar 2023 17:14:21 +1100 Subject: [PATCH 14/18] pass through file name for mime part --- src/PactNet.Abstractions/IRequestBuilder.cs | 5 +++-- src/PactNet/Drivers/HttpInteractionDriver.cs | 6 +++--- src/PactNet/Drivers/InteropActionExtensions.cs | 2 +- src/PactNet/Interop/NativeInterop.cs | 2 +- src/PactNet/RequestBuilder.cs | 11 ++++++----- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/PactNet.Abstractions/IRequestBuilder.cs b/src/PactNet.Abstractions/IRequestBuilder.cs index 6a6f9b90..b0a3fe4a 100644 --- a/src/PactNet.Abstractions/IRequestBuilder.cs +++ b/src/PactNet.Abstractions/IRequestBuilder.cs @@ -211,8 +211,9 @@ public interface IRequestBuilderV3 /// /// The content type of the file being uploaded /// Path to the file being uploaded - /// Fluent builderThe name of the file being uploaded as a part + /// Fluent builder + IRequestBuilderV3 WithMultipartSingleFileUpload(string contentType, string filePath, string partName); // TODO: Support binary and multi-part body diff --git a/src/PactNet/Drivers/HttpInteractionDriver.cs b/src/PactNet/Drivers/HttpInteractionDriver.cs index 01359072..0991bb06 100644 --- a/src/PactNet/Drivers/HttpInteractionDriver.cs +++ b/src/PactNet/Drivers/HttpInteractionDriver.cs @@ -103,8 +103,8 @@ public void WithResponseBody(string contentType, string body) /// /// Content type override /// path to file being uploaded - /// the name of the mime part being uploaded - public void WithMultipartSingleFileUpload(string contentType, string filePath, string mimePartName) - => NativeInterop.WithMultipartSingleFileUpload(this.interaction, InteractionPart.Request, contentType, filePath, mimePartName).CheckInteropSuccess(); + /// the name of the mime part being uploaded + public void WithMultipartSingleFileUpload(string contentType, string filePath, string partName) + => NativeInterop.WithMultipartSingleFileUpload(this.interaction, InteractionPart.Request, contentType, filePath, partName).CheckInteropSuccess(); } } diff --git a/src/PactNet/Drivers/InteropActionExtensions.cs b/src/PactNet/Drivers/InteropActionExtensions.cs index b0fd5daa..5cc59baa 100644 --- a/src/PactNet/Drivers/InteropActionExtensions.cs +++ b/src/PactNet/Drivers/InteropActionExtensions.cs @@ -31,7 +31,7 @@ public static void CheckInteropSuccess(this StringResult success) { if (success.tag != StringResult.Tag.StringResult_Ok) { - string errorMsg = Marshal.PtrToStringAnsi(success.failed._0); + string errorMsg = Marshal.PtrToStringAnsi(success.failed.errorPointer); throw new PactFailureException($"Unable to perform the given action. The interop call returned failure: {errorMsg}"); } } diff --git a/src/PactNet/Interop/NativeInterop.cs b/src/PactNet/Interop/NativeInterop.cs index 2b93154d..92441eb2 100644 --- a/src/PactNet/Interop/NativeInterop.cs +++ b/src/PactNet/Interop/NativeInterop.cs @@ -64,7 +64,7 @@ internal static class NativeInterop public static extern bool WithBody(InteractionHandle interaction, InteractionPart part, string contentType, string body); [DllImport(DllName, EntryPoint = "pactffi_with_multipart_file")] - public static extern StringResult WithMultipartSingleFileUpload(InteractionHandle interaction, InteractionPart part, string contentType, string filePath, string mimePartName); + public static extern StringResult WithMultipartSingleFileUpload(InteractionHandle interaction, InteractionPart part, string contentType, string filePath, string partName); [DllImport(DllName, EntryPoint = "pactffi_free_string")] public static extern void FreeString(IntPtr s); diff --git a/src/PactNet/RequestBuilder.cs b/src/PactNet/RequestBuilder.cs index 78fb002d..5acc46ac 100644 --- a/src/PactNet/RequestBuilder.cs +++ b/src/PactNet/RequestBuilder.cs @@ -248,9 +248,10 @@ IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSet /// /// The content type of the file being uploaded /// Path to the file being uploaded + /// The name of the file being uploaded as a part /// Fluent builder - IRequestBuilderV3 IRequestBuilderV3.WithMultipartSingleFileUpload(string contentType, string filePath) - => this.WithMultipartSingleFileUpload(contentType, filePath, "file"); + IRequestBuilderV3 IRequestBuilderV3.WithMultipartSingleFileUpload(string contentType, string filePath, string partName) + => this.WithMultipartSingleFileUpload(contentType, filePath, partName); /// /// A pre-formatted body which should be used as-is for the request @@ -406,11 +407,11 @@ internal RequestBuilder WithJsonBody(dynamic body, JsonSerializerSettings settin /// /// path to file being uploaded /// Content type override - /// The name of the mime part being uploaded + /// The name of the mime part being uploaded /// Fluent builder - internal RequestBuilder WithMultipartSingleFileUpload(string filePath, string contentType, string mimePartName) + internal RequestBuilder WithMultipartSingleFileUpload(string filePath, string contentType, string partName = "file") { - this.driver.WithMultipartSingleFileUpload(filePath, contentType, mimePartName); + this.driver.WithMultipartSingleFileUpload(filePath, contentType, partName); return this; } /// From 29d719b1bcb49864f18572c24d6097e46d12d4ba Mon Sep 17 00:00:00 2001 From: Inksprout Date: Tue, 28 Mar 2023 12:29:22 +1100 Subject: [PATCH 15/18] Rename method and use fileInfo --- src/PactNet.Abstractions/IRequestBuilder.cs | 5 +- src/PactNet/Drivers/HttpInteractionDriver.cs | 4 +- src/PactNet/Drivers/IHttpInteractionDriver.cs | 2 +- src/PactNet/Interop/NativeInterop.cs | 2 +- src/PactNet/RequestBuilder.cs | 13 +- .../Drivers/FfiIntegrationTests.cs | 6 +- tests/PactNet.Tests/RequestBuilderTests.cs | 421 +++++++++--------- 7 files changed, 229 insertions(+), 224 deletions(-) diff --git a/src/PactNet.Abstractions/IRequestBuilder.cs b/src/PactNet.Abstractions/IRequestBuilder.cs index b0a3fe4a..444fc47c 100644 --- a/src/PactNet.Abstractions/IRequestBuilder.cs +++ b/src/PactNet.Abstractions/IRequestBuilder.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Net.Http; using Newtonsoft.Json; using PactNet.Matchers; @@ -210,10 +211,10 @@ public interface IRequestBuilderV3 /// Set a body which is multipart/form-data but contains only one part, which is a file upload /// /// The content type of the file being uploaded - /// Path to the file being uploaded + /// Path to the file being uploaded /// The name of the file being uploaded as a part /// Fluent builder - IRequestBuilderV3 WithMultipartSingleFileUpload(string contentType, string filePath, string partName); + IRequestBuilderV3 WithFileUpload(string contentType, FileInfo fileInfo, string partName); // TODO: Support binary and multi-part body diff --git a/src/PactNet/Drivers/HttpInteractionDriver.cs b/src/PactNet/Drivers/HttpInteractionDriver.cs index 0991bb06..a62d4db1 100644 --- a/src/PactNet/Drivers/HttpInteractionDriver.cs +++ b/src/PactNet/Drivers/HttpInteractionDriver.cs @@ -104,7 +104,7 @@ public void WithResponseBody(string contentType, string body) /// Content type override /// path to file being uploaded /// the name of the mime part being uploaded - public void WithMultipartSingleFileUpload(string contentType, string filePath, string partName) - => NativeInterop.WithMultipartSingleFileUpload(this.interaction, InteractionPart.Request, contentType, filePath, partName).CheckInteropSuccess(); + public void WithFileUpload(string contentType, string filePath, string partName) + => NativeInterop.WithFileUpload(this.interaction, InteractionPart.Request, contentType, filePath, partName).CheckInteropSuccess(); } } diff --git a/src/PactNet/Drivers/IHttpInteractionDriver.cs b/src/PactNet/Drivers/IHttpInteractionDriver.cs index c2252c26..ac5acf8e 100644 --- a/src/PactNet/Drivers/IHttpInteractionDriver.cs +++ b/src/PactNet/Drivers/IHttpInteractionDriver.cs @@ -62,6 +62,6 @@ internal interface IHttpInteractionDriver : IProviderStateDriver /// path to file being uploaded /// Content type override /// the name of the mime part being uploaded - void WithMultipartSingleFileUpload(string filePath, string contentType, string mimePartName); + void WithFileUpload(string filePath, string contentType, string mimePartName); } } diff --git a/src/PactNet/Interop/NativeInterop.cs b/src/PactNet/Interop/NativeInterop.cs index 92441eb2..f6d246d1 100644 --- a/src/PactNet/Interop/NativeInterop.cs +++ b/src/PactNet/Interop/NativeInterop.cs @@ -64,7 +64,7 @@ internal static class NativeInterop public static extern bool WithBody(InteractionHandle interaction, InteractionPart part, string contentType, string body); [DllImport(DllName, EntryPoint = "pactffi_with_multipart_file")] - public static extern StringResult WithMultipartSingleFileUpload(InteractionHandle interaction, InteractionPart part, string contentType, string filePath, string partName); + public static extern StringResult WithFileUpload(InteractionHandle interaction, InteractionPart part, string contentType, string filePath, string partName); [DllImport(DllName, EntryPoint = "pactffi_free_string")] public static extern void FreeString(IntPtr s); diff --git a/src/PactNet/RequestBuilder.cs b/src/PactNet/RequestBuilder.cs index 5acc46ac..abfe1fe5 100644 --- a/src/PactNet/RequestBuilder.cs +++ b/src/PactNet/RequestBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Net.Http; using Newtonsoft.Json; using PactNet.Drivers; @@ -247,11 +248,11 @@ IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSet /// Set a body which is multipart/form-data but contains only one part, which is a file upload /// /// The content type of the file being uploaded - /// Path to the file being uploaded + /// >file info of the file being uploaded /// The name of the file being uploaded as a part /// Fluent builder - IRequestBuilderV3 IRequestBuilderV3.WithMultipartSingleFileUpload(string contentType, string filePath, string partName) - => this.WithMultipartSingleFileUpload(contentType, filePath, partName); + IRequestBuilderV3 IRequestBuilderV3.WithFileUpload(string contentType, FileInfo fileInfo, string partName) + => this.WithFileUpload(contentType, fileInfo, partName); /// /// A pre-formatted body which should be used as-is for the request @@ -405,13 +406,13 @@ internal RequestBuilder WithJsonBody(dynamic body, JsonSerializerSettings settin /// /// Set a body which is multipart/form-data but contains only one part, which is a file upload /// - /// path to file being uploaded + /// file info of the file being uploaded /// Content type override /// The name of the mime part being uploaded /// Fluent builder - internal RequestBuilder WithMultipartSingleFileUpload(string filePath, string contentType, string partName = "file") + internal RequestBuilder WithFileUpload(string contentType, FileInfo fileInfo, string partName = "file") { - this.driver.WithMultipartSingleFileUpload(filePath, contentType, partName); + this.driver.WithFileUpload(contentType, fileInfo.FullName, partName = "file"); return this; } /// diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index 6387e5b0..8ba4a58d 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -38,7 +38,7 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() { IHttpPactDriver pact = driver.NewHttpPact("NativeDriverTests-Consumer-V3", "NativeDriverTests-Provider-Multipart", - PactSpecification.V3); + PactSpecification.V4); IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); @@ -47,9 +47,11 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() interaction.Given("provider state"); interaction.WithRequest("POST", "/path"); var path = Path.GetFullPath("data/test_file.jpeg"); + + var fileInfo = new FileInfo(path); Assert.True(File.Exists(path)); - interaction.WithMultipartSingleFileUpload(contentType, path, "file"); + interaction.WithFileUpload(contentType, fileInfo.FullName, "file"); interaction.WithResponseStatus((ushort)HttpStatusCode.Created); interaction.WithResponseHeader("X-Response-Header", "value1", 0); diff --git a/tests/PactNet.Tests/RequestBuilderTests.cs b/tests/PactNet.Tests/RequestBuilderTests.cs index 57d24eee..0c769ef1 100644 --- a/tests/PactNet.Tests/RequestBuilderTests.cs +++ b/tests/PactNet.Tests/RequestBuilderTests.cs @@ -1,210 +1,211 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; -using FluentAssertions; -using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using PactNet.Drivers; -using Xunit; -using Match = PactNet.Matchers.Match; - -namespace PactNet.Tests -{ - public class RequestBuilderTests - { - private readonly RequestBuilder builder; - - private readonly Mock mockDriver; - - private readonly JsonSerializerSettings settings; - - public RequestBuilderTests() - { - this.mockDriver = new Mock(); - - this.settings = new JsonSerializerSettings(); - - this.builder = new RequestBuilder(this.mockDriver.Object, this.settings); - } - - [Fact] - public void Given_WhenCalled_AddsProviderState() - { - this.builder.Given("provider state"); - - this.mockDriver.Verify(s => s.Given("provider state")); - } - - [Fact] - public void Given_WithParams_AddsProviderState() - { - this.builder.Given("provider state", - new Dictionary - { - ["foo"] = "bar", - ["baz"] = "bash", - }); - - this.mockDriver.Verify(s => s.GivenWithParam("provider state", "foo", "bar")); - this.mockDriver.Verify(s => s.GivenWithParam("provider state", "baz", "bash")); - } - - [Fact] - public void WithRequest_HttpMethod_AddsRequest() - { - this.builder.WithRequest(HttpMethod.Post, "/some/path"); - - this.mockDriver.Verify(s => s.WithRequest("POST", "/some/path")); - } - - [Fact] - public void WithRequest_String_AddsRequest() - { - this.builder.WithRequest("POST", "/some/path"); - - this.mockDriver.Verify(s => s.WithRequest("POST", "/some/path")); - } - - [Fact] - public void WithQuery_WhenCalled_AddsQueryParam() - { - this.builder.WithQuery("name", "value"); - - this.mockDriver.Verify(s => s.WithQueryParameter("name", "value", 0)); - } - - [Fact] - public void WithQuery_RepeatedQuery_SetsIndex() - { - this.builder.WithQuery("name", "value1"); - this.builder.WithQuery("name", "value2"); - this.builder.WithQuery("other", "value"); - - this.mockDriver.Verify(s => s.WithQueryParameter("name", "value1", 0)); - this.mockDriver.Verify(s => s.WithQueryParameter("name", "value2", 1)); - this.mockDriver.Verify(s => s.WithQueryParameter("other", "value", 0)); - } - - [Fact] - public void WithHeader_Matcher_WhenCalled_AddsSerialisedHeaderParam() - { - var expectedValue = "{\"pact:matcher:type\":\"regex\",\"value\":\"header\",\"regex\":\"^header$\"}"; - - this.builder.WithHeader("name", Match.Regex("header", "^header$")); - - this.mockDriver.Verify(s => s.WithRequestHeader("name", expectedValue, 0)); - } - - [Fact] - public void WithHeader_RepeatedMatcherHeader_SetsIndex() - { - var expectedValue1 = "{\"pact:matcher:type\":\"regex\",\"value\":\"value1\",\"regex\":\"^value1$\"}"; - var expectedValue2 = "{\"pact:matcher:type\":\"type\",\"value\":\"value2\"}"; - var expectedValue = "{\"pact:matcher:type\":\"regex\",\"value\":\"value\",\"regex\":\"^value$\"}"; - - this.builder.WithHeader("name", Match.Regex("value1", "^value1$")); - this.builder.WithHeader("name", Match.Type("value2")); - this.builder.WithHeader("other", Match.Regex("value", "^value$")); - - this.mockDriver.Verify(s => s.WithRequestHeader("name", expectedValue1, 0)); - this.mockDriver.Verify(s => s.WithRequestHeader("name", expectedValue2, 1)); - this.mockDriver.Verify(s => s.WithRequestHeader("other", expectedValue, 0)); - } - - [Fact] - public void WithHeader_String_WhenCalled_AddsHeaderParam() - { - this.builder.WithHeader("name", "value"); - - this.mockDriver.Verify(s => s.WithRequestHeader("name", "value", 0)); - } - - [Fact] - public void WithHeader_RepeatedStringHeader_SetsIndex() - { - this.builder.WithHeader("name", "value1"); - this.builder.WithHeader("name", "value2"); - this.builder.WithHeader("other", "value"); - - this.mockDriver.Verify(s => s.WithRequestHeader("name", "value1", 0)); - this.mockDriver.Verify(s => s.WithRequestHeader("name", "value2", 1)); - this.mockDriver.Verify(s => s.WithRequestHeader("other", "value", 0)); - } - - [Fact] - public void WithJsonBody_NoOverrides_AddsRequestBodyWithDefaultSettings() - { - this.builder.WithJsonBody(new { Foo = 42 }); - - this.mockDriver.Verify(s => s.WithRequestBody("application/json", @"{""Foo"":42}")); - } - - [Fact] - public void WithJsonBody_OverrideContentType_AddsRequestBodyWithOverriddenContentType() - { - this.builder.WithJsonBody(new { Foo = 42 }, "application/json-patch+json"); - - this.mockDriver.Verify(s => s.WithRequestBody("application/json-patch+json", @"{""Foo"":42}")); - } - - [Fact] - public void WithJsonBody_OverrideJsonSettings_AddsRequestBodyWithOverriddenSettings() - { - this.builder.WithJsonBody(new { Foo = 42 }, - new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); - - this.mockDriver.Verify(s => s.WithRequestBody("application/json", @"{""foo"":42}")); - } - - [Fact] - public void WithJsonBody_OverrideContentTypeAndSettings_AddsRequestBodyWithOverriddenContentTypeAndSettings() - { - this.builder.WithJsonBody(new { Foo = 42 }, - new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }, - "application/json-patch+json"); - - this.mockDriver.Verify(s => s.WithRequestBody("application/json-patch+json", @"{""foo"":42}")); - } - - [Fact] - public void WithBody_WhenCalled_AddsRequestBody() - { - this.builder.WithBody("foo,bar\nbaz,bash", "text/csv"); - - this.mockDriver.Verify(s => s.WithRequestBody("text/csv", "foo,bar\nbaz,bash")); - } - - [Fact] - public void WillRespond_RequestConfigured_ReturnsResponseBuilder() - { - this.builder.WithRequest(HttpMethod.Delete, "/foo"); - - var responseBuilder = this.builder.WillRespond(); - - responseBuilder.Should().BeOfType(); - } - - [Fact] - public void WillRespond_RequestNotConfigured_ThrowsInvalidOperationException() - { - Action action = () => this.builder.WillRespond(); - - action.Should().Throw("because the request has not been configured"); - } - - [Fact] - public void WithMultipartSingleFileUpload_AddsRequestBody() - { - var path = Path.GetFullPath("data/test_file.jpeg"); - - this.builder.WithMultipartSingleFileUpload("image/jpeg", path, "file"); - - this.mockDriver.Verify(s => s.WithMultipartSingleFileUpload("image/jpeg", path, "file")); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using FluentAssertions; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using PactNet.Drivers; +using Xunit; +using Match = PactNet.Matchers.Match; + +namespace PactNet.Tests +{ + public class RequestBuilderTests + { + private readonly RequestBuilder builder; + + private readonly Mock mockDriver; + + private readonly JsonSerializerSettings settings; + + public RequestBuilderTests() + { + this.mockDriver = new Mock(); + + this.settings = new JsonSerializerSettings(); + + this.builder = new RequestBuilder(this.mockDriver.Object, this.settings); + } + + [Fact] + public void Given_WhenCalled_AddsProviderState() + { + this.builder.Given("provider state"); + + this.mockDriver.Verify(s => s.Given("provider state")); + } + + [Fact] + public void Given_WithParams_AddsProviderState() + { + this.builder.Given("provider state", + new Dictionary + { + ["foo"] = "bar", + ["baz"] = "bash", + }); + + this.mockDriver.Verify(s => s.GivenWithParam("provider state", "foo", "bar")); + this.mockDriver.Verify(s => s.GivenWithParam("provider state", "baz", "bash")); + } + + [Fact] + public void WithRequest_HttpMethod_AddsRequest() + { + this.builder.WithRequest(HttpMethod.Post, "/some/path"); + + this.mockDriver.Verify(s => s.WithRequest("POST", "/some/path")); + } + + [Fact] + public void WithRequest_String_AddsRequest() + { + this.builder.WithRequest("POST", "/some/path"); + + this.mockDriver.Verify(s => s.WithRequest("POST", "/some/path")); + } + + [Fact] + public void WithQuery_WhenCalled_AddsQueryParam() + { + this.builder.WithQuery("name", "value"); + + this.mockDriver.Verify(s => s.WithQueryParameter("name", "value", 0)); + } + + [Fact] + public void WithQuery_RepeatedQuery_SetsIndex() + { + this.builder.WithQuery("name", "value1"); + this.builder.WithQuery("name", "value2"); + this.builder.WithQuery("other", "value"); + + this.mockDriver.Verify(s => s.WithQueryParameter("name", "value1", 0)); + this.mockDriver.Verify(s => s.WithQueryParameter("name", "value2", 1)); + this.mockDriver.Verify(s => s.WithQueryParameter("other", "value", 0)); + } + + [Fact] + public void WithHeader_Matcher_WhenCalled_AddsSerialisedHeaderParam() + { + var expectedValue = "{\"pact:matcher:type\":\"regex\",\"value\":\"header\",\"regex\":\"^header$\"}"; + + this.builder.WithHeader("name", Match.Regex("header", "^header$")); + + this.mockDriver.Verify(s => s.WithRequestHeader("name", expectedValue, 0)); + } + + [Fact] + public void WithHeader_RepeatedMatcherHeader_SetsIndex() + { + var expectedValue1 = "{\"pact:matcher:type\":\"regex\",\"value\":\"value1\",\"regex\":\"^value1$\"}"; + var expectedValue2 = "{\"pact:matcher:type\":\"type\",\"value\":\"value2\"}"; + var expectedValue = "{\"pact:matcher:type\":\"regex\",\"value\":\"value\",\"regex\":\"^value$\"}"; + + this.builder.WithHeader("name", Match.Regex("value1", "^value1$")); + this.builder.WithHeader("name", Match.Type("value2")); + this.builder.WithHeader("other", Match.Regex("value", "^value$")); + + this.mockDriver.Verify(s => s.WithRequestHeader("name", expectedValue1, 0)); + this.mockDriver.Verify(s => s.WithRequestHeader("name", expectedValue2, 1)); + this.mockDriver.Verify(s => s.WithRequestHeader("other", expectedValue, 0)); + } + + [Fact] + public void WithHeader_String_WhenCalled_AddsHeaderParam() + { + this.builder.WithHeader("name", "value"); + + this.mockDriver.Verify(s => s.WithRequestHeader("name", "value", 0)); + } + + [Fact] + public void WithHeader_RepeatedStringHeader_SetsIndex() + { + this.builder.WithHeader("name", "value1"); + this.builder.WithHeader("name", "value2"); + this.builder.WithHeader("other", "value"); + + this.mockDriver.Verify(s => s.WithRequestHeader("name", "value1", 0)); + this.mockDriver.Verify(s => s.WithRequestHeader("name", "value2", 1)); + this.mockDriver.Verify(s => s.WithRequestHeader("other", "value", 0)); + } + + [Fact] + public void WithJsonBody_NoOverrides_AddsRequestBodyWithDefaultSettings() + { + this.builder.WithJsonBody(new { Foo = 42 }); + + this.mockDriver.Verify(s => s.WithRequestBody("application/json", @"{""Foo"":42}")); + } + + [Fact] + public void WithJsonBody_OverrideContentType_AddsRequestBodyWithOverriddenContentType() + { + this.builder.WithJsonBody(new { Foo = 42 }, "application/json-patch+json"); + + this.mockDriver.Verify(s => s.WithRequestBody("application/json-patch+json", @"{""Foo"":42}")); + } + + [Fact] + public void WithJsonBody_OverrideJsonSettings_AddsRequestBodyWithOverriddenSettings() + { + this.builder.WithJsonBody(new { Foo = 42 }, + new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); + + this.mockDriver.Verify(s => s.WithRequestBody("application/json", @"{""foo"":42}")); + } + + [Fact] + public void WithJsonBody_OverrideContentTypeAndSettings_AddsRequestBodyWithOverriddenContentTypeAndSettings() + { + this.builder.WithJsonBody(new { Foo = 42 }, + new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }, + "application/json-patch+json"); + + this.mockDriver.Verify(s => s.WithRequestBody("application/json-patch+json", @"{""foo"":42}")); + } + + [Fact] + public void WithBody_WhenCalled_AddsRequestBody() + { + this.builder.WithBody("foo,bar\nbaz,bash", "text/csv"); + + this.mockDriver.Verify(s => s.WithRequestBody("text/csv", "foo,bar\nbaz,bash")); + } + + [Fact] + public void WillRespond_RequestConfigured_ReturnsResponseBuilder() + { + this.builder.WithRequest(HttpMethod.Delete, "/foo"); + + var responseBuilder = this.builder.WillRespond(); + + responseBuilder.Should().BeOfType(); + } + + [Fact] + public void WillRespond_RequestNotConfigured_ThrowsInvalidOperationException() + { + Action action = () => this.builder.WillRespond(); + + action.Should().Throw("because the request has not been configured"); + } + + [Fact] + public void WithFileUpload_AddsRequestBody() + { + var path = Path.GetFullPath("data/test_file.jpeg"); + var fileInfo = new FileInfo(path); + + this.builder.WithFileUpload("image/jpeg", fileInfo, "file"); + + this.mockDriver.Verify(s => s.WithFileUpload("image/jpeg", path, "file")); + } + } +} From d0b9dc5c2cd4212459e248faf85bc72729d72772 Mon Sep 17 00:00:00 2001 From: Inksprout Date: Tue, 28 Mar 2023 13:07:18 +1100 Subject: [PATCH 16/18] fix test after changing pact spec to v4 --- tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index 8ba4a58d..814d4027 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -49,7 +49,7 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() var path = Path.GetFullPath("data/test_file.jpeg"); var fileInfo = new FileInfo(path); - Assert.True(File.Exists(path)); + Assert.True(File.Exists(fileInfo.FullName)); interaction.WithFileUpload(contentType, fileInfo.FullName, "file"); @@ -114,7 +114,7 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() JObject expectedPactObject = JObject.Parse(pactContents); - string contentTypeHeader = (string)pactObject["interactions"][0]["request"]["headers"]["Content-Type"]; + string contentTypeHeader = (string)pactObject["interactions"][0]["request"]["headers"]["Content-Type"][0]; Assert.Contains("multipart/form-data;", contentTypeHeader); From 18502aa66f6c46066db1a8db882b30d8bb6edad2 Mon Sep 17 00:00:00 2001 From: Inksprout Date: Tue, 28 Mar 2023 15:21:35 +1100 Subject: [PATCH 17/18] Create Verifier test --- .../Consumer.Tests/Consumer.Tests.csproj | 5 ++ .../EventsApiConsumerTestsV3.cs | 71 ++++++++++++++++++ .../EventApi/Consumer.Tests/test_file.jpeg | Bin 0 -> 760 bytes samples/EventApi/Consumer/EventsApiClient.cs | 41 ++++++++++ .../EventApi/Provider.Tests/EventAPITests.cs | 30 ++++++++ .../Provider/Controllers/EventsController.cs | 18 +++++ 6 files changed, 165 insertions(+) create mode 100644 samples/EventApi/Consumer.Tests/EventsApiConsumerTestsV3.cs create mode 100644 samples/EventApi/Consumer.Tests/test_file.jpeg diff --git a/samples/EventApi/Consumer.Tests/Consumer.Tests.csproj b/samples/EventApi/Consumer.Tests/Consumer.Tests.csproj index 99e2a773..d5845227 100644 --- a/samples/EventApi/Consumer.Tests/Consumer.Tests.csproj +++ b/samples/EventApi/Consumer.Tests/Consumer.Tests.csproj @@ -23,4 +23,9 @@ + + + PreserveNewest + + diff --git a/samples/EventApi/Consumer.Tests/EventsApiConsumerTestsV3.cs b/samples/EventApi/Consumer.Tests/EventsApiConsumerTestsV3.cs new file mode 100644 index 00000000..b987168e --- /dev/null +++ b/samples/EventApi/Consumer.Tests/EventsApiConsumerTestsV3.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Consumer.Models; +using FluentAssertions; +using FluentAssertions.Extensions; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using PactNet; +using PactNet.Matchers; +using Xunit; +using Xunit.Abstractions; + +namespace Consumer.Tests +{ + public class EventsApiConsumerTestsV3 + { + private const string Token = "SomeValidAuthToken"; + + private readonly IPactBuilderV3 pact; + + public EventsApiConsumerTestsV3(ITestOutputHelper output) + { + var config = new PactConfig + { + PactDir = "../../../pacts/", + Outputters = new[] + { + new XUnitOutput(output) + }, + DefaultJsonSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + } + }; + + var pact = Pact.V3("Event API ConsumerV3", "Event API", config); + this.pact = pact.WithHttpInteractions(); + } + + + [Fact] + public async Task UploadImage_WhenTheFileExists_Returns201() + { + string contentType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "application/octet-stream" : "image/jpeg"; + + var file = new FileInfo("test_file.jpeg"); + + + this.pact + .UponReceiving($"a request to upload a file") + .WithRequest(HttpMethod.Post, $"/events/upload-file") + .WithFileUpload(contentType, file, "file") + .WillRespond() + .WithStatus(201); + + await this.pact.VerifyAsync(async ctx => + { + var client = new EventsApiClient(ctx.MockServerUri, Token); + + var result = await client.UploadFile(file); + + result.Should().BeEquivalentTo(HttpStatusCode.Created); + }); + } + } +} diff --git a/samples/EventApi/Consumer.Tests/test_file.jpeg b/samples/EventApi/Consumer.Tests/test_file.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e9164f1ca8e1f6febbe52e16e4f1fcc1aa97a2c6 GIT binary patch literal 760 zcmbu7y-EW?6ot=ZMNEj8#RwKDwn&l25G}+aAY?INMKG)iMhi(cKTZDFM0^45ENtyW z5Z^#s3ky31L1JvgClEo~^-PwqQ6d((!|Z&Vd-u+rnd@|%SA*6_;a@3oa)mAGDio#$+Yio2E`Kl4F$tE)1~Mv}OD;W0JM%zT8WitTKc0Db62233SSNERg-AOZV=b4|E%;_ta zfsadIG6zgQ0D;|}kDotk)98w@t_DX1Rg|E?Kpht1qy;1}k45JNI-heilSxL)z`vav zs1vf?6rsYW3PlypEy8Yt^pDDQT&f{4p!AUH472={{Q()Q{!-b`T?7@uT@)3|h+p^; G&hs}q4qVm% literal 0 HcmV?d00001 diff --git a/samples/EventApi/Consumer/EventsApiClient.cs b/samples/EventApi/Consumer/EventsApiClient.cs index daee6801..5d780aeb 100644 --- a/samples/EventApi/Consumer/EventsApiClient.cs +++ b/samples/EventApi/Consumer/EventsApiClient.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using Consumer.Models; @@ -216,7 +218,46 @@ public async Task CreateEvent(Guid eventId, string eventType = "DetailsView") Dispose(request, response); } } + public async Task UploadFile(FileInfo file) + { + + using var fileStream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read); + + var request = new MultipartFormDataContent(); + request.Headers.ContentType.MediaType = "multipart/form-data"; + + var fileContent = new StreamContent(fileStream); + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/jpeg"); + + var fileName = file.Name; + var fileNameBytes = Encoding.UTF8.GetBytes(fileName); + var encodedFileName = Convert.ToBase64String(fileNameBytes); + request.Add(fileContent, "file", fileName); + request.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") + { + Name = "file", + FileName = fileName, + FileNameStar = $"utf-8''{encodedFileName}" + }; + HttpResponseMessage response = await this.httpClient.PostAsync("/events/upload-file", request); + try + { + var statusCode = response.StatusCode; + if (statusCode == HttpStatusCode.Created) + { + return statusCode; + } + throw new HttpRequestException( + string.Format("The Events API request for POST /upload-file failed. Response Status: {0}, Response Body: {1}", + response.StatusCode, + await response.Content.ReadAsStringAsync())); + } + finally + { + Dispose(request, response); + } + } private static async Task RaiseResponseError(HttpRequestMessage failedRequest, HttpResponseMessage failedResponse) { throw new HttpRequestException( diff --git a/samples/EventApi/Provider.Tests/EventAPITests.cs b/samples/EventApi/Provider.Tests/EventAPITests.cs index 8c62e4f2..e527baf8 100644 --- a/samples/EventApi/Provider.Tests/EventAPITests.cs +++ b/samples/EventApi/Provider.Tests/EventAPITests.cs @@ -50,5 +50,35 @@ public void EnsureEventApiHonoursPactWithConsumer() .WithSslVerificationDisabled() .Verify(); } + [Fact] + public void EnsureEventApiHonoursPactWithConsumerV3() + { + var config = new PactVerifierConfig + { + LogLevel = PactLogLevel.Information, + Outputters = new List + { + new XUnitOutput(this.output) + } + }; + + string pactPath = Path.Combine("..", + "..", + "..", + "..", + "Consumer.Tests", + "pacts", + "Event API ConsumerV3-Event API.json"); + + //Act / Assert + IPactVerifier verifier = new PactVerifier(config); + verifier + .ServiceProvider("Event API", this.fixture.ServerUri) + .WithFileSource(new FileInfo(pactPath)) + .WithProviderStateUrl(new Uri(this.fixture.ServerUri, "/provider-states")) + .WithRequestTimeout(TimeSpan.FromSeconds(2)) + .WithSslVerificationDisabled() + .Verify(); + } } } diff --git a/samples/EventApi/Provider/Controllers/EventsController.cs b/samples/EventApi/Provider/Controllers/EventsController.cs index f0f62d22..34cb6ea3 100644 --- a/samples/EventApi/Provider/Controllers/EventsController.cs +++ b/samples/EventApi/Provider/Controllers/EventsController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Provider.Api.Web.Models; @@ -41,6 +42,23 @@ public IActionResult Post(Event @event) : this.StatusCode((int)HttpStatusCode.Created); } + [HttpPost] + [Route("upload-file")] + [Consumes("multipart/form-data")] + public IActionResult FileUpload() + { + var singleFile = Request.Form.Files.SingleOrDefault(f => f.Name == "file"); + if (singleFile == null || Request.Form.Files.Count != 1) + { + return BadRequest("Request must contain a single file with a parameter named 'file'"); + } + if (singleFile.ContentType != "image/jpeg") + { + return BadRequest("File content-type must be image/jpeg"); + } + return StatusCode(201); + } + private IEnumerable GetAllEventsFromRepo() { return new List From 3246f34535766d645eb6cd97527330b7d7c9c918 Mon Sep 17 00:00:00 2001 From: Inksprout Date: Tue, 28 Mar 2023 15:58:46 +1100 Subject: [PATCH 18/18] skip tests on windows and update docs --- README.md | 31 +++++++++++++++++++ .../Consumer.Tests/Consumer.Tests.csproj | 1 + .../EventsApiConsumerTestsV3.cs | 7 +++-- .../EventApi/Provider.Tests/EventAPITests.cs | 7 +++-- .../Provider.Tests/Provider.Tests.csproj | 1 + .../Drivers/FfiIntegrationTests.cs | 7 +++-- tests/PactNet.Tests/PactNet.Tests.csproj | 1 + 7 files changed, 49 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1b90e8b1..7cc44ef4 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,37 @@ For writing messaging pacts instead of requests/response pacts, see the [messagi ![----------](https://raw.githubusercontent.com/pactumjs/pactum/master/assets/rainbow.png) +### Multipart/form-data Content-Type Support + +Pact-Net supports API Requests where a single file is uploaded using the multipart/form-data content-type using the `WithFileUpload` method. + +```csharp +this.pact + .UponReceiving($"a request to upload a file") + .WithRequest(HttpMethod.Post, $"/events/upload-file") + .WithFileUpload(contentType, fileInfo, "fileName") + .WillRespond() + .WithStatus(201); +``` +### Params + +contentType : The content-type of the file being uploaded, ie `image/jpeg`. Separate from the +content-type header of the request. + +fileInfo : The FileInfo of the file being uploaded. + +fileName : A string representing the name of the file being uploaded. + +### Limitations + +- The content-type of the file being uploaded will be verified when running the pact tests in a Unix environment such as your CI/CD environment. +- This feature is unsupported in Windows so it is recommended to ignore any tests that use WithFileUpload when running tests in a Windows environment. +- This is because the Pact core relies on the [Shared MIME-info Database](https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html), which is supported on Unix, can be installed in OSX and is not supported on Windows. +- Multipart/form-data payloads that contain more than one part are not supported currently. The recommended work around is to create a string payload with the expected parts and add this to the pact file using the .WithBody() method, which allows an arbitrary string to be set as the body. + +![----------](https://raw.githubusercontent.com/pactumjs/pactum/master/assets/rainbow.png) + + ## Compatibility ### Operating System diff --git a/samples/EventApi/Consumer.Tests/Consumer.Tests.csproj b/samples/EventApi/Consumer.Tests/Consumer.Tests.csproj index d5845227..62c01ace 100644 --- a/samples/EventApi/Consumer.Tests/Consumer.Tests.csproj +++ b/samples/EventApi/Consumer.Tests/Consumer.Tests.csproj @@ -12,6 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/samples/EventApi/Consumer.Tests/EventsApiConsumerTestsV3.cs b/samples/EventApi/Consumer.Tests/EventsApiConsumerTestsV3.cs index b987168e..9ace0b3c 100644 --- a/samples/EventApi/Consumer.Tests/EventsApiConsumerTestsV3.cs +++ b/samples/EventApi/Consumer.Tests/EventsApiConsumerTestsV3.cs @@ -42,11 +42,14 @@ public EventsApiConsumerTestsV3(ITestOutputHelper output) this.pact = pact.WithHttpInteractions(); } + [SkippableFact] - [Fact] + // Feature not supported on Windows public async Task UploadImage_WhenTheFileExists_Returns201() { - string contentType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "application/octet-stream" : "image/jpeg"; + Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + string contentType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "application/octet-stream" : "image/jpeg"; var file = new FileInfo("test_file.jpeg"); diff --git a/samples/EventApi/Provider.Tests/EventAPITests.cs b/samples/EventApi/Provider.Tests/EventAPITests.cs index e527baf8..a7fa4989 100644 --- a/samples/EventApi/Provider.Tests/EventAPITests.cs +++ b/samples/EventApi/Provider.Tests/EventAPITests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using PactNet; using PactNet.Infrastructure.Outputters; using PactNet.Verifier; @@ -50,9 +51,12 @@ public void EnsureEventApiHonoursPactWithConsumer() .WithSslVerificationDisabled() .Verify(); } - [Fact] + [SkippableFact] public void EnsureEventApiHonoursPactWithConsumerV3() { + // Feature not supported on Windows + Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + var config = new PactVerifierConfig { LogLevel = PactLogLevel.Information, @@ -70,7 +74,6 @@ public void EnsureEventApiHonoursPactWithConsumerV3() "pacts", "Event API ConsumerV3-Event API.json"); - //Act / Assert IPactVerifier verifier = new PactVerifier(config); verifier .ServiceProvider("Event API", this.fixture.ServerUri) diff --git a/samples/EventApi/Provider.Tests/Provider.Tests.csproj b/samples/EventApi/Provider.Tests/Provider.Tests.csproj index 5b6982af..ee881350 100644 --- a/samples/EventApi/Provider.Tests/Provider.Tests.csproj +++ b/samples/EventApi/Provider.Tests/Provider.Tests.csproj @@ -12,6 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index 814d4027..e12f882e 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -29,9 +29,12 @@ public FfiIntegrationTests(ITestOutputHelper output) NativeInterop.LogToBuffer(LevelFilter.Trace); } - [Fact] + [SkippableFact] public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() { + // Feature not supported on Windows + Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + var driver = new PactDriver(); try @@ -42,7 +45,7 @@ public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); - string contentType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "application/octet-stream" : "image/jpeg"; + string contentType = "image/jpeg"; interaction.Given("provider state"); interaction.WithRequest("POST", "/path"); diff --git a/tests/PactNet.Tests/PactNet.Tests.csproj b/tests/PactNet.Tests/PactNet.Tests.csproj index b79b54da..c1cda277 100644 --- a/tests/PactNet.Tests/PactNet.Tests.csproj +++ b/tests/PactNet.Tests/PactNet.Tests.csproj @@ -36,6 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all +