315 lines
8.4 KiB
JavaScript
315 lines
8.4 KiB
JavaScript
'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;
|