This commit is contained in:
Tutur33
2023-11-24 22:35:41 +01:00
parent 3c0b507a93
commit 7644b2a0f7
45165 changed files with 4803356 additions and 3 deletions
+53
View File
@@ -0,0 +1,53 @@
const get = require('lodash.get');
const set = require('lodash.set');
module.exports.parseErrors = (errors, response) => ({
actual: mapValuesByDataPath(errors, response),
expected: mapErrorsByDataPath(errors),
});
function mapValuesByDataPath(errors, response) {
return errors && errors.reduce((prev, error) => {
if (!error.dataPath) {
return prev;
}
const { root, fullPath } = getDataPath(error.dataPath);
const value = get(response, fullPath);
if (root !== fullPath || root.match(/[[.]/)) {
return set({ ...prev }, fullPath, value);
}
return { ...prev };
}, {});
}
function mapErrorsByDataPath(errors) {
return errors && errors.reduce((prev, error) => {
if (!error.params) {
return prev;
}
const { missingProperty, additionalProperty } = error.params;
const { fullPath } = getDataPath(error.dataPath);
if (missingProperty) {
const message = error.message.replace(/ '.*$/, '');
return set({ ...prev }, `${fullPath}.${missingProperty}`, message);
}
if (additionalProperty) {
const { message } = error;
return set({ ...prev }, `${fullPath}.${additionalProperty}`, message);
}
return set({ ...prev }, fullPath, error.message);
}, {});
}
function getDataPath(dataPath) {
const fullPath = dataPath
.replace(/^\./, '')
.replace(/\\/, '');
const [root] = fullPath.split('.');
return { root, fullPath };
}
+11
View File
@@ -0,0 +1,11 @@
module.exports.encode = (string) => {
const buffer = Buffer.from(string, 'utf8');
return buffer.toString('base64');
};
module.exports.decode = (string) => {
const buffer = Buffer.from(string, 'base64');
return buffer.toString();
};
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
messages: {
FAILED_TO_EXTRACT_RESPONSE_DETAILS: 'failed to extract response details',
EXPECTED_RESPONSE_TO_MATCH_SCHEMA: 'expected response to match API schema',
EXPECTED_RESPONSE_TO_NOT_MATCH_SCHEMA: 'expected response to not match API schema',
REQUIRED_API_DEFINITIONS_PATH: "'apiDefinitionsPath' is required",
DUPLICATE_API_DEFINITION: 'same api definition exist in two seperated files',
},
};
@@ -0,0 +1,17 @@
const chalk = require('chalk');
const columnify = require('columnify');
module.exports = (data) => columnify(data, {
columnSplitter: ' | ',
minWidth: 10,
headingTransform(title) {
switch (title) {
case 'statuses':
return chalk.yellow.underline.bold(`*${title.toUpperCase()}*`);
case 'method':
return chalk.green.underline.bold(`*${title.toUpperCase()}*`);
default:
return chalk.cyan.underline.bold(`*${title.toUpperCase()}*`);
}
},
});
+93
View File
@@ -0,0 +1,93 @@
/* eslint-disable no-console */
const chalk = require('chalk');
const get = require('lodash.get');
const set = require('lodash.set');
const flatten = require('lodash.flatten');
const fs = require('fs');
const schemaUtils = require('./schema-utils');
const buildTable = require('./coverage-table');
let coverage;
let schema;
module.exports.init = ({
reportCoverage, apiDefinitionsPath, buildSchemaOptions, exportCoverage,
}) => {
coverage = {};
const buildSchemaOptionsOverride = buildSchemaOptions || {};
schema = schemaUtils.getSchemaByFilesPath(apiDefinitionsPath, buildSchemaOptionsOverride);
let parsedCoverage;
if (reportCoverage === true || exportCoverage === true) {
process.on('beforeExit', () => {
parsedCoverage = getReport();
});
}
if (reportCoverage === true) {
process.on('beforeExit', () => {
printReport(parsedCoverage);
});
}
if (exportCoverage === true) {
process.on('beforeExit', () => {
printReportToFile(parsedCoverage);
});
}
};
module.exports.setCoverage = ({ path, method, status }) => {
if (!schema) {
console.warn('Coverage was not initiated. If you want to calculate coverage, make sure to call init() on coverage helper. Skipping coverage calculation');
return;
}
const route = schemaUtils.pathMatcher(schema, path, method);
set(coverage, `[${route}|${method}|${status}]`, true);
};
function getReport() {
const uncoveredDefinitions = Object.keys(schema)
.map((route) => getUncoveredDefinitions(route));
return flatten(uncoveredDefinitions)
.filter((api) => !!api);
}
module.exports.getReport = getReport;
function getUncoveredDefinitions(route) {
return Object
.keys(schema[route])
.map((method) => {
const statuses = Object
.keys(schema[route][method].responses)
.filter((status) => !(get(coverage, `[${route}|${method}|${status}]`)))
.toString();
return statuses ? { route, method: method.toUpperCase(), statuses } : undefined;
});
}
function printReport(report) {
console.info(chalk.bold('* API definitions coverage report *'));
if (report.length === 0) {
console.info(chalk.green('\nAll API definitions are covered\n'));
} else {
console.info(chalk.red('\nUncovered API definitions found'));
const table = buildTable(report);
console.info(table);
}
}
function printReportToFile(report) {
try {
fs.writeFileSync('./coverage.json', JSON.stringify(report));
} catch (e) {
console.info(chalk.red('Error writing report to file'));
console.info(chalk.red(e.message));
console.info(chalk.red(e.stack));
}
}
@@ -0,0 +1,2 @@
module.exports.filterMissingProps = (obj, requiredProps) => requiredProps
.filter((prop) => obj[prop] === undefined);
@@ -0,0 +1,129 @@
const get = require('lodash.get');
const urijs = require('uri-js');
function parseResponse(response) {
if (isFastifyResponse(response)) {
return parseFastifyResponse(response);
}
return parseGenericResponse(response);
}
function parseFastifyResponse(response) {
try {
const { res, req } = response.raw;
return {
request: {
method: getRequestMethod(req || res),
path: resolveUrlDataFastify(req.headers, req).path,
},
response: {
status: response.statusCode,
headers: response.headers,
body: response.json(),
},
};
} catch (error) {
return undefined;
}
}
function parseGenericResponse(response) {
try {
const { request, config } = response;
return {
request: {
method: getRequestMethod(config || request || response),
path: getRequestPath(request || response),
},
response: {
status: getResponseCode(response),
headers: getResponseHeaders(response),
body: getResponseBody(response),
},
};
} catch (error) {
return undefined;
}
}
function getRequestMethod(request) {
const { method } = request;
return method && method.toLowerCase();
}
function getRequestPath(request) {
// request promise
if (request.uri) {
return cleanPath(request.uri.pathname);
}
// axios
if (request.path) {
return cleanPath(request.path);
}
// supertest
if (get(request, 'req.path')) {
return cleanPath(request.req.path);
}
return undefined;
}
function getResponseHeaders(response) {
return response.headers;
}
function getResponseCode(response) {
// request-promise
if (response.statusCode) {
return response.statusCode;
}
// other
return response.status;
}
function getResponseBody(response) {
// request-promise/other
if (response.body) {
return response.body;
}
// axios
if (response.data) {
return response.data;
}
return undefined;
}
function cleanPath(path) {
return path
.split('?')[0] // clean query params
.replace(/\/*$/, ''); // clean trailing slashes
}
function isFastifyResponse(response) {
const userAgent = response && response.raw && response.raw.req && response.raw.req.headers['user-agent'];
if (userAgent) {
return userAgent.toLowerCase() === 'lightmyrequest';
}
return false;
}
function resolveUrlDataFastify(headers, req) {
const scheme = `${headers[':scheme'] ? `${headers[':scheme']}:` : ''}//`;
const host = headers[':authority'] || headers.host;
const path = headers[':path'] || req.url;
return urijs.parse(scheme + host + path);
}
module.exports = {
parseResponse,
};
+77
View File
@@ -0,0 +1,77 @@
const get = require('lodash.get');
const apiSchemaBuilder = require('api-schema-builder');
const { messages } = require('./common');
const base64 = require('./base64');
const getSchema = (() => {
const schemas = {};
return (filePath, schemaBuilderOpts) => {
const encodedFilePath = base64.encode(filePath);
if (!schemas[encodedFilePath]) {
schemas[encodedFilePath] = apiSchemaBuilder.buildSchemaSync(filePath, schemaBuilderOpts);
}
return schemas[encodedFilePath];
};
})();
module.exports.getSchemaByFilesPath = (apiDefinitionsPath, buildSchemaOptions) => {
const filesPaths = Array.isArray(apiDefinitionsPath) ? apiDefinitionsPath : [apiDefinitionsPath];
const schemas = filesPaths.map((path) => getSchema(path, buildSchemaOptions));
// eslint-disable-next-line array-callback-return,consistent-return
const schema = schemas.reduce((acc, cur) => {
// eslint-disable-next-line no-restricted-syntax
for (const key of Object.keys(cur)) {
if (acc[key]) {
throw new Error(`${messages.DUPLICATE_API_DEFINITION}: ${key}`);
}
acc[key] = cur[key];
}
return acc;
}, {});
return schema;
};
module.exports.getValidatorByPathMethodAndCode = (schema, request, response) => {
const route = pathMatcher(schema, request.path, request.method);
return get(schema, `${route}.${request.method}.responses.${response.status}`);
};
module.exports.pathMatcher = pathMatcher;
function pathMatcher(routes, path, method) {
return Object
.keys(routes)
.sort((currentRoute, nextRoute) => {
const firstResult = calculateRouteScore(currentRoute);
const secondResult = calculateRouteScore(nextRoute);
return firstResult - secondResult;
})
.filter((route) => {
const routeArr = route.split('/');
const pathArr = path.split('/');
if (routeArr.length !== pathArr.length) return false;
return routeArr.every((seg, idx) => {
if (seg === pathArr[idx]) return true;
// if current path segment is param
if (seg.startsWith(':') && pathArr[idx]) return true;
return false;
});
}).filter(((route) => routes[route][String(method).toLowerCase()]))[0];
}
function calculateRouteScore(route) {
return route
// split to path segments
.split('/')
// mark path params locations
.map((pathSegment) => pathSegment.includes(':'))
// give weight to each path segment according to its location
.map((isPathParam, i, pathSegments) => isPathParam * (10 ** pathSegments.length - i))
.reduce((sum, seg) => sum + seg, 0); // summarize the path score
}