pact-foundation / pact-reference

Reference implementations for the pact specifications
https://pact.io
MIT License
91 stars 46 forks source link

Support array matcher in query strings [FFI] #205

Closed mefellows closed 2 years ago

mefellows commented 2 years ago

I'm trying to setup an array matcher on a repeated query string value, but I always end up with the following error:

Expected query parameter 'catId' with 1 value(s) but received 2 value(s)

It's currently part of a semi-regression suite we run in Pact JS. I've ported the beta branch legacy interface to use the rust core, so that most users should be able to just upgrade without having to modify all of their tests.

The setup looks a bit like this:

const catsListRequest = {
  uponReceiving: 'a request for cats with given catId',
  withRequest: {
    method: 'GET',
    path: '/cats',
    query: {
      'catId[]': Matchers.eachLike('1'),
    },
    headers: {
      Accept: 'application/json',
    },
  },
};

This would produce a query matcher in JSON format as follows:

{
  "catId": {
    "value": ["1"],
    "pact:matcher:type": "type",
    "min": 1
  }
}

The request being sent is GET /cats?catId[]=2&catId[]=3.

What I've tried

  1. Different array parameter syntax - both the catId=1&catId=2... syntax as well as the rails array syntax catId[]=1&catId[]=2... yield the same result
  2. Using the standard type matcher, rather than an array matcher (it looks like query params are always treated as arrays, so I suspected maybe that interfered with the matching rule logic)
  3. All combinations of 1 and 2

Debug logs:

[2022-07-07T07:10:31Z DEBUG pact_ffi::mock_server::bodies] Path = $.catId[0]
[2022-07-07T07:10:31Z DEBUG pact_ffi::mock_server::bodies] detected pact:matcher:type, will configure a matcher
[2022-07-07T07:10:31Z DEBUG pact_ffi::mock_server::bodies] Path = $.catId[0]
[2022-07-07T07:10:31Z DEBUG pact_ffi::mock_server::bodies] Path = $
[2022-07-07T07:10:31Z DEBUG pact_ffi::mock_server::bodies] Path = $[0]
[2022-07-07T07:10:31Z DEBUG pact_ffi::mock_server::bodies] Configuring a normal object
[2022-07-07 07:10:31.961 +0000] INFO (85377 on SB-AS-G7GM9F7): pact@10.0.0-beta.60: Setting up Pact with Consumer "Jest-Consumer-Example" and Provider "Jest-Provider-Example"
        using mock service on Port: "50801"

[2022-07-07 07:10:31.962 +0000] DEBUG (85377 on SB-AS-G7GM9F7): pact@10.0.0-beta.60: mock service started on port: 50801

 RUNS  __tests__/index.spec.js
[2022-07-07T07:10:31Z DEBUG hyper::proto::h1::io] parsed 4 headers
[2022-07-07T07:10:31Z DEBUG hyper::proto::h1::conn] incoming body is empty
[2022-07-07T07:10:31Z DEBUG pact_mock_server::hyper_server] Creating pact request from hyper request
[2022-07-07T07:10:31Z DEBUG pact_mock_server::hyper_server] Extracting query from uri /cats?catId=2&catId=3
[2022-07-07T07:10:31Z INFO  pact_mock_server::hyper_server] Received request HTTP Request ( method: GET, path: /cats, query: Some({"catId": ["2", "3"]}), headers: Some({"connection": ["close"], "accept": ["application/json"], "host": ["127.0.0.1:50801"], "user-agent": ["axios/0.21.4"]}), body: Empty )
[2022-07-07T07:10:31Z INFO  pact_matching] comparing to expected HTTP Request ( method: GET, path: /cats, query: Some({"catId": ["[\"1\"]"]}), headers: Some({"Accept": ["application/json"]}), body: Missing )
[2022-07-07T07:10:31Z DEBUG pact_matching]      body: ''
[2022-07-07T07:10:31Z DEBUG pact_matching]      matching_rules: MatchingRules { rules: {HEADER: MatchingRuleCategory { name: HEADER, rules: {} }, QUERY: MatchingRuleCategory { name: QUERY, rules: {DocPath { path_tokens: [Root, Field("catId"), Index(0)], expr: "$.catId[0]" }: RuleList { rules: [MinType(1)], rule_logic: And, cascaded: false }} }, PATH: MatchingRuleCategory { name: PATH, rules: {} }} }
[2022-07-07T07:10:31Z DEBUG pact_matching]      generators: Generators { categories: {} }
[2022-07-07T07:10:31Z DEBUG pact_matching::matchers] String -> String: comparing '/cats' to '/cats' using Equality (false)
[2022-07-07T07:10:31Z DEBUG pact_matching] expected content type = '*/*', actual content type = '*/*'
[2022-07-07T07:10:31Z DEBUG pact_matching] content type header matcher = 'RuleList { rules: [], rule_logic: And, cascaded: false }'
[2022-07-07T07:10:31Z DEBUG pact_matching::matchers] String -> String: comparing '["1"]' to '2' using MinType(1) (false)
[2022-07-07T07:10:31Z DEBUG pact_matching] --> Mismatches: [QueryMismatch { parameter: "catId", expected: "[\"[\\\"1\\\"]\"]", actual: "[\"2\", \"3\"]", mismatch: "Expected query parameter 'catId' with 1 value(s) but received 2 value(s)" }]
[2022-07-07T07:10:31Z DEBUG pact_mock_server::hyper_server] Request did not match: Request did not match - HTTP Request ( method: GET, path: /cats, query: Some({"catId": ["[\"1\"]"]}), headers: S[2022-07-07 07:10:31.971 +0000] DEBUG (85377 on SB-AS-G7GM9F7): pact@10.0.0-beta.60: cleaning up old mock server on port 50801alue(s)

 RUNS  __tests__/index.spec.js
[2022-07-07T07:10:31Z DEBUG pact_mock_server::server_manager] Shutting down mock server with port 50801
[2022-07-07T07:10:31Z DEBUG pact_mock_server::server_manager] Shutting down mock server with port 50801 - MockServerMetrics { requests: 1 }
 FAIL  __tests__/index.spec.jsct_mock_server::mock_server] Mock server 4fc79205-8b88-4bfc-b256-3782b48a1dbb shutdown - MockServerMetrics { requests: 1 }
  Pact between Jest-Consumer-Example and Jest-Provider-Example
    with 30000 ms timeout for Pact
      Dogs API
        ✓ returns a successful body (58 ms)
      Cats API
        ✕ returns a successful body (14 ms)

  ● Pact between Jest-Consumer-Example and Jest-Provider-Example › with 30000 ms timeout for Pact › Cats API › returns a successful body

    Request failed with status code 500

      at createError (node_modules/axios/lib/core/createError.js:16:15)

  ● Pact between Jest-Consumer-Example and Jest-Provider-Example › with 30000 ms timeout for Pact › Cats API › returns a successful body

    Test failed for the following reasons:

      Mock server failed with the following mismatches:

        0) The following request was incorrect:

                GET /cats

                 1.0 Expected query parameter 'catId' with 1 value(s) but received 2 value(s)
rholshausen commented 2 years ago

Looks like there are multiple problems here:

  1. The JSON format being sent is not correct (it has the query parameter name without the brackets). It also doesn't make sense to send the parameter name in the value payload, as it is already sent separately.
  2. The value is being used as is, not being parsed as an array, hence String -> String: comparing '["1"]' to '2'
  3. It is not applying the matching rule as a collection, but to the individual values.
mefellows commented 2 years ago

Thanks Ron.

For (1), are you saying that Pact JS (and other clients) should send the query in a different shape or is this some internal thing in the rust core?

rholshausen commented 2 years ago

Both. Defect in the Rust core and we should change the payload being sent.

mefellows commented 2 years ago

Thanks. Because my brain is still not working, what would be the correct JSON payload shape to send for the query? I think I tried a few variations.

rholshausen commented 2 years ago

I've added a new function that takes

{
    "value": ["1"],
    "pact:matcher:type": "type",
    "min": 1
}