Skip to content

Commit

Permalink
feat: add AND and OR operator to filterCalls class method
Browse files Browse the repository at this point in the history
  • Loading branch information
blephy committed Jan 28, 2025
1 parent 101d108 commit 1cdb1d4
Show file tree
Hide file tree
Showing 7 changed files with 383 additions and 26 deletions.
21 changes: 11 additions & 10 deletions docs/docs/api/MockAgent.md
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ client.intercept({ path: '/', method: 'GET' }).reply(200, 'hi !').registerCallHi
await request('http://example.com') // intercepted
await request('http://example.com', { method: 'POST', body: JSON.stringify({ data: 'hello' }), headers: { 'content-type': 'application/json' }})

mockAgent.getCallHistory('my-specific-history-name').calls()
mockAgent.getCallHistory('my-specific-history-name')?.calls()
// Returns [
// MockCallHistoryLog {
// body: undefined,
Expand Down Expand Up @@ -663,13 +663,14 @@ const mockAgent = new MockAgent()

const mockAgentHistory = mockAgent.getCallHistory()

mockAgentHistory.calls() // returns an array of MockCallHistoryLogs
mockAgentHistory.firstCall() // returns the first MockCallHistoryLogs or undefined
mockAgentHistory.lastCall() // returns the last MockCallHistoryLogs or undefined
mockAgentHistory.nthCall(3) // returns the third MockCallHistoryLogs or undefined
mockAgentHistory.filterCalls({ path: '/endpoint', hash: '#hash-value' }) // returns an Array of MockCallHistoryLogs WHERE path === /endpoint OR hash === #hash-value
mockAgentHistory.filterCalls(/"data": "{}"/) // returns an Array of MockCallHistoryLogs where any value match regexp
mockAgentHistory.filterCalls('application/json') // returns an Array of MockCallHistoryLogs where any value === 'application/json'
mockAgentHistory.filterCalls((log) => log.path === '/endpoint') // returns an Array of MockCallHistoryLogs when given function returns true
mockAgentHistory.clear() // clear the history
mockAgentHistory?.calls() // returns an array of MockCallHistoryLogs
mockAgentHistory?.firstCall() // returns the first MockCallHistoryLogs or undefined
mockAgentHistory?.lastCall() // returns the last MockCallHistoryLogs or undefined
mockAgentHistory?.nthCall(3) // returns the third MockCallHistoryLogs or undefined
mockAgentHistory?.filterCalls({ path: '/endpoint', hash: '#hash-value' }) // returns an Array of MockCallHistoryLogs WHERE path === /endpoint OR hash === #hash-value
mockAgentHistory?.filterCalls({ path: '/endpoint', hash: '#hash-value' }, { operator: 'AND' }) // returns an Array of MockCallHistoryLogs WHERE path === /endpoint AND hash === #hash-value
mockAgentHistory?.filterCalls(/"data": "{}"/) // returns an Array of MockCallHistoryLogs where any value match regexp
mockAgentHistory?.filterCalls('application/json') // returns an Array of MockCallHistoryLogs where any value === 'application/json'
mockAgentHistory?.filterCalls((log) => log.path === '/endpoint') // returns an Array of MockCallHistoryLogs when given function returns true
mockAgentHistory?.clear() // clear the history
```
192 changes: 192 additions & 0 deletions docs/docs/api/MockCallHistory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# Class: MockCallHistory

Access to an instance with :

```js
const mockAgent = new MockAgent({ enableCallHistory: true })
mockAgent.getCallHistory()
// or
const mockAgent = new MockAgent()
mockAgent.enableMockHistory()
mockAgent.getCallHistory()
// or

const mockAgent = new MockAgent()
const mockClient = mockAgent.get('http://localhost:3000')
mockClient
.intercept({ path: '/' })
.reply(200, 'hello')
.registerCallHistory('my-custom-history')
mockAgent.getCallHistory('my-custom-history')
```

## class methods

### clear

Clear all MockCallHistoryLog registered

```js
mockAgent.getCallHistory()?.clear() // clear only mockAgent history
mockAgent.getCallHistory('my-custom-history')?.clear() // clear only 'my-custom-history' history
```
### calls
Get all MockCallHistoryLog registered as an array
```js
mockAgent.getCallHistory()?.calls()
```
### firstCall
Get the first MockCallHistoryLog registered or undefined
```js
mockAgent.getCallHistory()?.firstCall()
```
### lastCall
Get the last MockCallHistoryLog registered or undefined
```js
mockAgent.getCallHistory()?.lastCall()
```
### nthCall
Get the nth MockCallHistoryLog registered or undefined
```js
mockAgent.getCallHistory()?.nthCall(3) // the third MockCallHistoryLog registered
```
### filterCallsByProtocol
Filter MockCallHistoryLog by protocol.
> more details for the first parameter can be found [here](#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByProtocol(/https/)
mockAgent.getCallHistory()?.filterCallsByProtocol('https:')
```
### filterCallsByHost
Filter MockCallHistoryLog by host.
> more details for the first parameter can be found [here](#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByHost(/localhost/)
mockAgent.getCallHistory()?.filterCallsByHost('localhost:3000')
```
### filterCallsByPort
Filter MockCallHistoryLog by port.
> more details for the first parameter can be found [here](#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByPort(/3000/)
mockAgent.getCallHistory()?.filterCallsByPort('3000')
mockAgent.getCallHistory()?.filterCallsByPort('')
```
### filterCallsByOrigin
Filter MockCallHistoryLog by origin.
> more details for the first parameter can be found [here](#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByOrigin(/http:\/\/localhost:3000/)
mockAgent.getCallHistory()?.filterCallsByOrigin('http://localhost:3000')
```
### filterCallsByPath
Filter MockCallHistoryLog by path.
> more details for the first parameter can be found [here](#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByPath(/api\/v1\/graphql/)
mockAgent.getCallHistory()?.filterCallsByPath('/api/v1/graphql')
```
### filterCallsByHash
Filter MockCallHistoryLog by hash.
> more details for the first parameter can be found [here](#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByPath(/hash/)
mockAgent.getCallHistory()?.filterCallsByPath('#hash')
```
### filterCallsByFullUrl
Filter MockCallHistoryLog by fullUrl. fullUrl contains protocol, host, port, path, hash, and query params
> more details for the first parameter can be found [here](#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByFullUrl(/https:\/\/localhost:3000\/\?query=value#hash/)
mockAgent.getCallHistory()?.filterCallsByFullUrl('https://localhost:3000/?query=value#hash')
```
### filterCallsByMethod
Filter MockCallHistoryLog by method.
> more details for the first parameter can be found [here](#filter-parameter)
```js
mockAgent.getCallHistory()?.filterCallsByMethod(/POST/)
mockAgent.getCallHistory()?.filterCallsByMethod('POST')
```
### filterCalls
This class method is a meta function / alias to apply complex filtering in one way.
Parameters :
- criteria : this first parameter. a function, regexp or object.
- function : filter MockCallHistoryLog when the function returns false
- regexp : filter MockCallHistoryLog when the regexp does not match on MockCallHistoryLog.toString() ([see](./MockCallHistoryLog.md#to-string))
- object : an object with MockCallHistoryLog properties as keys to apply multiple filters. each values are a [filter parameter](#filter-parameter)
- options : the second parameter. an object.
- options.operator : `'AND'` or `'OR'` (default `'OR'`). Used only if criteria is an object. see below
```js
mockAgent.getCallHistory()?.filterCalls((log) => log.hash === value && log.headers?.['authorization'] !== undefined)
mockAgent.getCallHistory()?.filterCalls(/"data": "{ "errors": "wrong body" }"/)

// returns MockCallHistoryLog which have
// - a hash containing my-hash
// - OR
// - a path equal to /endpoint
mockAgent.getCallHistory()?.filterCalls({ hash: /my-hash/, path: '/endpoint' })

// returns MockCallHistoryLog which have
// - a hash containing my-hash
// - AND
// - a path equal to /endpoint
mockAgent.getCallHistory()?.filterCalls({ hash: /my-hash/, path: '/endpoint' }, { operator: 'AND' })
```
## filter parameter
Can be :
- string. filtered if `value !== parameterValue`
- null. filtered if `value !== parameterValue`
- undefined. filtered if `value !== parameterValue`
- regexp. filtered if `!parameterValue.test(value)`
43 changes: 43 additions & 0 deletions docs/docs/api/MockCallHistoryLog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Class: MockCallHistoryLog

Access to an instance with :

```js
const mockAgent = new MockAgent({ enableCallHistory: true })
mockAgent.getCallHistory()?.firstCall()
```
## class properties
- body `mockAgent.getCallHistory()?.firstCall()?.body`
- headers `mockAgent.getCallHistory()?.firstCall()?.headers` an object
- method `mockAgent.getCallHistory()?.firstCall()?.method` a string
- fullUrl `mockAgent.getCallHistory()?.firstCall()?.fullUrl` a string containing the protocol, origin, path, query and hash
- origin `mockAgent.getCallHistory()?.firstCall()?.origin` a string containing the protocol and the host
- headers `mockAgent.getCallHistory()?.firstCall()?.headers` an object
- path `mockAgent.getCallHistory()?.firstCall()?.path` a string always starting with `/`
- searchParams `mockAgent.getCallHistory()?.firstCall()?.searchParams` an object
- protocol `mockAgent.getCallHistory()?.firstCall()?.protocol` a string (`https:`)
- host `mockAgent.getCallHistory()?.firstCall()?.host` a string
- port `mockAgent.getCallHistory()?.firstCall()?.port` an empty string or a string containing numbers
- hash `mockAgent.getCallHistory()?.firstCall()?.hash` an empty string or a string starting with `#`
## class methods
### toMap
Returns a Map instance
```js
mockAgent.getCallHistory()?.firstCall()?.toMap().get('hash')
// #hash
```
### toString
Returns a a string computed with any class property name and value pair
```js
mockAgent.getCallHistory()?.firstCall()?.toString()
// protocol->https:|host->localhost:4000|port->4000|origin->https://localhost:4000|path->/endpoint|hash->#here|searchParams->{"query":"value"}|fullUrl->https://localhost:4000/endpoint?query=value#here|method->PUT|body->"{ "data": "hello" }"|headers->{"content-type":"application/json"}
```
4 changes: 4 additions & 0 deletions docs/docs/best-practices/mocking-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ Calling `mockAgent.close()` will automatically clear and delete every call histo
Explore other MockAgent functionality [here](/docs/docs/api/MockAgent.md)
Explore other MockCallHistory functionality [here](/docs/docs/api/MockCallHistory.md)
Explore other MockCallHistoryLog functionality [here](/docs/docs/api/MockCallHistoryLog.md)
## Debug Mock Value
When the interceptor and the request options are not the same, undici will automatically make a real HTTP request. To prevent real requests from being made, use `mockAgent.disableNetConnect()`:
Expand Down
59 changes: 45 additions & 14 deletions lib/mock/mock-call-history.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,37 @@ const {
} = require('./mock-symbols')
const { InvalidArgumentError } = require('../core/errors')

function handleFilterCallsWithOptions (criteria, options, handler, store) {
switch (options.operator) {
case 'OR':
store.push(...handler(criteria))

return store
case 'AND':
return handler.call({ logs: store }, criteria)
default:
// guard -- should never happens because buildAndValidateFilterCallsOptions is called before
throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
}
}

function buildAndValidateFilterCallsOptions (options = {}) {
const finalOptions = {}

if ('operator' in options) {
if (typeof options.operator !== 'string' || (options.operator.toUpperCase() !== 'OR' && options.operator.toUpperCase() !== 'AND')) {
throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
}

return {
...finalOptions,
operator: options.operator.toUpperCase()
}
}

return finalOptions
}

function makeFilterCalls (parameterName) {
return (parameterValue) => {
if (typeof parameterValue === 'string' || parameterValue == null) {
Expand Down Expand Up @@ -160,15 +191,13 @@ class MockCallHistory {
return this.logs.at(number - 1)
}

filterCalls (criteria) {
filterCalls (criteria, options) {
// perf
if (this.logs.length === 0) {
return this.logs
}
if (typeof criteria === 'function') {
return this.logs.filter((log) => {
return criteria(log)
})
return this.logs.filter(criteria)
}
if (criteria instanceof RegExp) {
return this.logs.filter((log) => {
Expand All @@ -181,38 +210,40 @@ class MockCallHistory {
return this.logs
}

const maybeDuplicatedLogsFiltered = []
const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options) }

let maybeDuplicatedLogsFiltered = []
if ('protocol' in criteria) {
maybeDuplicatedLogsFiltered.push(...this.filterCallsByProtocol(criteria.protocol))
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered)
}
if ('host' in criteria) {
maybeDuplicatedLogsFiltered.push(...this.filterCallsByHost(criteria.host))
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered)
}
if ('port' in criteria) {
maybeDuplicatedLogsFiltered.push(...this.filterCallsByPort(criteria.port))
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered)
}
if ('origin' in criteria) {
maybeDuplicatedLogsFiltered.push(...this.filterCallsByOrigin(criteria.origin))
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered)
}
if ('path' in criteria) {
maybeDuplicatedLogsFiltered.push(...this.filterCallsByPath(criteria.path))
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered)
}
if ('hash' in criteria) {
maybeDuplicatedLogsFiltered.push(...this.filterCallsByHash(criteria.hash))
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered)
}
if ('fullUrl' in criteria) {
maybeDuplicatedLogsFiltered.push(...this.filterCallsByFullUrl(criteria.fullUrl))
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered)
}
if ('method' in criteria) {
maybeDuplicatedLogsFiltered.push(...this.filterCallsByMethod(criteria.method))
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered)
}

const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)]

return uniqLogsFiltered
}

throw new InvalidArgumentError('criteria parameter should be one of string, function, regexp, or object')
throw new InvalidArgumentError('criteria parameter should be one of function, regexp, or object')
}

filterCallsByProtocol = makeFilterCalls.call(this, 'protocol')
Expand Down
Loading

0 comments on commit 1cdb1d4

Please sign in to comment.