Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat multipart file upload support #447

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/PactNet.Abstractions/IRequestBuilder.cs
Original file line number Diff line number Diff line change
@@ -204,7 +204,14 @@ public interface IRequestBuilderV3
/// <param name="body">Request body</param>
/// <param name="contentType">Content type</param>
/// <returns>Fluent builder</returns>
IRequestBuilderV3 WithBody(string body, string contentType);
IRequestBuilderV3 WithBody(string body, string contentType);

/// <summary>
/// A Multipart body containing a single part, which is an uploaded file
/// </summary>
/// <param name="filePath">The absolute path of the file being uploaded</param>
/// <returns>Fluent builder</returns>
IRequestBuilderV3 WithMultipartSingleFileUpload(string filePath);

// TODO: Support binary and multi-part body

11 changes: 10 additions & 1 deletion src/PactNet/Drivers/HttpInteractionDriver.cs
Original file line number Diff line number Diff line change
@@ -96,6 +96,15 @@ public void WithRequestBody(string contentType, string body)
/// <param name="contentType">Context type</param>
/// <param name="body">Serialised body</param>
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();

/// <summary>
/// Set the request body to multipart/form-data for file upload
/// </summary>
/// <param name="contentType">Content type override</param>
/// <param name="filePath">path to file being uploaded</param>
/// <param name="mimePartName">the name of the mime part being uploaded</param>
public void WithMultipartSingleFileUpload(string contentType, string filePath, string mimePartName)
=> NativeInterop.WithMultipartSingleFileUpload(this.interaction, InteractionPart.Request, contentType, filePath, mimePartName).CheckInteropSuccess();
}
}
10 changes: 9 additions & 1 deletion src/PactNet/Drivers/IHttpInteractionDriver.cs
Original file line number Diff line number Diff line change
@@ -54,6 +54,14 @@ internal interface IHttpInteractionDriver : IProviderStateDriver
/// </summary>
/// <param name="contentType">Context type</param>
/// <param name="body">Serialised body</param>
void WithResponseBody(string contentType, string body);
void WithResponseBody(string contentType, string body);

/// <summary>
/// Set the response body for a single file to be uploaded as a multipart/form-data content type
/// </summary>
/// <param name="filePath">path to file being uploaded</param>
/// <param name="contentType">Content type override</param>
/// <param name="mimePartName">the name of the mime part being uploaded</param>
void WithMultipartSingleFileUpload(string filePath, string contentType, string mimePartName);
}
}
15 changes: 13 additions & 2 deletions src/PactNet/Drivers/InteropActionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using PactNet.Exceptions;

using System.Runtime.InteropServices;
using PactNet.Exceptions;
using PactNet.Interop;

namespace PactNet.Drivers
{
/// <summary>
@@ -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 returned failure: {errorMsg}");
}
}
}
}
5 changes: 4 additions & 1 deletion src/PactNet/Interop/NativeInterop.cs
Original file line number Diff line number Diff line change
@@ -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 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);
36 changes: 36 additions & 0 deletions src/PactNet/Interop/StringResult.cs
Original file line number Diff line number Diff line change
@@ -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 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;
}
}
26 changes: 23 additions & 3 deletions src/PactNet/RequestBuilder.cs
Original file line number Diff line number Diff line change
@@ -241,8 +241,16 @@ IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSet
/// <param name="contentType">Content type override</param>
/// <returns>Fluent builder</returns>
IRequestBuilderV3 IRequestBuilderV3.WithJsonBody(dynamic body, JsonSerializerSettings settings, string contentType)
=> this.WithJsonBody(body, settings, contentType);

=> this.WithJsonBody(body, settings, contentType);

/// <summary>
/// Set a body which is multipart/form-data but contains only one part, which is a file upload
/// </summary>
/// <param name="filePath">Path to the file being uploaded</param>
/// <returns>Fluent builder</returns>
IRequestBuilderV3 IRequestBuilderV3.WithMultipartSingleFileUpload(string filePath)
=> this.WithMultipartSingleFileUpload(filePath, "multipart/form-data", "file");

/// <summary>
/// A pre-formatted body which should be used as-is for the request
/// </summary>
@@ -390,8 +398,20 @@ internal RequestBuilder WithJsonBody(dynamic body, JsonSerializerSettings settin
{
string serialised = JsonConvert.SerializeObject(body, settings);
return this.WithBody(serialised, contentType);
}

/// <summary>
/// Set a body which is multipart/form-data but contains only one part, which is a file upload
/// </summary>
/// <param name="filePath">path to file being uploaded</param>
/// <param name="contentType">Content type override</param>
/// <param name="mimePartName">The name of the mime part being uploaded</param>
/// <returns>Fluent builder</returns>
internal RequestBuilder WithMultipartSingleFileUpload(string filePath, string contentType, string mimePartName)
{
this.driver.WithMultipartSingleFileUpload(filePath, contentType, mimePartName);
return this;
}

/// <summary>
/// A pre-formatted body which should be used as-is for the request
/// </summary>
139 changes: 119 additions & 20 deletions tests/PactNet.Tests/Drivers/FfiIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -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;
@@ -24,28 +27,29 @@ 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);

IHttpInteractionDriver interaction = pact.NewHttpInteraction("a sample interaction");
"NativeDriverTests-Provider-Multipart",
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.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(contentType, path, "file");

interaction.WithResponseStatus((ushort)HttpStatusCode.Created);
interaction.WithResponseHeader("X-Response-Header", "value1", 0);
@@ -54,19 +58,114 @@ 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" });

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");
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("[]");

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);
}
// 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");
9 changes: 9 additions & 0 deletions tests/PactNet.Tests/PactNet.Tests.csproj
Original file line number Diff line number Diff line change
@@ -42,4 +42,13 @@
<ProjectReference Include="..\..\src\PactNet\PactNet.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="data\test_file.jpeg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data\v3-server-integration-MultipartFormDataBody.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
11 changes: 11 additions & 0 deletions tests/PactNet.Tests/RequestBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using FluentAssertions;
using Moq;
@@ -194,6 +195,16 @@ public void WillRespond_RequestNotConfigured_ThrowsInvalidOperationException()
Action action = () => this.builder.WillRespond();

action.Should().Throw<InvalidOperationException>("because the request has not been configured");
}

[Fact]
public void WithMultipartSingleFileUpload_AddsRequestBody()
{
var path = Path.GetFullPath("data/test_file.jpeg");

this.builder.WithMultipartSingleFileUpload(path,"multipart/form-data", "file");

this.mockDriver.Verify(s => s.WithMultipartSingleFileUpload(path, "multipart/form-data", "file"));
}
}
}
Binary file added tests/PactNet.Tests/data/test_file.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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==",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this isn't what I expected to see... How can we validate that the body is actually a multi-part one in the correct format if the entire body is base64 encoded? That's made the interaction pretty opaque, which will make it hard to debug if the verifier fails.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've decoded this body and I'm confused by something. The body is:

--LCSjq3qukfbg6Z2S
Content-Disposition: form-data; name="file"; filename="test_file.jpeg"
Content-Type: image/jpeg

<binary data>
--LCSjq3qukfbg6Z2S--

but where does the Content-Type: image/jpeg come from? The public API doesn't allow you to specify the content type of a part as far as I can see. Is that being auto-detected or something? Either way, you should probably be able to set it yourself.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a similar issue to the "file" one you mentioned, I have updated the code to include this as a parameter the user sets. There is a limitation here I am planning to document however, the content type matching in the Rust library uses the Shared MIME-info Database (https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html) to do the binary comparison. This library is GNU licensed, so we can't embed it in the framework, it has to be installed in the system. It is not supported in windows, so while content matching will work as expected in a Unix type build pipeline, the content type will only come through as 'application/octet-stream' when using windows. You can see the test I added reflects this, as I have to check and set the content type appropriately so that test can pass in each of the test environments the repo has.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, that's quite the gotcha I wasn't aware of. So running the same test on Windows and Linux will produce different pact files? That doesn't sound good.

What if you run it on Linux with and without the mime detection library installed? Does it error or give you different results also?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just realised the auto detect probably breaks my use case as well, because we support multiple types of upload (e.g. you could upload a CSV or XLSX file) and we need control over the mime type so we can handle the file properly on the receiver side. There's no way to guarantee that the auto detect generates the mime we need, especially if it's a vendor specific mime.

We've had issues raised by other users when we hard coded the content type for JSON bodies as well, because sometimes people want to add extensions such as application/json+patch or they have to integrate with other systems which don't behave themselves.

Also, the round trip integration test that I've asked for will fail in CI because we run it on all 3 major OSs and verify the file contents afterwards. A test library that produces different results on different OSs isn't really desirable. We shouldn't be building OS specific switches into the tests either, that's an obvious code smell.

I think that's big enough to be a blocker to me if the tests aren't reproducible and the reason for that is unclear to the user. That's a foot gun that's too easy to trigger, and then all the issues will start coming in.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really willing to skip the tests on Windows for a .Net library.

Windows is the main development OS for .Net by far, but obviously they also need to work in other OSs as people increasingly run in dev containers or run their CI on different OS.

A pretty typical workflow, and the one everyone uses at my work, is for people to develop on Windows but run CI and production in Linux containers. We need the same tests to produce the same results in that situation.

I honestly think this change is blocked into we get an FFI behaviour change. Fortunately the issue is linked now so we can track it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A pretty typical workflow, and the one everyone uses at my work, is for people to develop on Windows but run CI and production in Linux containers. We need the same tests to produce the same results in that situation.

If I understand correctly, the issue here will be that the CI will publish a contract with the correct MIME type e.g. image/jpeg and any verification process on a Windows development machine will fail, because the type will be detected as application/octet-stream.

If I further understand the implementation detail, even if you attempted to do the manual workaround you suggested Adam (setting content-type header, length and explicitly laying out the multi-part boundaries) the FFI will still do the mime byte check and fail with the same issue.

Am I right in assuming that @rholshausen ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is correct

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth noting that I don't think the current behaviour conforms to the spec, which says

If there is a Content-Type header, the value of the header determines the content type.

That means I'd expect to be always be able to control the content type by passing the appropriate header. In the case of multi-part then obviously the content type header applies to each part separately.

It also doesn't conform to this part:

default to either application/json (as V1) or text/plain.

Because the default is application/octet-stream instead of text/plain, and the spec notes that the magic byte test (although the spec incorrectly calls it 'magic number test') is optional.

I think the default should be that the user has to specify a content type, with an opt-in for auto-detection. That way we can document that it may work differently on different OSs and the user can conciously make that choice. If it doesn't work, e.g. because they use different OS or the OS fails auto-detection, then they've always got the option to specify manually.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That section of the spec is referring to the Content-Type of the body of a request, however the issue here is with the Content-Type of the file being uploaded. Requests with a file as part of them include both content types. In the case of using the pactffi_with_multipart_file method from the ffi, the detection of the body Content-Type should work according to the spec. The Content-type of the file being uploaded is what the pactffi_with_multipart_file method helps match. This is the content type that is not able to be detected correctly when using Windows. Hope that helps clear up the issue.

"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"
}
}