'use strict'; const { kMockCallHistoryAddLog } = 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) { return this.logs.filter((log) => { return log[parameterName] === parameterValue; }); } if (parameterValue instanceof RegExp) { return this.logs.filter((log) => { return parameterValue.test(log[parameterName]); }); } throw new InvalidArgumentError( `${parameterName} parameter should be one of string, regexp, undefined or null` ); }; } function computeUrlWithMaybeSearchParameters(requestInit) { // path can contains query url parameters // or query can contains query url parameters try { const url = new URL(requestInit.path, requestInit.origin); // requestInit.path contains query url parameters // requestInit.query is then undefined if (url.search.length !== 0) { return url; } // requestInit.query can be populated here url.search = new URLSearchParams(requestInit.query).toString(); return url; } catch (error) { throw new InvalidArgumentError( 'An error occurred when computing MockCallHistoryLog.url', { cause: error } ); } } class MockCallHistoryLog { constructor(requestInit = {}) { this.body = requestInit.body; this.headers = requestInit.headers; this.method = requestInit.method; const url = computeUrlWithMaybeSearchParameters(requestInit); this.fullUrl = url.toString(); this.origin = url.origin; this.path = url.pathname; this.searchParams = Object.fromEntries(url.searchParams); this.protocol = url.protocol; this.host = url.host; this.port = url.port; this.hash = url.hash; } toMap() { return new Map([ ['protocol', this.protocol], ['host', this.host], ['port', this.port], ['origin', this.origin], ['path', this.path], ['hash', this.hash], ['searchParams', this.searchParams], ['fullUrl', this.fullUrl], ['method', this.method], ['body', this.body], ['headers', this.headers], ]); } toString() { const options = { betweenKeyValueSeparator: '->', betweenPairSeparator: '|', }; let result = ''; this.toMap().forEach((value, key) => { if (typeof value === 'string' || value === undefined || value === null) { result = `${result}${key}${options.betweenKeyValueSeparator}${value}${options.betweenPairSeparator}`; } if ( (typeof value === 'object' && value !== null) || Array.isArray(value) ) { result = `${result}${key}${options.betweenKeyValueSeparator}${JSON.stringify(value)}${options.betweenPairSeparator}`; } // maybe miss something for non Record / Array headers and searchParams here }); // delete last betweenPairSeparator return result.slice(0, -1); } } class MockCallHistory { logs = []; calls() { return this.logs; } firstCall() { return this.logs.at(0); } lastCall() { return this.logs.at(-1); } nthCall(number) { if (typeof number !== 'number') { throw new InvalidArgumentError('nthCall must be called with a number'); } if (!Number.isInteger(number)) { throw new InvalidArgumentError('nthCall must be called with an integer'); } if (Math.sign(number) !== 1) { throw new InvalidArgumentError( 'nthCall must be called with a positive value. use firstCall or lastCall instead' ); } // non zero based index. this is more human readable return this.logs.at(number - 1); } filterCalls(criteria, options) { // perf if (this.logs.length === 0) { return this.logs; } if (typeof criteria === 'function') { return this.logs.filter(criteria); } if (criteria instanceof RegExp) { return this.logs.filter((log) => { return criteria.test(log.toString()); }); } if (typeof criteria === 'object' && criteria !== null) { // no criteria - returning all logs if (Object.keys(criteria).length === 0) { return this.logs; } const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options), }; let maybeDuplicatedLogsFiltered = []; if ('protocol' in criteria) { maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions( criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered ); } if ('host' in criteria) { maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions( criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered ); } if ('port' in criteria) { maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions( criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered ); } if ('origin' in criteria) { maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions( criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered ); } if ('path' in criteria) { maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions( criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered ); } if ('hash' in criteria) { maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions( criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered ); } if ('fullUrl' in criteria) { maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions( criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered ); } if ('method' in criteria) { maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions( criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered ); } const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)]; return uniqLogsFiltered; } throw new InvalidArgumentError( 'criteria parameter should be one of function, regexp, or object' ); } filterCallsByProtocol = makeFilterCalls.call(this, 'protocol'); filterCallsByHost = makeFilterCalls.call(this, 'host'); filterCallsByPort = makeFilterCalls.call(this, 'port'); filterCallsByOrigin = makeFilterCalls.call(this, 'origin'); filterCallsByPath = makeFilterCalls.call(this, 'path'); filterCallsByHash = makeFilterCalls.call(this, 'hash'); filterCallsByFullUrl = makeFilterCalls.call(this, 'fullUrl'); filterCallsByMethod = makeFilterCalls.call(this, 'method'); clear() { this.logs = []; } [kMockCallHistoryAddLog](requestInit) { const log = new MockCallHistoryLog(requestInit); this.logs.push(log); return log; } *[Symbol.iterator]() { for (const log of this.calls()) { yield log; } } } module.exports.MockCallHistory = MockCallHistory; module.exports.MockCallHistoryLog = MockCallHistoryLog;