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