cypress icon indicating copy to clipboard operation
cypress copied to clipboard

Add option to cy.intercept() to match request body

Open asumaran opened this issue 4 years ago ā€¢ 24 comments

I was trying to cy.wait() some specific GraphQL requests that were made using fetch and using cy.route2() but I don't see an option to add a matcher for the request body. According to the type definition there's not body param

The closest thing is query but it doesn't consider the request body. Is this by design or there are plans to add it?

Current behavior:

All GraphQL queries go to the same endpoint "server.com/graphql" using the POST method. The query itself is sent in the request payload and it's a JSON-like string as follows:

{
  "query": "query ($limit: Int) {
    searchShippingTemplate(limit: $limit) {
      shippingTemplates {
        id
      }
    }
  }",
  "variables": {
    "limit": 25
  }
}

My plan was add a match for searchShippingTemplate like:

cy.route2({
  url: "http://localhost:3000/graphql",
  method: "POST",
  body: "*searchShippingTemplate*",
}).as("getQuery");

But there's no way to do that.

Desired behavior:

Add body option to RouteMatcherOptions in order to match a string in the request body.

It would be even better to be able to pass a function (for debugging purposes) since there's no easy way to know what exactly is .route2() making the comparison against for each RouteMatcherOptions option.

cy.route2({
  url: "http://localhost:3000/graphql",
  method: "POST",
  body: (bodyString) => {
    console.log(bodyString); // In theory we could even mutate the request
    return bodyString;
  },
}).as("getQuery");

Test code to reproduce

This is not a bug and more like a feature request.

Versions

"cypress": "^5.1.0"

asumaran avatar Sep 12 '20 02:09 asumaran

@asumaran yes, this would be nice to add, it is currently not implemented but as you say it would help with some implementations.

You can modify the request body and response body already.

// modify an outgoing request
cy.route2({
  url: "http://localhost:3000/graphql",
  method: "POST"
}, (req) => {
  console.log(req.body)
  req.body.replace('foo, 'bar') // modifies outgoing request
})

You can also use a deferred promise to .wait on a request that you've "matched" via the request handler.

// wait on a dynamically-matched request using a deferred Promise
const p = Cypress.Promise.defer()

cy.route2({
  url: "http://localhost:3000/graphql",
  method: "POST"
}, (req) => {
  if (req.body.includes('searchShippingTemplate')) {
    p.resolve() // resolve the deferred promise
  }
})
// ... do some more Cypress stuff here ...
// now, wait on that deferred Promise to resolve:
cy.wrap(p)

I hope this helps in the meantime while we add a more formal API around this. :)

flotwig avatar Sep 21 '20 20:09 flotwig

Thank you @flotwig. Iā€™m going to try your suggestions.

asumaran avatar Sep 21 '20 20:09 asumaran

I'm interested in this as well.

AnderssonChristian avatar Oct 05 '20 11:10 AnderssonChristian

+1 It would be great to add this feature for the same reasons commented here.

jandrade avatar Oct 30 '20 23:10 jandrade

It would be a great feature to match GraphQl queries.

vrknetha avatar Nov 02 '20 12:11 vrknetha

@flotwig given Promise.defer() is deprecated, is there any way to do this with aliases? Does returning a promise that is resolved/rejected based on the request work?

cy.route2({
  url: "http://localhost:3000/graphql",
  method: "POST"
}, (req) => {
  return new Promise((resolve, reject) => {
    if (req.body.includes('searchShippingTemplate')) {
      resolve() // resolve the deferred promise
    } else {
      reject()
    }
})}).as(`searchShipping`)

cy.wait(`@searchShipping`)

(edit)

To answer my own question - no :(

m4dc4p avatar Nov 11 '20 19:11 m4dc4p

šŸ““ @bahmutov wrote a great post about graphQL requests and cy.route2 and matching request body's.

  • https://glebbahmutov.com/blog/smart-graphql-stubbing/

šŸ““ @m4dc4p Additionally there apparently is a new way to alias specific requests now too.

  • https://docs.cypress.io/api/commands/route2.html#Aliasing-individual-requests
cy.route2('POST', '/graphql', (req) => {
  if (req.body.includes('mutation')) {
    req.alias = 'gqlMutation'
  }
})
// assert that a matching request has been made
cy.wait('@gqlMutation')

hartzis avatar Nov 11 '20 22:11 hartzis

šŸ““ @bahmutov wrote a great post about graphQL requests and cy.route2 and matching request body's.

* https://glebbahmutov.com/blog/smart-graphql-stubbing/

I've read it. Unfortunately, it does not cover matching specific requests ...

šŸ““ @m4dc4p Additionally there apparently is a new way to alias specific requests now too.

* https://docs.cypress.io/api/commands/route2.html#Aliasing-individual-requests

šŸ¤Æ - that looks like what I need. Thank you!

m4dc4p avatar Nov 12 '20 00:11 m4dc4p

@flotwig given Promise.defer() is deprecated, is there any way to do this with aliases? Does returning a promise that is resolved/rejected based on the request work?

@m4dc4p bah, I wish they hadn't stuck that deprecated warning on defer, defer is totally fine to use in situations like this imo.

You can create your own deferred promise:

function deferredPromise() {
	let resolve, reject
	const promise = new Cypress.Promise((_resolve, reject) => {
		resolve = _resolve
		reject = _reject
	})
	return { resolve, reject, promise }
}

Works the same as Promise.defer.

https://docs.cypress.io/api/commands/route2.html#Aliasing-individual-requests also works well, especially if you need to match on dynamic criteria.

flotwig avatar Nov 12 '20 18:11 flotwig

This link is dead: https://docs.cypress.io/api/commands/route2.html#Aliasing-individual-requests

mehrad77 avatar Apr 19 '21 11:04 mehrad77

@mehrad77 https://on.cypress.io/intercept#Aliasing-individual-requests

jennifer-shehane avatar Apr 26 '21 19:04 jennifer-shehane

+1 I have a similar situation when POST requests query params are the same, but actions are located in the payload, so it would be nice to catch only requests with action that you're waiting for.

antonlegkiy avatar Jun 11 '21 09:06 antonlegkiy

+1 here as well. This would be great to have.

aturkewi avatar Jun 21 '21 18:06 aturkewi

Another +1

ChapDDR avatar Sep 01 '21 11:09 ChapDDR

+1

locinus avatar Sep 07 '21 12:09 locinus

+1

phattanun avatar Sep 23 '21 08:09 phattanun

I went through the very well-crafted doc many times and couldn't believe this wasn't a feature yet.

+1+1+1+1+1+1+1

Raph-Capitale avatar Oct 08 '21 13:10 Raph-Capitale

This is really needed +1

ozthekoder avatar Nov 01 '21 15:11 ozthekoder

Maybe the easiest way to support this and potentially many other use cases, would be if intercept would optionally take a function as the first parameter, and expect this function to return a boolean. (true = request matched, false = request did not match).

I am thinking of something like this:

  cy.intercept((req) => {
    req.method === 'POST' && req.url.match('some-url-fragment') && req.body.match('someParam')
  })

andi-dev avatar Nov 03 '21 10:11 andi-dev

I think it would be nice to do what the request header matching does - you just need one of the headers to match. I often use it to stub GraphQL resources using the custom "X-operation-name" header

from https://github.com/bahmutov/todo-graphql-example/blob/master/cypress/integration/intercept-spec.js

    // we have special middleware in our GraphQL client
    // that puts the operation name in the request header "x-gql-operation-name"
    // we can define intercepts using this custom header
    cy.intercept({
      method: 'POST',
      url: '/',
      headers: {
        'x-gql-operation-name': 'allTodos',
      },
    }).as('allTodos')

    cy.intercept({
      method: 'POST',
      url: '/',
      headers: {
        'x-gql-operation-name': 'AddTodo',
      },
    }).as('addTodo')

    cy.intercept({
      method: 'POST',
      url: '/',
      headers: {
        'x-gql-operation-name': 'updateTodo',
      },
    }).as('updateTodo')

We cannot modify the application code like this, so I would love to be able to match by a part of the request body. In GraphQL requests I have operationName field for example and I would love to use it. Maybe something like bodyPart which could be a nested object; if the request body includes it, then the matcher fires?

 cy.intercept({
    method: 'POST',
    url: '/',
    bodyPart: {
        'operationName': 'allTodos',
    },
}).as('allTodos')

bahmutov avatar Nov 03 '21 12:11 bahmutov

Maybe the easiest way to support this and potentially many other use cases, would be if intercept would optionally take a function as the first parameter, and expect this function to return a boolean. (true = request matched, false = request did not match).

I am thinking of something like this:

  cy.intercept((req) => {
    req.method === 'POST' && req.url.match('some-url-fragment') && req.body.match('someParam')
  })

This would be the simplest yet most versatile solution. We have $batch request for odata where the endpoint stays the same while the request path is specified in the request body (or request body part if you will). If the function matcher were to be supported, we could write our own requestMatcher with ease.

johnmiroki avatar Jan 04 '22 05:01 johnmiroki

+1

tripflex avatar Aug 04 '22 15:08 tripflex

+1

mcollins2-quantium avatar Aug 17 '22 08:08 mcollins2-quantium

This is really needed +1

Grutula avatar Sep 21 '22 06:09 Grutula

+1

amyzhao-seismic avatar Sep 22 '22 07:09 amyzhao-seismic

This is really needed +1

Baudry-G avatar Sep 22 '22 08:09 Baudry-G

+1

ohirnyak avatar Sep 30 '22 09:09 ohirnyak

+1

lokriet avatar Oct 18 '22 21:10 lokriet

+1

ilibilibom avatar Nov 22 '22 11:11 ilibilibom

+1

shinsid avatar Mar 16 '23 16:03 shinsid