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

headers with matchers are not working the same way as in v9 #1071

Open jhayes-dev opened 1 year ago

jhayes-dev commented 1 year ago

Software versions

Issue Checklist

Please confirm the following:

Expected behaviour

I'm upgrading to pact@11 form pact@9. When defining the consumer tests the mock server return the correct body.

{
  expected_response: {
    data: 'some string',
    event_id: '37227fda-94d3-4422-908d-fe2303914e98',
    posted_date: '2021-03-17T07:52:34.666Z'
  },
}

Actual behaviour

But it is returning the 'raw' willRespondWith.body value

{
  data: {
    data: { value: 'some string', 'pact:matcher:type': 'type' },
    event_id: {
      value: '37227fda-94d3-4422-908d-fe2303914e98',
      'pact:matcher:type': 'type'
    },
    posted_date: {
      value: '2021-03-17T07:52:34.666Z',
      regex: '^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$',
      'pact:matcher:type': 'regex'
    }
  }
}

Steps to reproduce

Here is a script to reproduce the problem. See the comments in withRequest.headers and willRespondWith.headers for more details.

test-pact.ts:

import path from 'node:path';
import { Pact, Matchers } from '@pact-foundation/pact';
import axios, { AxiosResponse } from 'axios';

interface RequestPayload {
    data: string;
}

interface ResponsePayload extends RequestPayload {
    event_id: string;
    posted_date: string;
}

function toTemplate<T extends object>(obj: T): { [K in keyof T]: T[K] } {
    return obj;
}

async function main() {

    const provider: Pact = new Pact({
        consumer: 'consumer',
        provider: 'provider',
        dir: path.resolve('pacts'),
        spec: 2,
        logLevel: 'debug'
    });

    await provider.setup();

    const request: RequestPayload = {
        data: 'some string'
    };

    const expected_response: ResponsePayload = {
        ...request,
        event_id: '37227fda-94d3-4422-908d-fe2303914e98',
        posted_date: '2021-03-17T07:52:34.666Z'
    };

    await provider.addInteraction({
        state: undefined,
        uponReceiving: 'a post to the event bus',
        willRespondWith: {
            body: {
                data: Matchers.string(expected_response.data),
                event_id: Matchers.string(expected_response.event_id),
                posted_date: Matchers.iso8601DateTimeWithMillis(expected_response.posted_date),
            },
            headers: {
                // this worked in pact v9 but in v11 the returned body has all the 'value', 'regex' and 'pact:matcher:type' fields.
                // 'Content-Type': Matchers.term({
                //     generate: 'application/json;charset=utf-8',
                //     matcher: 'application/json.*'
                // }),
                // this also returns the body without the matchers replaced like above.
                'Content-Type': Matchers.term({
                    generate: 'application/json',
                    matcher: 'application/json.*'
                }),
                // this returns the correct body (with the matchers converted to example values)
                // 'Content-Type': 'application/json',
            },
            status: 201 
        },
        withRequest: {
            body: toTemplate(request),
            headers: {
                // this worked in pact v9 but in v11 return server error (500)
                // 'Content-Type': Matchers.term({
                //     generate: 'application/json;charset=utf-8',
                //     matcher: 'application/json.*'
                // }),
                // this works in pact v11 (I think it is just checking against the generate value not the regex)
                'Content-Type': Matchers.term({
                    generate: 'application/json',
                    matcher: 'application/json.*'
                }),
                // 'Content-Type': 'application/json',
            },
            method: 'POST',
            path: '/endpoint'
        }
    });

    const client = axios.create({ baseURL: provider.mockService.baseUrl });
    const { data } = await client.post<RequestPayload, AxiosResponse<ResponsePayload>>('/endpoint', request);
    console.dir({ request, expected_response, data }, { depth: null });

    await provider.verify();
    await provider.finalize();
}

main().catch(err => {
    console.log('error: ', err)
    process.exitCode = 1;
})

Relevant log files

% npx ts-node test-pack.ts
2023-03-08T21:57:23.371394Z  WARN ThreadId(01) pact_models::content_types: Failed to parse '{"value":"application/json","regex":"application/json.*","pact:matcher:type":"regex"}' as a content type: mime parse error: an invalid token was encountered, 7B at position 0
2023-03-08T21:57:23.371436Z  WARN ThreadId(01) pact_models::content_types: Failed to parse '{"value":"application/json","regex":"application/json.*","pact:matcher:type":"regex"}' as a content type: mime parse error: an invalid token was encountered, 7B at position 0
2023-03-08T21:57:23.371639Z DEBUG ThreadId(01) pact_ffi::mock_server::handles: detected pact:matcher:type, will configure a matcher
2023-03-08T21:57:23.371730Z  WARN ThreadId(01) pact_models::content_types: Failed to parse '{"value":"application/json","regex":"application/json.*","pact:matcher:type":"regex"}' as a content type: mime parse error: an invalid token was encountered, 7B at position 0
2023-03-08T21:57:23.371735Z  WARN ThreadId(01) pact_models::content_types: Failed to parse '{"value":"application/json","regex":"application/json.*","pact:matcher:type":"regex"}' as a content type: mime parse error: an invalid token was encountered, 7B at position 0
2023-03-08T21:57:23.371783Z DEBUG ThreadId(01) pact_ffi::mock_server::handles: detected pact:matcher:type, will configure a matcher
2023-03-08T21:57:23.371974Z DEBUG ThreadId(01) pact_plugin_driver::catalogue_manager: Updated catalogue entries:
core/transport/http
core/transport/https
2023-03-08T21:57:23.371996Z 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-03-08T21:57:23.372014Z 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
core/matcher/v4-semver
2023-03-08T21:57:23.372170Z DEBUG ThreadId(01) pact_mock_server::mock_server: Started mock server on 127.0.0.1:61520
2023-03-08T21:57:23.377848Z DEBUG tokio-runtime-worker hyper::proto::h1::io: parsed 6 headers
2023-03-08T21:57:23.377864Z DEBUG tokio-runtime-worker hyper::proto::h1::conn: incoming body is content-length (22 bytes)
2023-03-08T21:57:23.377872Z DEBUG tokio-runtime-worker hyper::proto::h1::conn: incoming body completed
2023-03-08T21:57:23.377876Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server: Creating pact request from hyper request
2023-03-08T21:57:23.377878Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server: Extracting query from uri /endpoint
2023-03-08T21:57:23.377905Z  INFO tokio-runtime-worker pact_mock_server::hyper_server: Received request HTTP Request ( method: POST, path: /endpoint, query: None, headers: Some({"content-type": ["application/json"], "content-length": ["22"], "host": ["127.0.0.1:61520"], "user-agent": ["axios/0.27.2"], "connection": ["close"], "accept": ["application/json", "text/plain", "*/*"]}), body: Present(22 bytes, application/json) )
2023-03-08T21:57:23.377916Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server:      body: '{"data":"some string"}'
2023-03-08T21:57:23.377938Z  INFO tokio-runtime-worker pact_matching: comparing to expected HTTP Request ( method: POST, path: /endpoint, query: None, headers: Some({"Content-Type": ["application/json"]}), body: Present(22 bytes) )
2023-03-08T21:57:23.377945Z DEBUG tokio-runtime-worker pact_matching:      body: '7B2264617461223A22736F6D6520737472696E67227D (22 bytes)'
2023-03-08T21:57:23.377946Z DEBUG tokio-runtime-worker pact_matching:      matching_rules: MatchingRules { rules: {PATH: MatchingRuleCategory { name: PATH, rules: {} }, HEADER: MatchingRuleCategory { name: HEADER, rules: {DocPath { path_tokens: [Root, Field("Content-Type"), Index(0)], expr: "$['Content-Type'][0]" }: RuleList { rules: [Regex("application/json.*")], rule_logic: And, cascaded: false }} }} }
2023-03-08T21:57:23.377954Z DEBUG tokio-runtime-worker pact_matching:      generators: Generators { categories: {} }
2023-03-08T21:57:23.377967Z DEBUG tokio-runtime-worker pact_matching::matchers: String -> String: comparing '/endpoint' to '/endpoint' ==> true cascaded=false matcher=Equality
2023-03-08T21:57:23.377977Z DEBUG tokio-runtime-worker pact_matching: expected content type = 'application/json', actual content type = 'application/json'
2023-03-08T21:57:23.377990Z DEBUG tokio-runtime-worker pact_matching: content type header matcher = 'RuleList { rules: [], rule_logic: And, cascaded: false }'
2023-03-08T21:57:23.377994Z DEBUG tokio-runtime-worker pact_plugin_driver::catalogue_manager: Looking for a content matcher for application/json
2023-03-08T21:57:23.378144Z DEBUG tokio-runtime-worker pact_matching: No content matcher defined for content type 'application/json', using core matcher implementation
2023-03-08T21:57:23.378148Z DEBUG tokio-runtime-worker pact_matching: Using body matcher for content type 'application/json'
2023-03-08T21:57:23.378157Z DEBUG tokio-runtime-worker pact_matching::json: compare: Comparing path $
2023-03-08T21:57:23.378160Z DEBUG tokio-runtime-worker pact_matching::json: compare_maps: Comparing maps at $: {"data": String("some string")} -> {"data": String("some string")}
2023-03-08T21:57:23.378177Z DEBUG tokio-runtime-worker pact_matching::json: compare: Comparing path $.data
2023-03-08T21:57:23.378180Z DEBUG tokio-runtime-worker pact_matching::json: JSON -> JSON: Comparing '"some string"' to '"some string"' using Equality -> Ok(())
2023-03-08T21:57:23.378183Z DEBUG tokio-runtime-worker pact_matching::json: compare_values: Comparing 'String("some string")' to 'String("some string")' at path '$.data' -> Ok(())
2023-03-08T21:57:23.378220Z DEBUG tokio-runtime-worker pact_matching::matchers: String -> String: comparing 'application/json' to 'application/json' ==> true cascaded=false matcher=Regex("application/json.*")
2023-03-08T21:57:23.378226Z DEBUG tokio-runtime-worker pact_matching: --> Mismatches: []
2023-03-08T21:57:23.378249Z DEBUG tokio-runtime-worker pact_mock_server::hyper_server: Test context = {"mockServer": Object {"href": String("http://127.0.0.1:61520"), "port": Number(61520)}}
2023-03-08T21:57:23.378256Z  INFO tokio-runtime-worker pact_mock_server::hyper_server: Request matched, sending response HTTP Response ( status: 201, headers: Some({"Content-Type": ["application/json"]}), body: Present(324 bytes) )
2023-03-08T21:57:23.378290Z DEBUG tokio-runtime-worker hyper::proto::h1::io: flushed 682 bytes
{
  request: { data: 'some string' },
  expected_response: {
    data: 'some string',
    event_id: '37227fda-94d3-4422-908d-fe2303914e98',
    posted_date: '2021-03-17T07:52:34.666Z'
  },
  data: {
    data: { value: 'some string', 'pact:matcher:type': 'type' },
    event_id: {
      value: '37227fda-94d3-4422-908d-fe2303914e98',
      'pact:matcher:type': 'type'
    },
    posted_date: {
      value: '2021-03-17T07:52:34.666Z',
      regex: '^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$',
      'pact:matcher:type': 'regex'
    }
  }
}
2023-03-08T21:57:23.380837Z DEBUG ThreadId(01) pact_ffi::mock_server::handles: pact_ffi::mock_server::handles::pactffi_pact_handle_write_file FFI function invoked
2023-03-08T21:57:23.380879Z DEBUG ThreadId(01) pact_models::pact: Merging pact with file "/Users/johnhayes/src/test-pact-failure/pacts/consumer-provider.json"
2023-03-08T21:57:23.381929Z  WARN ThreadId(01) pact_models::pact: Note: Existing pact is an older specification version (V2), and will be upgraded
2023-03-08T21:57:23.383255Z 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-03-08T21:57:23.383266Z DEBUG ThreadId(01) pact_mock_server::server_manager: Shutting down mock server with ID b6c1662a-9534-4f3e-865a-8d7d111930ca - MockServerMetrics { requests: 1 }
2023-03-08T21:57:23.383274Z DEBUG ThreadId(01) pact_mock_server::mock_server: Mock server b6c1662a-9534-4f3e-865a-8d7d111930ca shutdown - MockServerMetrics { requests: 1 }
2023-03-08T21:57:23.383295Z DEBUG tokio-runtime-worker hyper::server::shutdown: signal received, starting graceful shutdown
[13:57:23.368] DEBUG (81148): pact-core@13.13.5: Initalising native core at log level 'debug'
[13:57:23.370] DEBUG (81148): pact@11.0.0: free port discovered: 61520
[13:57:23.371] DEBUG (81148): pact@11.0.0: setting header request value for [object Object] at index 0 to "{\"value\":\"application/json\",\"regex\":\"application/json.*\",\"pact:matcher:type\":\"regex\"}"
[13:57:23.371] DEBUG (81148): pact@11.0.0: setting header response value for [object Object] at index 0 to "{\"value\":\"application/json\",\"regex\":\"application/json.*\",\"pact:matcher:type\":\"regex\"}"
[13:57:23.371] DEBUG (81148): pact@11.0.0: Setting up Pact mock server with Consumer "consumer" and Provider "provider"
        using mock service on Port: "61520"
[13:57:23.372] DEBUG (81148): pact@11.0.0: mock service started on port: 61520
[13:57:23.383] DEBUG (81148): pact@11.0.0: cleaning up old mock server on port 61520
mefellows commented 1 year ago

Thanks for this, we'll take a look. It looks like the matchers aren't being correctly sent, and therefore are being treated as the payload.

kapustam-chg commented 4 months ago

I have the same issue. Also trying to upgrade from v9 -> v11 and I got raw out of matchers object.