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

V4InteractionWithCompleteRequest is unimplemented #1224

Open
4 of 5 tasks
shishkin opened this issue Jun 21, 2024 · 9 comments
Open
4 of 5 tasks

V4InteractionWithCompleteRequest is unimplemented #1224

shishkin opened this issue Jun 21, 2024 · 9 comments
Labels
awaiting feedback Awaiting Feedback from OP bug Indicates an unexpected problem or unintended behavior help wanted Indicates that a maintainer wants help on an issue or pull request triage This issue is yet to be triaged by a maintainer

Comments

@shishkin
Copy link

shishkin commented Jun 21, 2024

I'm trying to write a pact for multipart/form-data with file upload. When I use V4 DSL withRequest I get body content type mismatch because my test uses random boundary for multipart body. Then I tried withCompleteRequest so I can just use regex matcher for the body. PactJS gives me Error: V4InteractionWithCompleteRequest is unimplemented.

Software versions

  • OS: MacOS 14.5
  • Consumer Pact library: Pact JS 12.5.2
  • Provider Pact library: Pact JS 12.5.2
  • Node Version: v20.11.1

Issue Checklist

Please confirm the following:

  • I have upgraded to the latest
  • I have the read the FAQs in the Readme
  • I have triple checked, that there are no unhandled promises in my code and have read the section on intermittent test failures
  • I have set my log level to debug and attached a log file showing the complete request/response cycle
  • For bonus points and virtual high fives, I have created a reproduceable git repository (see below) to illustrate the problem

Expected behaviour

There should be a way to relax request matching while still providing a specific example for provider verification.

Actual behaviour

Error: V4InteractionWithCompleteRequest is unimplemented

Steps to reproduce

await new PactV4({
        provider: "P",
        consumer: "C",
        logLevel: "debug",
    })
    .addInteraction()
    .uponReceiving("...")
    .withCompleteRequest({
        method: "POST",
        path: "/upload-file",
        headers: {
            accept: M.regex(/application\/json/, "application/json"),
            "content-type": M.regex(
                /^multipart\/form-data.+$/,
                `multipart/form-data; boundary=${boundary}`,
            ),
        },
        body: M.regex(/.*/, bodyBuffer.toString("utf-8")),
    })
    .withCompleteResponse({
        status: 200,
        contentType: "application/json; charset=utf-8",
        body: {
            success: true,
        },
    })
    .executeTest(async (server) => {
        const form = new FormData();
        form.append("uploadPath", "/blog");
        form.append(
            "files",
            await openAsBlob(testImagePath),
            "image.png",
        );
        await fetch(new URL("/upload-file", server.url), {
            method: "POST",
            headers: {
                accept: "application/json",
            },
            body: form,
        });
    });

Relevant log files

2024-06-21T05:18:39.925518Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server:
      ----------------------------------------------------------------------------------------
       method: POST
       path: /upload-file
       query: None
       headers: Some({"connection": ["keep-alive"], "host": ["127.0.0.1:58624"], "accept-language": ["*"], "accept-encoding": ["gzip", "deflate"], "content-length": ["363"], "accept": ["application/json"], "user-agent": ["node"], "sec-fetch-mode": ["cors"], "content-type": ["multipart/form-data; boundary=----formdata-undici-074263581352"]})
       body: Present(363 bytes, multipart/form-data;boundary=----formdata-undici-074263581352)
      ----------------------------------------------------------------------------------------

2024-06-21T05:18:39.925555Z  INFO tokio-runtime-worker pact_matching: comparing to expected HTTP Request ( method: POST, path: /upload-file, query: None, headers: Some({"accept": ["application/json"], "content-type": ["multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"]}), body: Present(362 bytes) )
2024-06-21T05:18:39.925564Z DEBUG tokio-runtime-worker pact_matching:      body: '2D2D2D2D5765624B6974466F726D426F756E64617279374D41345957786B5472... (362 bytes)'
2024-06-21T05:18:39.925565Z DEBUG tokio-runtime-worker pact_matching:      matching_rules: MatchingRules { rules: {BODY: MatchingRuleCategory { name: BODY, rules: {DocPath { path_tokens: [Root], expr: "$" }: RuleList { rules: [ContentType("multipart/form-data")], rule_logic: And, cascaded: false }} }, PATH: MatchingRuleCategory { name: PATH, rules: {} }, HEADER: MatchingRuleCategory { name: HEADER, rules: {DocPath { path_tokens: [Root, Field("content-type")], expr: "$['content-type']" }: RuleList { rules: [Regex("^multipart\\/form-data.+$")], rule_logic: And, cascaded: false }} }} }
2024-06-21T05:18:39.925573Z DEBUG tokio-runtime-worker pact_matching:      generators: Generators { categories: {} }
2024-06-21T05:18:39.925594Z DEBUG tokio-runtime-worker pact_matching::matchers: String -> String: comparing '/upload-file' to '/upload-file' ==> true cascaded=false matcher=Equality
2024-06-21T05:18:39.925602Z DEBUG tokio-runtime-worker pact_matching: expected content type = 'multipart/form-data;boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW', actual content type = 'multipart/form-data;boundary=----formdata-undici-074263581352'
2024-06-21T05:18:39.925619Z DEBUG tokio-runtime-worker pact_matching: content type header matcher = 'RuleList { rules: [], rule_logic: And, cascaded: false }'
2024-06-21T05:18:39.925670Z DEBUG tokio-runtime-worker pact_matching::matchers: String -> String: comparing 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' to 'multipart/form-data; boundary=----formdata-undici-074263581352' ==> true cascaded=false matcher=Regex("^multipart\\/form-data.+$")
2024-06-21T05:18:39.925677Z DEBUG tokio-runtime-worker pact_matching: --> Mismatches: [BodyTypeMismatch { expected: "multipart/form-data;boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW", actual: "multipart/form-data;boundary=----formdata-undici-074263581352", mismatch: "Expected a body of 'multipart/form-data;boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' but the actual content type was 'multipart/form-data;boundary=----formdata-undici-074263581352'", expected_body: Some(b"----WebKitFormBoundary7MA4YWxkTrZu0gW\nContent-Disposition: form-data; name=\"files\"; filename=\"white.png\"\nContent-Type: application/octet-stream\n\n\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\0\x01\0\0\0\x01\x08\x06\0\0\0\x1f\x15\xc4\x89\0\0\0\x01sRGB\0\xae\xce\x1c\xe9\0\0\0\rIDAT\x18Wc\xf8\xff\xef\xdf\x7f\0\t\xf6\x03\xfbW\xfe{\x1b\0\0\0\0IEND\xaeB`\x82\n----WebKitFormBoundary7MA4YWxkTrZu0gW\nContent-Disposition: form-data; name=\"uploadPath\"\n\n/blog\n----WebKitFormBoundary7MA4YWxkTrZu0gW\n"), actual_body: Some(b"------formdata-undici-074263581352\r\nContent-Disposition: form-data; name=\"uploadPath\"\r\n\r\n/blog\r\n------formdata-undici-074263581352\r\nContent-Disposition: form-data; name=\"files\"; filename=\"image.png\"\r\nContent-Type: application/octet-stream\r\n\r\n\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\0\x01\0\0\0\x01\x08\x06\0\0\0\x1f\x15\xc4\x89\0\0\0\x01sRGB\0\xae\xce\x1c\xe9\0\0\0\rIDAT\x18Wc\xf8\xff\xef\xdf\x7f\0\t\xf6\x03\xfbW\xfe{\x1b\0\0\0\0IEND\xaeB`\x82\r\n------formdata-undici-074263581352--") }]
2024-06-21T05:18:39.925738Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server: Request did not match: Request did not match - HTTP Request ( method: POST, path: /upload-file, query: None, headers: Some({"accept": ["application/json"], "content-type": ["multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"]}), body: Present(362 bytes) )    0) Expected a body of 'multipart/form-data;boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' but the actual content type was 'multipart/form-data;boundary=----formdata-undici-074263581352'
@shishkin shishkin added bug Indicates an unexpected problem or unintended behavior triage This issue is yet to be triaged by a maintainer labels Jun 21, 2024
@mefellows mefellows added help wanted Indicates that a maintainer wants help on an issue or pull request good first issue Indicates a good issue for first-time contributors labels Jun 21, 2024
@mefellows
Copy link
Member

Even if we were to implement the V4InteractionWithCompleteRequest method, it would not solve your problem - regex only applies to fields on JSON bodies.

You could use the PactV3 interface, and just set the spec version to 4 (I think that's possible), for equivalent capability.

It should be straightforward enough to implement the requested feature though if you're keen to contribute that.

As to your actual need:

mismatch because my test uses random boundary for multipart body

What we need, possibly, is to support multipart requests with random boundaries.

I'm confused by this part of your code though:

    .withCompleteRequest({
        method: "POST",
        path: "/upload-file",
        headers: {
            accept: M.regex(/application\/json/, "application/json"),
            "content-type": M.regex(
                /^multipart\/form-data.+$/,
                `multipart/form-data; boundary=${boundary}`, // <------- do you know the boundary here?
            ),
        },
        body: M.regex(/.*/, bodyBuffer.toString("utf-8")),
    })

@mefellows mefellows added awaiting feedback Awaiting Feedback from OP and removed good first issue Indicates a good issue for first-time contributors labels Jun 21, 2024
@shishkin
Copy link
Author

do you know the boundary here?

That is the example boundary I want to send to the provider. I extracted it into a variable because it's used multiple times for constructing example bodyBuffer. But I don't know the boundary that node fetch will use inside the executeTest.

To be honest, I'm very confused by pact architecture and support roadmap. All language libraries seems to deviate a lot from each other and the zoo of versions (V2/V3/V4) and indirection layers (JS/native/ffi/...) is incomprehensible. Where do I even start?

@mefellows
Copy link
Member

Some background on the ecosystem here: https://docs.pact.io/diagrams/ecosystem

The spec versions (V2/V3/V4 as you say) relate to the contract file serialisation format and supported features the contract file can model.

The FFI is the shared library where the key business logic resides to avoid repeating the effort across 10+ languages (and multiple spec versions).

In this case, that particular method you wanted can call the existing FFI methods that are already exposed to Pact JS (example for the V3 interface: https://github.com/pact-foundation/pact-js/blob/master/src/v3/pact.ts#L111-L121). Ultimately, they would just call the same functions that the type-state builder DSL already calls.


@rholshausen is there a way to have the mock server match multipart bodies where the boundary name is unknown in advance? I've looked at the fetch API and other common clients and most don't seem to have a way to set them (i.e. they're generated).

@rholshausen
Copy link

The mock server does not need to know the boundary in advance. As long as the boundary value in the content type header matches that used in the body, it will parse it correctly (i.e. it has to be a correctly formed multipart body).

The main issue is that the actual and expected content type headers will have values that don't match. That is why the regex for the header is important.

@rholshausen
Copy link

The issue is seen in the logs above. The content type header is been treated as different, so the body is not being compared.

@mefellows
Copy link
Member

As long as the boundary value in the content type header matches that used in the body

Sorry, that's the point I think - a lot of the HTTP clients don't let you specify the boundary names (they are generated) so you can't specify the boundary attribute in the content-type header - this is what I meant by telling the mock server in advance.

@rholshausen
Copy link

Normally both the content type header and body is generated. You need to be able to retrieve the header, or get the header to be set.

@shishkin
Copy link
Author

The main issue is that the actual and expected content type headers will have values that don't match. That is why the regex for the header is important.

I believe that it the problem, because Pact JS DSL requires body content type to be a fixed string and not a RegEx. So I'm not able to specify the content type as a RegEx in both the headers matcher and the body example.

@mefellows
Copy link
Member

That's slightly different, I think. The implementation for headers calls a separate FFI method:

this.interaction.withRequestHeader(
`${k}`,
index,
matcherValueOrString(header)
);
}
which calls the underlying FFI method: pactffi_with_header_v2. This is what sets the matching on the content-type header.

The method you linked to identifies the specific content type on the body, if known (see https://github.com/pact-foundation/pact-reference/blob/efc54d263e7ea53c9511cf389870e790c0158bc3/rust/pact_ffi/src/mock_server/handles.rs#L1861).

If you shared a wider log file @shishkin you would likely see the content-type header matching rules setup, along with your regex.

@rholshausen correct me if I'm wrong here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting feedback Awaiting Feedback from OP bug Indicates an unexpected problem or unintended behavior help wanted Indicates that a maintainer wants help on an issue or pull request triage This issue is yet to be triaged by a maintainer
Projects
None yet
Development

No branches or pull requests

3 participants