pact-mock_service icon indicating copy to clipboard operation
pact-mock_service copied to clipboard

Same endpoint, different query strings

Open tarciosaraiva opened this issue 8 years ago • 29 comments

Hi there recently a bug was opened on Pact JS relating to having two interactions with the same endpoint but with one of the interactions differing only by a query string on the request.

Here's the issue: https://github.com/pact-foundation/pact-js/issues/12

I've tested locally and got the same results. Here's the test I created as part of the Pact JS suite: https://github.com/pact-foundation/pact-js/blob/test-query-string/test/dsl/integration.spec.js#L137

Edit: I suspect the issue is on Pact Mock Server, hence the bug. Below is the output of my log file. The Pact file gets created with only one interaction:

I, [2016-09-24T16:03:37.783385 #9663]  INFO -- : Registered expected interaction GET /projects
D, [2016-09-24T16:03:37.783615 #9663] DEBUG -- : {
  "description": "a request for projects",
  "provider_state": "i have a list of projects",
  "request": {
    "method": "GET",
    "path": "/projects",
    "headers": {
      "Accept": "application/json"
    }
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "body": [
      {
        "id": 1,
        "name": "Project 1",
        "due": "2016-02-11T09:46:56.023Z",
        "tasks": [
          {
            "id": 1,
            "name": "Do the laundry",
            "done": true
          },
          {
            "id": 2,
            "name": "Do the dishes",
            "done": false
          },
          {
            "id": 3,
            "name": "Do the backyard",
            "done": false
          },
          {
            "id": 4,
            "name": "Do nothing",
            "done": false
          }
        ]
      }
    ]
  }
}
I, [2016-09-24T16:03:37.785313 #9663]  INFO -- : Registered expected interaction GET /projects?from=today
D, [2016-09-24T16:03:37.785473 #9663] DEBUG -- : {
  "description": "a request for projects starting today",
  "provider_state": "i have a list of projects starting today",
  "request": {
    "method": "GET",
    "path": "/projects",
    "query": {
      "from": [
        "today"
      ]
    },
    "headers": {
      "Accept": "application/json"
    }
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "body": [
      {
        "id": 1,
        "name": "Project 1",
        "due": "2016-02-11T09:46:56.023Z",
        "tasks": [
          {
            "id": 1,
            "name": "Do the laundry",
            "done": true
          },
          {
            "id": 2,
            "name": "Do the dishes",
            "done": false
          },
          {
            "id": 3,
            "name": "Do the backyard",
            "done": false
          },
          {
            "id": 4,
            "name": "Do nothing",
            "done": false
          }
        ]
      }
    ]
  }
}
I, [2016-09-24T16:03:37.794977 #9663]  INFO -- : Received request GET /projects
D, [2016-09-24T16:03:37.795086 #9663] DEBUG -- : {
  "path": "/projects",
  "query": "",
  "method": "get",
  "headers": {
    "Host": "localhost:1234",
    "Accept-Encoding": "gzip, deflate",
    "User-Agent": "node-superagent/2.3.0",
    "Accept": "application/json",
    "Connection": "close",
    "Version": "HTTP/1.1"
  }
}
I, [2016-09-24T16:03:37.795500 #9663]  INFO -- : Found matching response for GET /projects
D, [2016-09-24T16:03:37.795785 #9663] DEBUG -- : {
  "status": 200,
  "headers": {
    "Content-Type": "application/json"
  },
  "body": [
    {
      "id": 1,
      "name": "Project 1",
      "due": "2016-02-11T09:46:56.023Z",
      "tasks": [
        {
          "id": 1,
          "name": "Do the laundry",
          "done": true
        },
        {
          "id": 2,
          "name": "Do the dishes",
          "done": false
        },
        {
          "id": 3,
          "name": "Do the backyard",
          "done": false
        },
        {
          "id": 4,
          "name": "Do nothing",
          "done": false
        }
      ]
    }
  ]
}
I, [2016-09-24T16:03:37.801958 #9663]  INFO -- : Received request GET /projects?from=today
D, [2016-09-24T16:03:37.802037 #9663] DEBUG -- : {
  "path": "/projects",
  "query": "from=today",
  "method": "get",
  "headers": {
    "Host": "localhost:1234",
    "Accept-Encoding": "gzip, deflate",
    "User-Agent": "node-superagent/2.3.0",
    "Accept": "application/json",
    "Connection": "close",
    "Version": "HTTP/1.1"
  }
}
E, [2016-09-24T16:03:37.802210 #9663] ERROR -- : Multiple interactions found for GET /projects?from=today:
D, [2016-09-24T16:03:37.802344 #9663] DEBUG -- : {
  "description": "a request for projects",
  "provider_state": "i have a list of projects",
  "request": {
    "method": "GET",
    "path": "/projects",
    "headers": {
      "Accept": "application/json"
    }
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "body": [
      {
        "id": 1,
        "name": "Project 1",
        "due": "2016-02-11T09:46:56.023Z",
        "tasks": [
          {
            "id": 1,
            "name": "Do the laundry",
            "done": true
          },
          {
            "id": 2,
            "name": "Do the dishes",
            "done": false
          },
          {
            "id": 3,
            "name": "Do the backyard",
            "done": false
          },
          {
            "id": 4,
            "name": "Do nothing",
            "done": false
          }
        ]
      }
    ]
  }
}
D, [2016-09-24T16:03:37.802467 #9663] DEBUG -- : {
  "description": "a request for projects starting today",
  "provider_state": "i have a list of projects starting today",
  "request": {
    "method": "GET",
    "path": "/projects",
    "query": {
      "from": [
        "today"
      ]
    },
    "headers": {
      "Accept": "application/json"
    }
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "body": [
      {
        "id": 1,
        "name": "Project 1",
        "due": "2016-02-11T09:46:56.023Z",
        "tasks": [
          {
            "id": 1,
            "name": "Do the laundry",
            "done": true
          },
          {
            "id": 2,
            "name": "Do the dishes",
            "done": false
          },
          {
            "id": 3,
            "name": "Do the backyard",
            "done": false
          },
          {
            "id": 4,
            "name": "Do nothing",
            "done": false
          }
        ]
      }
    ]
  }
}
I, [2016-09-24T16:03:37.806536 #9663]  INFO -- : Writing pact with details {:consumer=>{:name=>"Consumer 1"}, :provider=>{:name=>"Provider 1"}}
I, [2016-09-24T16:03:37.806612 #9663]  INFO -- : Writing pact for Provider 1 to /home/tarcio/workspace/pact/pact-js/pacts/consumer_1-provider_1.json
I, [2016-09-24T16:03:37.808968 #9663]  INFO -- : Cleared interactions before example ""

tarciosaraiva avatar Sep 24 '16 06:09 tarciosaraiva

OK I think I found the cause. In pact-support there are two matcher methods that check the difference excluding the query string:

  • https://github.com/bethesque/pact-support/blob/master/lib/pact/consumer_contract/request.rb#L33
  • https://github.com/bethesque/pact-support/blob/master/lib/pact/consumer_contract/request.rb#L39

Both of them are invoked by pact-mock_service in here and here.

There are 3 solutions to this in my mind right now:

  • new method in pact-support that does stricter checking (i.e. checks for qs) which would leave the decision making on which method to invoke to pact-mock_service
  • modify methods in pact-support to check difference using query string if query string is passed in (no code change in pact-mock_service)
  • just change the check to include query string and be done with it (least preferred option)

Please advise, happy to submit PR.

tarciosaraiva avatar Sep 25 '16 12:09 tarciosaraiva

Option 2 does feel like the better option.

uglyog avatar Oct 04 '16 00:10 uglyog

What does it do if you send GET /projects to it? Same thing? Yes, I think I agree that option 2 is correct.

It's this line that does it: https://github.com/bethesque/pact-support/blob/master/lib/pact/consumer_contract/request.rb#L48


def query_diff actual_query
  if specified?(:query)
    # If the query was expected, check the query. This should probably be if the query was expected OR a query was given.
    query_diff = query.difference(actual_query)
    query_diff.any? ? {query: query_diff} : {}
  else
    {}
  end
end

bethesque avatar Oct 04 '16 04:10 bethesque

The only problem is that this will be a breaking change - it could well impact existing projects. Major version release?

bethesque avatar Oct 04 '16 04:10 bethesque

@bethesque regarding your question above

What does it do if you send GET /projects to it? Same thing?

Not quite. The log above shows that a request for the endpoint without the query string finds a match right away.

With the query string it gets confused.

tarciosaraiva avatar Oct 04 '16 04:10 tarciosaraiva

I'm having a similar issue with matching on Cookies. I have a request to the same endpoint but expect a different response based on whether you have a cookie provided (your session). However, when I do provide the cookie it complains about multiple matches. The interaction which doesn't specify the cookie is matching as well as the one that matches the cookie header exactly.

Any thoughts on this?

Blackbaud-JonathanBell avatar Oct 14 '16 20:10 Blackbaud-JonathanBell

My thoughts are that Tarcio was going to submit a PR ;-)

On 15 Oct 2016 7:21 AM, "Blackbaud-JonathanBell" [email protected] wrote:

I'm having a similar issue with matching on Cookies. I have a request to the same endpoint but expect a different response based on whether you have a cookie provided (your session). However, when I do provide the cookie it complains about multiple matches. The interaction which doesn't specify the cookie is matching as well as the one that matches the cookie header exactly.

Any thoughts on this?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/bethesque/pact-mock_service/issues/54#issuecomment-253909691, or mute the thread https://github.com/notifications/unsubscribe-auth/AAbPFAevkDYsL7YzKeXzbHhJw4XC1ZEGks5qz-RlgaJpZM4KFkdb .

bethesque avatar Oct 14 '16 20:10 bethesque

Just bumped into the same problem: "Multiple interactions found for POST". In my case only the request body is different but I think the problem is the same. Did anyone fix this or make a PR?

Right now I will try to create separate Pact files (changing consumer name) for each scenario, but this seems like a hack/workaround and I'd rather see this issue resolved.

@tarciosaraiva can make a PR to try and fix this issue?

ptornhult avatar Sep 13 '17 13:09 ptornhult

I'll have another look at it. Can you bump this if I haven't gotten back to you in a couple of days?

bethesque avatar Sep 15 '17 02:09 bethesque

Awesome, thanks!

In case anyone else stumbles on this problem in the mean time. I've implemented a workaround in my Pact.js tests for now where I split all Pact specs (which use the same path) to use different consumer names (also logfiles and mock server port). This way multiple pact files are generated and we don't get the conflict.

ptornhult avatar Sep 15 '17 06:09 ptornhult

The mock service is designed so that you load just the interactions for a single test, run that test, then reset the interactions. Are you using it in a long lived way over multiple tests?

bethesque avatar Sep 15 '17 07:09 bethesque

Since I couldn't load more than 1 interaction for the same endpoint in the mock service, I've split each interaction to use their own mock service

ptornhult avatar Sep 15 '17 07:09 ptornhult

You can load them all into the same mock service, just not at the same time. That's the way it's designed. You're not supposed to set up every interaction at the same time for the entire test suite. You just load the interactions for one test, run that, clear the interactions, then set up the interactions for the next test, run that, clear the interactions etc. Unless you need to test both of the conflicting interactions in the same test case, it shouldn't be a problem. Here's a gist showing how the mock service is designed to be used: https://gist.github.com/bethesque/9d81f21d6f77650811f4

bethesque avatar Sep 15 '17 21:09 bethesque

My bad, I tried to merge my interactions into the same spec again and it's working :) I think I might have been confused about where/when to run provider.verify()

ptornhult avatar Sep 18 '17 07:09 ptornhult

Having a look at this again. If we don't specify a body, then no body match is performed either. What happens in the JVM impl Ron?

bethesque avatar Oct 02 '17 04:10 bethesque

In the JVM, if the expected interaction has no body, the actual body is ignored.

uglyog avatar Oct 02 '17 05:10 uglyog

I am now running into this (using the JS DSL; see pact-foundation/pact-js#12). Do I understand correctly that there is no way to specify one response for a GET without a query parameter, and another response for a GET with a query parameter?

eatdrinksleepcode avatar Dec 07 '17 03:12 eatdrinksleepcode

I am also running into the original problem in this issue. Are there any plans to fix this?

cpul avatar Dec 07 '17 15:12 cpul

@eatdrinksleepcode you can specify both of those queries, just not at the same time. The mock service is not meant to have a single session for the entire test suite. You should set up one or two interactions for a single test, then do the test, and then clear the mock. Have a read of these usage instructions.

https://github.com/pact-foundation/pact-mock_service#mock-service-usage

If we can work out the "correct" logic for making the two interactions work in the same session, I'm happy to make a fix. The problem is, the consistent rule has been, if X is not specified, then accept any value of X. We can't change this without breaking backwards compatibility.

bethesque avatar Dec 07 '17 22:12 bethesque

@bethesque I guess my problem with that approach is that for this particular API, "if X is not specified, then accept any value of X" is not correct. For the test that is supposed to NOT send the query string, if my code DOES send the query string, I want that pact verification to fail.

Has there ever been any design work put into the general idea of negative specifications?

eatdrinksleepcode avatar Jan 18 '18 06:01 eatdrinksleepcode

This is a tricky one. Negative specifications have been deliberately not supported, because each consumer should specify what they care about, and ignore what they don't. It also kind of relates to this: https://github.com/pact-foundation/pact-ruby/wiki/FAQ#why-is-there-no-support-for-specifying-optional-attributes

I can see this is awkward in a case where you're defining behaviour based on the absence of a field however.

If you're not sure if the field is there or not for each interaction, it would seem to me that you're not fully in control of the provider state set up, and that may mean that pact is not the best option tool for the situation.

bethesque avatar Jan 18 '18 22:01 bethesque

Negative specifications have been deliberately not supported, because each consumer should specify what they care about, and ignore what they don't.

That's the second half of Postel's law: be liberal in what you accept. I am talking about the first half: be conservative in what you send. I want to exactly specify what the request (what I am sending) should be, and fail the test if the actual request does not exactly match the specification.

eatdrinksleepcode avatar Jan 19 '18 05:01 eatdrinksleepcode

If you specify a query string, then the string must match exactly, with no extra parameters. If you use a hash, then the order is ignored, but again, you can't have any extra parameters. If you want to specify that there are no query parameters, then try using an empty string or and empty hash. I'm not sure if that will work, but try it, and if it doesn't, we can modify the code.

bethesque avatar Jan 21 '18 22:01 bethesque

@eatdrinksleepcode I think that sort of test case is better served by traditional validated mocks that are local to your code base.

The reason is that these sorts of tests are very likely to create Pacts that are not enforceable by the Providing service, as they start to contain consumer-specific rules.

mefellows avatar Jun 24 '18 02:06 mefellows

got the same issue with a post. issue was when to call verify. if it was in afterEach then it cleared the interactions before my second test case and failed stating "No matching interaction".

Changed the verify to run after all test cases pass

    after(async function() {
        provider.verify()
        this.timeout(10000) // it takes time to stop the mock server and gather the contracts
        await provider.finalize()

    })

now it works.

shavo007 avatar Aug 16 '18 05:08 shavo007

Rather than a timeout, does await provider.verify() not work?

mefellows avatar Aug 16 '18 06:08 mefellows

yea @mefellows ive changed the structure now and it works

    before(async function() {
        await provider.setup()
    })

    after(async function() {
        await provider.finalize()
    })

    afterEach(async function()  {
        await provider.verify()
        sandbox.restore()
    })

shavo007 avatar Aug 17 '18 01:08 shavo007

Really don't know is it right way or not but it works.

beforeEach(async () => { await provider.setup(); }); afterEach(async () => { await provider.finalize(); });

121371 avatar Jan 27 '19 10:01 121371

@121371 what's your question? You can certainly use async/await (it's easier to just return the promise).

Is your question related to this issue?

mefellows avatar Jan 29 '19 21:01 mefellows