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/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 99e2a773..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 + @@ -23,4 +24,9 @@ + + + PreserveNewest + + diff --git a/samples/EventApi/Consumer.Tests/EventsApiConsumerTestsV3.cs b/samples/EventApi/Consumer.Tests/EventsApiConsumerTestsV3.cs new file mode 100644 index 00000000..9ace0b3c --- /dev/null +++ b/samples/EventApi/Consumer.Tests/EventsApiConsumerTestsV3.cs @@ -0,0 +1,74 @@ +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(); + } + + [SkippableFact] + + // Feature not supported on Windows + public async Task UploadImage_WhenTheFileExists_Returns201() + { + Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + 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 00000000..e9164f1c Binary files /dev/null and b/samples/EventApi/Consumer.Tests/test_file.jpeg differ 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..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,5 +51,37 @@ public void EnsureEventApiHonoursPactWithConsumer() .WithSslVerificationDisabled() .Verify(); } + [SkippableFact] + public void EnsureEventApiHonoursPactWithConsumerV3() + { + // Feature not supported on Windows + Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + 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"); + + 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.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/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 diff --git a/src/PactNet.Abstractions/IRequestBuilder.cs b/src/PactNet.Abstractions/IRequestBuilder.cs index cc65edab..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; @@ -206,6 +207,15 @@ public interface IRequestBuilderV3 /// Fluent builder 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 + /// The name of the file being uploaded as a part + /// Fluent builder + 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 a91d6b4f..a62d4db1 100644 --- a/src/PactNet/Drivers/HttpInteractionDriver.cs +++ b/src/PactNet/Drivers/HttpInteractionDriver.cs @@ -97,5 +97,14 @@ public void WithRequestBody(string contentType, string body) /// Serialised body public void WithResponseBody(string contentType, string body) => 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 + 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 ccb1473e..ac5acf8e 100644 --- a/src/PactNet/Drivers/IHttpInteractionDriver.cs +++ b/src/PactNet/Drivers/IHttpInteractionDriver.cs @@ -55,5 +55,13 @@ internal interface IHttpInteractionDriver : IProviderStateDriver /// Context type /// 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 + /// + /// path to file being uploaded + /// Content type override + /// the name of the mime part being uploaded + void WithFileUpload(string filePath, string contentType, string mimePartName); } } diff --git a/src/PactNet/Drivers/InteropActionExtensions.cs b/src/PactNet/Drivers/InteropActionExtensions.cs index d58354a7..5cc59baa 100644 --- a/src/PactNet/Drivers/InteropActionExtensions.cs +++ b/src/PactNet/Drivers/InteropActionExtensions.cs @@ -1,4 +1,6 @@ -using PactNet.Exceptions; +using System.Runtime.InteropServices; +using PactNet.Exceptions; +using PactNet.Interop; namespace PactNet.Drivers { @@ -19,5 +21,19 @@ public static void CheckInteropSuccess(this bool success) throw new PactFailureException("Unable to perform the given action. The interop call indicated failure"); } } + + /// + /// 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) + { + 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 5fd96e3c..f6d246d1 100644 --- a/src/PactNet/Interop/NativeInterop.cs +++ b/src/PactNet/Interop/NativeInterop.cs @@ -63,6 +63,9 @@ internal static class NativeInterop [DllImport(DllName, EntryPoint = "pactffi_with_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 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/Interop/StringResult.cs b/src/PactNet/Interop/StringResult.cs new file mode 100644 index 00000000..5a85c2e5 --- /dev/null +++ b/src/PactNet/Interop/StringResult.cs @@ -0,0 +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 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 617f743d..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; @@ -243,6 +244,16 @@ IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSet IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSettings settings, string 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 + /// >file info of the file being uploaded + /// The name of the file being uploaded as a part + /// Fluent builder + 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 /// @@ -392,6 +403,18 @@ internal RequestBuilder WithJsonBody(dynamic body, JsonSerializerSettings settin return this.WithBody(serialised, contentType); } + /// + /// Set a body which is multipart/form-data but contains only one part, which is a file upload + /// + /// file info of the file being uploaded + /// Content type override + /// The name of the mime part being uploaded + /// Fluent builder + internal RequestBuilder WithFileUpload(string contentType, FileInfo fileInfo, string partName = "file") + { + this.driver.WithFileUpload(contentType, fileInfo.FullName, partName = "file"); + return this; + } /// /// A pre-formatted body which should be used as-is for the request /// diff --git a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs index 81ab072e..e12f882e 100644 --- a/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs +++ b/tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs @@ -2,9 +2,12 @@ using System.IO; 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; +using Newtonsoft.Json.Linq; using PactNet.Drivers; using PactNet.Interop; using Xunit; @@ -26,6 +29,107 @@ public FfiIntegrationTests(ITestOutputHelper output) NativeInterop.LogToBuffer(LevelFilter.Trace); } + [SkippableFact] + public async Task HttpInteraction_v3_CreatesPactFile_WithMultiPartRequest() + { + // Feature not supported on Windows + Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + var driver = new PactDriver(); + + try + { + IHttpPactDriver pact = driver.NewHttpPact("NativeDriverTests-Consumer-V3", + "NativeDriverTests-Provider-Multipart", + PactSpecification.V4); + + IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction"); + + string contentType = "image/jpeg"; + + 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(fileInfo.FullName)); + + interaction.WithFileUpload(contentType, fileInfo.FullName, "file"); + + 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 }; + + 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}"); + + mockServer.MockServerMismatches().Should().Be("[]"); + + logs.Should().NotBeEmpty(); + + this.output.WriteLine("Mock Server Logs"); + this.output.WriteLine("----------------"); + this.output.WriteLine(logs); + + 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"][0]; + 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() { diff --git a/tests/PactNet.Tests/PactNet.Tests.csproj b/tests/PactNet.Tests/PactNet.Tests.csproj index eee03509..c1cda277 100644 --- a/tests/PactNet.Tests/PactNet.Tests.csproj +++ b/tests/PactNet.Tests/PactNet.Tests.csproj @@ -36,10 +36,20 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + PreserveNewest + + + PreserveNewest + + + diff --git a/tests/PactNet.Tests/RequestBuilderTests.cs b/tests/PactNet.Tests/RequestBuilderTests.cs index c1ce1749..0c769ef1 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; @@ -195,5 +196,16 @@ public void WillRespond_RequestNotConfigured_ThrowsInvalidOperationException() 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")); + } } } diff --git a/tests/PactNet.Tests/data/test_file.jpeg b/tests/PactNet.Tests/data/test_file.jpeg new file mode 100644 index 00000000..e9164f1c Binary files /dev/null and b/tests/PactNet.Tests/data/test_file.jpeg differ 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..ddf47e55 --- /dev/null +++ b/tests/PactNet.Tests/data/v3-server-integration-MultipartFormDataBody.json @@ -0,0 +1,69 @@ +{ + "consumer": { + "name": "NativeDriverTests-Consumer-V3" + }, + "interactions": [ + { + "description": "a sample interaction", + "providerStates": [ + { + "name": "provider state" + } + ], + "request": { + "body": "LS1MQ1NqcTNxdWtmYmc2WjJTDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9ImZpbGUiOyBmaWxlbmFtZT0idGVzdF9maWxlLmpwZWciDQpDb250ZW50LVR5cGU6IGltYWdlL2pwZWcNCg0K/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAABv/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAVAQEBAAAAAAAAAAAAAAAAAAAJCv/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AC6lAjD/2Q0KLS1MQ1NqcTNxdWtmYmc2WjJTLS0NCg==", + "headers": { + "Content-Type": "multipart/form-data; boundary=LCSjq3qukfbg6Z2S" + }, + "matchingRules": { + "body": { + "$.file": { + "combine": "AND", + "matchers": [ + { + "match": "contentType", + "value": "application/octet-stream" + } + ] + } + }, + "header": { + "Content-Type": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "multipart/form-data;(\\s*charset=[^;]*;)?\\s*boundary=.*" + } + ] + } + } + }, + "method": "POST", + "path": "/path" + }, + "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-Multipart" + } +} \ No newline at end of file