pact-foundation / pact-js

JS version of Pact. Pact is a contract testing framework for HTTP APIs and non-HTTP asynchronous messaging systems.
https://pact.io
Other
1.59k stars 343 forks source link

.withRequestMultipartFileUpload() detects application/pdf or application/octet-stream type as text/plain #1094

Open spawluk-zartis opened 1 year ago

spawluk-zartis commented 1 year ago

Software versions

Issue Checklist

Please confirm the following:

Expected behaviour

When testing multipart/form-data upload, PDF file should be detected as application/pdf or application/octet-stream

Actual behaviour

The uploaded PDF file is detected as text/plain.

Moreover on Azure DevOps agent running Ubuntu, the same file is detected as text/x-tex

I'm not 100% sure but I think it is related to mime type detection logic here

Steps to reproduce

Clone https://github.com/spawluk-zartis/pact-multipart-form-data-upload

npm install
npm test

Relevant log files

> pact-multipart-form-data-upload@1.0.0 test
> jest

 RUNS  tests/UploadPdf.test.ts
2023-05-18T08:16:51.469779Z DEBUG ThreadId(01) pact_plugin_driver::catalogue_manager: Updated catalogue entries:
core/transport/http
core/transport/https
2023-05-18T08:16:51.470707Z DEBUG ThreadId(01) pact_plugin_driver::catalogue_manager: Updated catalogue entries:
core/content-generator/binary
core/content-generator/json
core/content-matcher/json
core/content-matcher/multipart-form-data
core/content-matcher/text
core/content-matcher/xml
2023-05-18T08:16:51.472088Z DEBUG ThreadId(01) pact_plugin_driver::catalogue_manager: Updated catalogue entries:
core/matcher/v1-equality
core/matcher/v2-max-type
core/matcher/v2-min-type
core/matcher/v2-minmax-type
core/matcher/v2-regex
core/matcher/v2-type
core/matcher/v3-content-type
core/matcher/v3-date
core/matcher/v3-datetime
core/matcher/v3-decimal-type
core/matcher/v3-includes
core/matcher/v3-integer-type
core/matcher/v3-null
core/matcher/v3-number-type
core/matcher/v3-time
core/matcher/v4-array-contains
core/matcher/v4-equals-ignore-order
core/matcher/v4-max-equals-ignore-order
core/matcher/v4-min-equals-ignore-order
core/matcher/v4-minmax-equals-ignore-order
core/matcher/v4-not-empty
(node:29032) ExperimentalWarning: buffer.File is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

 RUNS  tests/UploadPdf.test.ts
2023-05-18T08:16:51.642547Z DEBUG tokio-runtime-worker hyper::proto::h1::io: parsed 7 headers
2023-05-18T08:16:51.642977Z DEBUG tokio-runtime-worker hyper::proto::h1::conn: incoming body is content-length (981 bytes)
2023-05-18T08:16:51.643376Z DEBUG tokio-runtime-worker hyper::proto::h1::conn: incoming body completed
2023-05-18T08:16:51.643730Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server: Creating pact request from hyper request
2023-05-18T08:16:51.644197Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server: Extracting query from uri /api/upload
2023-05-18T08:16:51.644765Z  INFO tokio-runtime-worker pact_mock_server::hyper_server: Received request HTTP Request ( method: POST, path: /api/upload, query: None, headers: Some({"accept": ["application/json", "text/plain", 
"*/*"], "content-type": ["multipart/form-data; boundary=axios-1.3.4-boundary-DSxfzSsQnKAavHGBv3Gy-xrx8"], "content-length": ["981"], "host": ["127.0.0.1:54119"], "connection": ["close"], "accept-encoding": ["gzip", "compress", "deflate", "br"], "user-agent": ["axios/1.3.4"]}), body: Present(981 bytes, multipart/form-data;boundary=axios-1.3.4-boundary-DSxfzSsQnKAavHGBv3Gy-xrx8) )
2023-05-18T08:16:51.645823Z  INFO tokio-runtime-worker pact_matching: comparing to expected HTTP Request ( method: POST, path: /api/upload, query: None, headers: Some({"Content-Type": ["multipart/form-data; boundary=ZLyxFXoU09gGYGlJ"]}), body: Present(924 bytes, multipart/form-data) )
2023-05-18T08:16:51.646343Z DEBUG tokio-runtime-worker pact_matching:      body: '2D2D5A4C797846586F553039674759476C4ADA436F6E74656E742D44697370... (924 bytes)'
2023-05-18T08:16:51.646615Z DEBUG tokio-runtime-worker pact_matching:      matching_rules: MatchingRules { rules: {BODY: MatchingRuleCategory { name: BODY, rules: {DocPath { path_tokens: [Root, Field("file")], expr: "$.file" 
}: RuleList { rules: [ContentType("application/pdf")], 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;(\\s*charset=[^;]*;)?\\s*boundary=.*")], rule_logic: And, cascaded: false }} }} }
2023-05-18T08:16:51.647368Z DEBUG tokio-runtime-worker pact_matching:      generators: Generators { categories: {} }
2023-05-18T08:16:51.647776Z DEBUG tokio-runtime-worker pact_matching::matchers: String -> String: comparing '/api/upload' to '/api/upload' ==> true cascaded=false matcher=Equality
2023-05-18T08:16:51.648105Z DEBUG tokio-runtime-worker pact_matching: expected content type = 'multipart/form-data', actual content type = 'multipart/form-data;boundary=axios-1.3.4-boundary-DSxfzSsQnKAavHGBv3Gy-xrx8'
2023-05-18T08:16:51.648663Z DEBUG tokio-runtime-worker pact_matching: content type header matcher = 'RuleList { rules: [Regex("multipart/form-data;(\\s*charset=[^;]*;)?\\s*boundary=.*")], rule_logic: And, cascaded: false }'  
2023-05-18T08:16:51.649001Z DEBUG tokio-runtime-worker pact_plugin_driver::catalogue_manager: Looking for a content matcher for multipart/form-data
2023-05-18T08:16:51.649982Z DEBUG tokio-runtime-worker pact_matching: No content matcher defined for content type 'multipart/form-data', using core matcher implementation
2023-05-18T08:16:51.650272Z DEBUG tokio-runtime-worker pact_matching: Using body matcher for content type 'multipart/form-data'
2023-05-18T08:16:51.650549Z DEBUG tokio-runtime-worker pact_matching::binary_utils: matching MIME multipart contents
2023-05-18T08:16:51.650986Z DEBUG tokio-runtime-worker pact_matching::binary_utils: Comparing MIME field multipart 'file'
2023-05-18T08:16:51.651291Z DEBUG tokio-runtime-worker pact_matching::binary_utils: Calling match_values for path $.file
2023-05-18T08:16:51.651574Z DEBUG tokio-runtime-worker pact_matching::binary_utils: FilePart: comparing binary data to 'Some("application/pdf")' using ContentType("application/pdf")
2023-05-18T08:16:51.652470Z DEBUG tokio-runtime-worker pact_matching::binary_utils: Matching binary contents by content type: expected 'application/pdf', detected 'text/plain' -> false
2023-05-18T08:16:51.652737Z DEBUG tokio-runtime-worker pact_models::content_types: Detecting content type from byte contents
2023-05-18T08:16:51.653087Z DEBUG tokio-runtime-worker pact_matching::binary_utils: Comparing 'MimeFile { name: "file", content_type: Some("application/pdf"), filename: "empty.pdf", data: b"%PDF-1.4\n%\xd3\xeb\xe9\xe1\n1 0 obj\n<</Title (Untitled document)\n/Producer (Skia/PDF m115 Google Docs Renderer)>>\nendobj\n3 0 obj\n<</ca 1\n/BM /Normal>>\nendobj\n4 0 obj\n<</Length 84>> stream\n1 0 0 -1 0 792 cm\nq\n.75 0 0 .75 0 0 cm\n1 1 1 RG 1 1 1 rg\n/G3 gs\n0 0 816 1056 re\nf\nQ\n\nendstream\nendobj\n2 0 obj\n<</Type /Page\n/Resources <</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]\n/ExtGState <</G3 3 0 R>>>>\n/MediaBox [0 0 612 792]\n/Contents 4 0 R\n/StructParents 0\n/Parent 5 0 R>>\nendobj\n5 0 obj\n<</Type /Pages\n/Count 1\n/Kids [2 0 R]>>\nendobj\n6 0 obj\n<</Type /Catalog\n/Pages 5 0 R>>\nendobj\nxref\n0 7\n0000000000 65535 f \n0000000015 00000 n \n0000000277 00000 n \n0000000108 00000 n \n0000000145 00000 n \n0000000465 00000 n \n0000000520 00000 n \ntrailer\n<</Size 7\n/Root 6 0 R\n/Info 1 0 R>>\nstartxref\n567\n%%EOF\n" }' to 'MimeFile { name: "file", content_type: Some("application/pdf"), filename: "blob", data: b"%PDF-1.4\n%\xd3\xeb\xe9\xe1\n1 0 obj\n<</Title (Untitled document)\n/Producer (Skia/PDF m115 Google Docs Renderer)>>\nendobj\n3 0 obj\n<</ca 1\n/BM /Normal>>\nendobj\n4 0 obj\n<</Length 84>> stream\n1 0 0 -1 0 792 cm\nq\n.75 0 0 .75 0 0 cm\n1 1 1 RG 1 1 1 rg\n/G3 gs\n0 0 816 1056 re\nf\nQ\n\nendstream\nendobj\n2 0 obj\n<</Type /Page\n/Resources <</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]\n/ExtGState <</G3 3 0 R>>>>\n/MediaBox [0 
0 612 792]\n/Contents 4 0 R\n/StructParents 0\n/Parent 5 0 R>>\nendobj\n5 0 obj\n<</Type /Pages\n/Count 1\n/Kids [2 0 R]>>\nendobj\n6 0 obj\n<</Type /Catalog\n/Pages 5 0 R>>\nendobj\nxref\n0 7\n0000000000 65535 f \n0000000015 00000 n \n0000000277 00000 n \n0000000108 00000 n \n0000000145 00000 n \n0000000465 00000 n \n0000000520 00000 n \ntrailer\n<</Size 7\n/Root 6 0 R\n/Info 1 0 R>>\nstartxref\n567\n%%EOF\n" }' at path '$.file' -> Err([BodyMismatch { path: "$.file", expected: None, actual: None, mismatch: "MIME part 'file': Expected binary contents to have content type 'application/pdf' but detected contents was 'text/plain'" }])
2023-05-18T08:16:51.655837Z DEBUG tokio-runtime-worker pact_matching::matchers: String -> String: comparing 'multipart/form-data; boundary=ZLyxFXoU09gGYGlJ' to 'multipart/form-data; boundary=axios-1.3.4-boundary-DSxfzSsQnKAavHGBv3Gy-xrx8' ==> true cascaded=false matcher=Regex("multipart/form-data;(\\s*charset=[^;]*;)?\\s*boundary=.*")
2023-05-18T08:16:51.656453Z DEBUG tokio-runtime-worker pact_matching: --> Mismatches: [BodyMismatch { path: "$.file", expected: None, actual: None, mismatch: "MIME part 'file': Expected binary contents to have content type 'application/pdf' but detected contents was 'text/plain'" }]
2023-05-18T08:16:51.657515Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server: Request did not match: Request did not match - HTTP Request ( method: POST, path: /api/upload, query: None, headers: Some({"Content-Type": ["multipart/form-data; boundary=ZLyxFXoU09gGYGlJ"]}), body: Present(924 bytes, multipart/form-data) )    0) $.file -> MIME part 'file': Expected binary contents to have content type 'application/pdf' but detected contents was 'text/plain'
2023-05-18T08:16:51.659274Z DEBUG tokio-runtime-worker hyper::proto::h1::io: flushed 2111 bytes
2023-05-18T08:16:51.666657Z DEBUG ThreadId(01) pact_matching::metrics: Could not get the tokio runtime, will not send metrics - there is no reactor running, must be called from the context of a Tokio 1.x runtime
2023-05-18T08:16:51.667054Z DEBUG ThreadId(01) pact_mock_server::server_manager: Shutting down mock server with ID 46ce9b3e-800a-492b-88fc-7335e03b3065 - MockServerMetrics { requests: 1 }
2023-05-18T08:16:51.667407Z DEBUG ThreadId(01) pact_mock_server::mock_server: Mock server 46ce9b3e-800a-492b-88fc-7335e03b3065 shutdown - MockServerMetrics { requests: 1 }
2023-05-18T08:16:51.667432Z DEBUG tokio-runtime-worker hyper::server::shutdown: signal received, starting graceful shutdown
[10:16:51.669] ERROR (29032): pact@11.0.2: Test failed for the following reasons:

  Mock server failed with the following mismatches:

        0) The following request was incorrect:

                POST /api/upload

                         1.0    $.file: MIME part 'file': Expected binary contents to have content type 'application/pdf' but detected contents was 'text/plain'

 FAIL  tests/UploadPdf.test.ts (7.282 s)
  POST /api/upload
    × upload data and return 201 (214 ms)

  ● POST /api/upload › upload data and return 201

    expect(received).resolves.not.toBeNull()

    Received promise rejected instead of resolved
    Rejected to value: [AxiosError: Request failed with status code 500]

      32 |
      33 |     return provider.executeTest(async (mockServer) => {
    > 34 |       await expect(
         |             ^
      35 |         uploadService.uploadPdf({
      36 |           baseUrl: mockServer.url,
      37 |           file: new Blob([PDF_FILE_DATA], {

      at expect (node_modules/expect/build/index.js:105:15)
      at tests/UploadPdf.test.ts:34:13
      at tests/UploadPdf.test.ts:27:71
      at Object.<anonymous>.__awaiter (tests/UploadPdf.test.ts:23:12)
      at tests/UploadPdf.test.ts:33:54
      at PactV3.<anonymous> (node_modules/@pact-foundation/src/v3/pact.ts:201:19)
      at step (node_modules/@pact-foundation/pact/src/v3/pact.js:33:23)
      at Object.next (node_modules/@pact-foundation/pact/src/v3/pact.js:14:53)
      at node_modules/@pact-foundation/pact/src/v3/pact.js:8:71
      at Object.<anonymous>.__awaiter (node_modules/@pact-foundation/pact/src/v3/pact.js:4:12)
      at PactV3.Object.<anonymous>.PactV3.executeTest (node_modules/@pact-foundation/pact/src/v3/pact.js:153:16)
      at Object.<anonymous> (tests/UploadPdf.test.ts:33:21)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        7.671 s
Ran all test suites.
mefellows commented 1 year ago

Thanks for the repro.

I also get the x-tex issue on my MacOSX also. It depends on the mime database available on the OS.

             1.0    $.file: MIME part 'file': Expected binary contents to have content type 'application/pdf' but detected contents was 'text/x-tex'

@uglyog FYI.

I think in this case, you're best falling back to not using that method, and using something like this instead:

  it("upload data and return 201", () => {
    provider
      .uponReceiving("a request to upload pdf")
      .withRequest({
       method: "POST",
       path: "/api/upload",
       headers: {
          "Content-Type": MatchersV3.regex("multipart\/form-data; boundary=.*", "multipart/form-data; boundary=axios-1.3.4-boundary-Py9nQHSit6xdTOTPzJ2U6vH54"),
          "Content-Length": `981`, // Ideally this is also set, you could probably get it from the FormData object
       },
       body: "--boundary......" // see pseudo-code here, albeit it's probably not strictly that valuable: https://github.com/pact-foundation/pact-net/issues/410#issuecomment-1457853792
      })
      .willRespondWith({
        status: 204
      });
corwestermaniddink commented 8 months ago

Latest pact versions.

Same with image/png file: BodyMismatch { path: "$.file['content-type']", expected: Some(b"image/png"), actual: Some(b"application/octet-stream"), mismatch: "MIME part 'file': header 'content-type': Expected 'application/octet-stream' to be equal to 'image/png'" }]

interaction('A request for POST an App', ({ provider, execute }) => { const PNG_FILE_PATH = path.join(__dirname, "archie.png"); console.log(__dirname ) beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientModule], providers: [AppsHttpService], }); provider .given('Apps API available') .uponReceiving('a request to create an app through AppsHttpService') .withRequestMultipartFileUpload( { method: "POST", path:/apps, body: {"title":"test.nl","thumbnail_url":"archie.png","image_url":"archie.png","url":{"ios":"test.nl","android":"test.nl","web":"test.nl"}} }, "application/octet-stream", PNG_FILE_PATH, "file" ) .willRespondWith({ status: 200, }) } );

When I change the "application/octet-stream" to "image/png", I got the error other way around: BodyTypeMismatch { expected: "image/png", actual: "application/octet-stream", mismatch: "MIME part 'file': Expected a body of 'image/png' but the actual content type was 'application/octet-stream'"

@mefellows Can this please be fixed? :)

mefellows commented 8 months ago

Thanks for contributing to the discussion, there is no need to ask for it to be fixed though. Clearly there is a bug that needs fixing.

You're welcome to help fix it, or use the suggested workaround. We'll get to a fix when we can.

corwestermaniddink commented 8 months ago

@mefellows I did some extra testing, but this is a pact-js-core issue in my opinion. If I change this test with a .png image file, the same error occures.