Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,49 @@ after(() => {

```

### cy.usePactIntercept(option, alias)
Use `cy.usePactIntercept` to intercept network calls and record response and it's matching rules to a pact file.

- Accepts a valid Cypress request options argument [Cypress request options argument](https://docs.cypress.io/api/commands/request#Arguments)

**Example**
```js

before(() => {
cy.setupPact('ui-consumer', 'api-provider')
cy.usePactIntercept(
{
method: 'POST',
url: '**/admin_login',
response: {
"body": {
"token": "token_token_token",
"refresh_token": "refresh_token"
},
"headers": {
"content-type": "application/json"
},
"statusCode": 200
},
matchingRules: {
"$.body.token": {
"match": "type"
},
"$.body.refresh_token": {
"match": "type"
}
},
},
'adminLogin'
)
})

//... cypress test

after(() => {
cy.usePactWait(['adminLogin']).
})

## Example Project
Check out a simple react app example project at [/example/todo-example](/example/todo-example/)

26 changes: 23 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { MatchingRulesType } from './types';
import { AUTOGEN_HEADER_BLOCKLIST } from './constants'
import { AliasType, AnyObject, PactConfigType, XHRRequestAndResponse, RequestOptionType } from 'types'
import { AliasType, AnyObject, PactConfigType, XHRRequestAndResponse, RequestOptionType, InterceptOptionType } from 'types'
import { formatAlias, writePact } from './utils'
import { env } from 'process'

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
usePactIntercept: (option: InterceptOptionType, alias: string) => Chainable
usePactWait: (alias: AliasType) => Chainable
usePactRequest: (option: AnyObject, alias: string) => Chainable
usePactGet: (alias: string, pactConfig: PactConfigType) => Chainable
Expand Down Expand Up @@ -36,30 +38,47 @@ const setupPactHeaderBlocklist = (headers: string[]) => {
headersBlocklist = [...headers, ...headersBlocklist]
}

const interceptDataMap: {[alias: string]: InterceptOptionType} = {}
const usePactIntercept = (option: InterceptOptionType, alias: string) => {
cy.intercept(option.method, option.url, option.response).as(alias)

interceptDataMap[`@${alias}`] = option
}

const usePactWait = (alias: AliasType) => {

const formattedAlias = formatAlias(alias)
const matchingRules: { matchingRules?: MatchingRulesType } = {};
if (interceptDataMap[`@${alias}`]) {
matchingRules.matchingRules = interceptDataMap[`@${alias}`]?.matchingRules
}

// Cypress versions older than 8.2 do not have a currentTest objects
const testCaseTitle = Cypress.currentTest ? Cypress.currentTest.title : ''
//NOTE: spread only works for array containing more than one item
if (formattedAlias.length > 1) {
cy.wait([...formattedAlias]).spread((...intercepts) => {
intercepts.forEach((intercept, index) => {

writePact({
intercept,
testCaseTitle: `${testCaseTitle}-${formattedAlias[index]}`,
pactConfig,
blocklist: headersBlocklist
blocklist: headersBlocklist,
...matchingRules,
})
})
})
} else {
cy.wait(formattedAlias).then((intercept) => {
const flattenIntercept = Array.isArray(intercept) ? intercept[0] : intercept

writePact({
intercept: flattenIntercept,
testCaseTitle: `${testCaseTitle}`,
pactConfig,
blocklist: headersBlocklist
blocklist: headersBlocklist,
...matchingRules,
})
})
}
Expand Down Expand Up @@ -107,6 +126,7 @@ const usePactRequest = (option: Partial<RequestOptionType>, alias: string) => {

Cypress.Commands.add('usePactWait', usePactWait)
Cypress.Commands.add('usePactRequest', usePactRequest)
Cypress.Commands.add('usePactIntercept', usePactIntercept)
Cypress.Commands.add('usePactGet', usePactGet)
Cypress.Commands.add('setupPact', setupPact)
Cypress.Commands.add('setupPactHeaderBlocklist', setupPactHeaderBlocklist)
40 changes: 37 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Interception } from 'cypress/types/net-stubbing'
import { Interception, RouteMatcher, RouteHandler } from 'cypress/types/net-stubbing'

export type AliasType = string | string[]

Expand Down Expand Up @@ -26,7 +26,8 @@ export type Interaction = {
query: string
} & BaseXHR
response: {
status: string | number | undefined
status: string | number | undefined,
matchingRules?: MatchingRulesType,
} & BaseXHR
}

Expand Down Expand Up @@ -69,10 +70,43 @@ export type RequestOptionType = {
url: string
}

type MatchingRulesTypeV2 = {
[K in string | number]: {
match: 'type' | 'regex',
regex?: string,
min?: number,
max?: number,
}
}

type MatchingRulesTypeV3 = {
body: {
[K in string | number]: {
"matchers": Array<{
match: 'type' | 'regex' | 'include' | 'integer' | 'decimal' | 'null' | 'datetime' | 'time' | 'date',
format?: string,
regex?: string,
min?: number,
max?: number,
}>,
}
}
}

export type MatchingRulesType = MatchingRulesTypeV2 | MatchingRulesTypeV3

export type InterceptOptionType = {
method: string | any, // cypress does not export Method type
url: RouteMatcher,
response: RouteHandler,
matchingRules: MatchingRulesType
};

export type PactFileType = {
intercept: Interception | XHRRequestAndResponse
testCaseTitle: string
pactConfig: PactConfigType
blocklist?: string[],
content?: any
content?: any,
matchingRules?: MatchingRulesType,
}
43 changes: 31 additions & 12 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Interception } from 'cypress/types/net-stubbing'
import { uniqBy, reverse, omit } from 'lodash'
import { AliasType, Interaction, PactConfigType, XHRRequestAndResponse, PactFileType, HeaderType } from 'types'
import { AliasType, Interaction, PactConfigType, XHRRequestAndResponse, PactFileType, HeaderType, MatchingRulesType } from 'types'
const pjson = require('../package.json')
export const formatAlias = (alias: AliasType) => {
if (Array.isArray(alias)) {
Expand All @@ -12,15 +12,15 @@ export const formatAlias = (alias: AliasType) => {
const constructFilePath = ({ consumerName, providerName }: PactConfigType) =>
`cypress/pacts/${providerName}-${consumerName}.json`

export const writePact = ({ intercept, testCaseTitle, pactConfig, blocklist }: PactFileType) => {
export const writePact = ({ intercept, testCaseTitle, pactConfig, blocklist, matchingRules }: PactFileType) => {
const filePath = constructFilePath(pactConfig)
cy.task('readFile', filePath)
.then((content) => {
if (content) {
const parsedContent = JSON.parse(content as string)
return constructPactFile({ intercept, testCaseTitle, pactConfig, blocklist, content: parsedContent })
return constructPactFile({ intercept, testCaseTitle, pactConfig, blocklist, content: parsedContent, matchingRules })
} else {
return constructPactFile({ intercept, testCaseTitle, pactConfig, blocklist })
return constructPactFile({ intercept, testCaseTitle, pactConfig, blocklist, matchingRules })
}
})
.then((data) => {
Expand All @@ -32,13 +32,29 @@ export const writePact = ({ intercept, testCaseTitle, pactConfig, blocklist }: P
}

export const omitHeaders = (headers: HeaderType, blocklist: string[]) => {
return omit(headers, [...blocklist])
return omit(headers, [...blocklist]);
}

export const sortHeaders = (headers: HeaderType) => {

if (!headers) {
return headers;
}

return Object.keys(headers).sort().reduce(
(obj:Record<string, string | string[]>, key) => {
obj[key] = headers[key];
return obj;
},
{}
);
}

const constructInteraction = (
intercept: Interception | XHRRequestAndResponse,
testTitle: string,
blocklist: string[]
blocklist: string[],
matchingRules?: MatchingRulesType,
): Interaction => {
const path = new URL(intercept.request.url).pathname
const search = new URL(intercept.request.url).search
Expand All @@ -49,18 +65,19 @@ const constructInteraction = (
request: {
method: intercept.request.method,
path: path,
headers: omitHeaders(intercept.request.headers, blocklist),
headers: sortHeaders(omitHeaders(intercept.request.headers, blocklist)),
body: intercept.request.body,
query: query
},
response: {
status: intercept.response?.statusCode,
headers: omitHeaders(intercept.response?.headers, blocklist),
body: intercept.response?.body
headers: sortHeaders(omitHeaders(intercept.response?.headers, blocklist)),
body: intercept.response?.body,
matchingRules: matchingRules,
}
}
}
export const constructPactFile = ({ intercept, testCaseTitle, pactConfig, blocklist = [], content }: PactFileType) => {
export const constructPactFile = ({ intercept, testCaseTitle, pactConfig, blocklist = [], content, matchingRules }: PactFileType) => {
const pactSkeletonObject = {
consumer: { name: pactConfig.consumerName },
provider: { name: pactConfig.providerName },
Expand All @@ -77,19 +94,21 @@ export const constructPactFile = ({ intercept, testCaseTitle, pactConfig, blockl
}

if (content) {
const interactions = [...content.interactions, constructInteraction(intercept, testCaseTitle, blocklist)]
const interactions = [...content.interactions, constructInteraction(intercept, testCaseTitle, blocklist, matchingRules)]
const nonDuplicatesInteractions = reverse(uniqBy(reverse(interactions), 'description'))

const data = {
...pactSkeletonObject,
...content,
interactions: nonDuplicatesInteractions
}

return data
}

return {
...pactSkeletonObject,
interactions: [...pactSkeletonObject.interactions, constructInteraction(intercept, testCaseTitle, blocklist)]
interactions: [...pactSkeletonObject.interactions, constructInteraction(intercept, testCaseTitle, blocklist, matchingRules)]
}
}

Expand Down