UNPKG

@ply-ct/ply

Version:

REST API Automated Testing

547 lines 22.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ResultFlowParser = exports.ResponseParser = exports.ResultPaths = exports.Verifier = exports.PlyResult = void 0; const yaml = __importStar(require("./yaml")); const location_1 = require("./location"); const storage_1 = require("./storage"); const retrieval_1 = require("./retrieval"); const code_1 = require("./code"); const compare_1 = require("./compare"); const util = __importStar(require("./util")); const runs_1 = require("./runs/runs"); class PlyResult { constructor(name, request, response) { this.name = name; this.status = 'Pending'; this.line = 0; this.request = { ...request }; this.request.headers = {}; Object.keys(request.headers).forEach((key) => { if (key !== 'Authorization') { this.request.headers[key] = request.headers[key]; } }); this.response = response; } /** * Returns the result with request/response bodies as objects (if parseable). */ getResult(options) { var _a; return { name: this.name, status: this.status, message: this.message, request: this.request, response: (_a = this.response) === null || _a === void 0 ? void 0 : _a.getResponse(this.response.runId, options) }; } merge(outcome) { this.status = outcome.status; this.message = outcome.message; this.line = outcome.line || 0; this.diff = outcome.diff; } } exports.PlyResult = PlyResult; class Verifier { constructor(name, expectedYaml, logger) { this.name = name; this.expectedYaml = expectedYaml; this.logger = logger; } /** * Verify expected vs actual results yaml after substituting values. * Diffs/messages always contain \n newlines. */ verify(actualYaml, values, runOptions) { // this.logger.debug(`Expected:\n${this.expectedYaml}\n` + `Actual:\n${actualYaml}\n`); const expected = new code_1.Code(this.expectedYaml.text, '#'); const actual = new code_1.Code(actualYaml.text, '#'); const diffs = new compare_1.Compare(this.logger).diffLines(expected.extractCode(), actual.extractCode(), values, runOptions === null || runOptions === void 0 ? void 0 : runOptions.trusted); let firstDiffLine = 0; let diffMsg = ''; if (diffs) { let line = 1; let actLine = 1; for (let i = 0; i < diffs.length; i++) { const diff = diffs[i]; if (diff.removed) { const correspondingAdd = i < diffs.length - 1 && diffs[i + 1].added ? diffs[i + 1] : null; if (!diff.ignored) { if (!firstDiffLine) { firstDiffLine = line; } diffMsg += this.diffLine(line, diff.count); diffMsg += '\n'; diffMsg += this.prefix(diff.value, '- ', expected.lines, line); if (correspondingAdd) { diffMsg += this.prefix(correspondingAdd.value, '+ ', actual.lines, actLine); } diffMsg += '===\n'; } line += diff.count; if (correspondingAdd) { i++; // corresponding add already covered actLine += correspondingAdd.count; } } else if (diff.added) { if (!diff.ignored) { // added with no corresponding remove if (!firstDiffLine) { firstDiffLine = line; } diffMsg += '-> ' + this.diffLine(actLine, diff.count); diffMsg += '\n'; diffMsg += this.prefix(diff.value, '+ ', actual.lines, actLine); diffMsg += '===\n'; } actLine += diff.count; } else { line += diff.count; actLine += diff.count; } } } if (firstDiffLine) { return { status: 'Failed', message: `Results differ from line ${this.diffLine(firstDiffLine, 1, this.name)}`, line: firstDiffLine, diff: diffMsg, diffs }; } else { return { status: 'Passed', line: 0 }; } } diffLine(line, count = 1, name) { if (typeof this.expectedYaml.start === 'undefined') { return '' + line; } let dl = `${line + this.expectedYaml.start}`; if (count > 1) { dl += `-${line + this.expectedYaml.start + count - 1}`; } if (this.expectedYaml.start > 0) { dl += ' ('; if (name) { dl += name + ':'; } dl += line; if (count > 1) { dl += `-${line + count - 1}`; } dl += ')'; } return dl; } /** * Adds pre to the beginning of each line in str (except trailing newline). * Optional codeLines, start to restore comments. */ prefix(str, pre, codeLines, start) { return util.lines(str).reduce((a, seg, i, arr) => { let line = i === arr.length - 1 && seg.length === 0 ? '' : pre + seg; if (line) { if (codeLines) { const codeLine = codeLines[start + i]; if (codeLine.comment) { line += codeLine.comment; } } line += '\n'; } return a + line; }, ''); } } exports.Verifier = Verifier; class ResultPaths { constructor(expected, actual, options, log, runs) { this.expected = expected; this.actual = actual; this.options = options; this.log = log; this.runs = runs; this.isFlowResult = false; } /** * excluding file extension */ static bases(options, retrieval, suiteName) { if (suiteName.endsWith('.ply')) { suiteName = suiteName.substring(0, suiteName.length - 4); } if (options.resultFollowsRelativePath && retrieval.location.isChildOf(options.testsLocation)) { const relPath = retrieval.location.relativeTo(options.testsLocation); const parent = new location_1.Location(relPath).parent; // undefined if relPath is just filename const resultFilePath = parent ? parent + '/' + suiteName : suiteName; return { expected: options.expectedLocation + '/' + resultFilePath, actual: options.actualLocation + '/' + resultFilePath, log: options.logLocation + '/' + resultFilePath, runs: options.logLocation + '/runs/' + resultFilePath }; } else { // flatly use the specified paths return { expected: options.expectedLocation + '/' + suiteName, actual: options.actualLocation + '/' + suiteName, log: options.logLocation + '/' + suiteName, runs: options.logLocation + '/runs' }; } } /** * Figures out locations and file extensions for results. * Result file path relative to configured result location is the same as retrieval relative * to configured tests location. */ static async create(options, retrieval, suiteName = retrieval.location.base) { const basePaths = this.bases(options, retrieval, suiteName); let ext = '.yaml'; if (!(await new retrieval_1.Retrieval(basePaths.expected + ext).exists)) { if ((await new retrieval_1.Retrieval(basePaths.expected + '.yml').exists) || retrieval.location.ext === 'yml') { ext = '.yml'; } } return new ResultPaths(new retrieval_1.Retrieval(basePaths.expected + ext), new storage_1.Storage(basePaths.actual + ext), options, new storage_1.Storage(basePaths.log + '.log'), new runs_1.Runs(basePaths.runs)); } /** * Figures out locations and file extensions for results. * Result file path relative to configured result location is the same as retrieval relative * to configured tests location. */ static createSync(options, retrieval, suiteName = retrieval.location.base) { const basePaths = this.bases(options, retrieval, suiteName); let ext = '.yaml'; if (!new storage_1.Storage(basePaths.expected + '.yaml').exists) { if (new storage_1.Storage(basePaths.expected + '.yml').exists || retrieval.location.ext === 'yml') { ext = '.yml'; } } return new ResultPaths(new retrieval_1.Retrieval(basePaths.expected + ext), new storage_1.Storage(basePaths.actual + ext), options, new storage_1.Storage(basePaths.log + '.log'), new runs_1.Runs(basePaths.runs)); } /** * Flow step result by id */ static extractById(yamlObj, id, instNum, indent = 2) { const hyphen = id.indexOf('-'); if (hyphen > 0) { const subflowId = id.substring(0, hyphen); id = id.substring(hyphen + 1); for (const key of Object.keys(yamlObj)) { if ((!instNum || key.endsWith(`_${instNum}`)) && yamlObj[key].id === subflowId) { const subflowStart = (yamlObj[key].__start || 0) + 1; yamlObj = yaml.load(key, yaml.dump(yamlObj[key], indent), true); for (const value of Object.values(yamlObj)) { if (typeof value === 'object' && typeof value.__start === 'number') { value.__start += subflowStart; } } break; } } } for (const key of Object.keys(yamlObj)) { if ((!instNum || key.endsWith(`_${instNum}`)) && yamlObj[key].id === id) { return yamlObj[key]; } } } /** * Newlines are always \n. */ async getExpectedYaml(name, instNum = 0) { let expected = await this.expected.read(); if (typeof expected === 'undefined') { throw new Error(`Expected result file not found: ${this.expected}`); } if (name) { let expectedObj; if (this.isFlowResult) { // name is (subflowId.)stepId expectedObj = ResultPaths.extractById(yaml.load(this.expected.toString(), expected, true), name, instNum, this.options.prettyIndent); } else { expectedObj = yaml.load(this.expected.toString(), expected, true)[name]; } if (!expectedObj) { let label = `${this.expected}#${name}`; if (instNum > 0) label += ` (${instNum})`; throw new Error(`Expected result not found: ${label}`); } let expectedLines; if (this.isFlowResult) { // exclude step info from request expected const { __start: start, __end: _end, status: _status, result: _result, message: _message, ...rawObj } = expectedObj; const startLine = util.lines(expected)[start]; let indent = this.options.prettyIndent || 2; expected = yaml.dump(rawObj, indent); if (name.startsWith('f')) { indent += this.options.prettyIndent || 2; // extra indent for subflow } expectedLines = util .lines(expected) .map((l) => (l.trim().length > 0 ? l.padStart(l.length + indent) : l)); expectedLines.unshift(startLine); return { start, text: expectedLines.join('\n') }; } else { expectedLines = util.lines(expected); return { start: expectedObj.__start || 0, text: expectedLines.slice(expectedObj.__start, expectedObj.__end + 1).join('\n') }; } } else { return { start: 0, text: expected }; } } async expectedExists(name, instNum = 0) { const expected = await this.expected.read(); if (typeof expected === 'undefined') return false; if (name) { const obj = yaml.load(this.expected.toString(), expected); if (this.isFlowResult) { return !!ResultPaths.extractById(obj, name, instNum); } else { return !!obj[name]; } } else { return true; } } /** * Newlines are always \n. Trailing \n is appended. */ getActualYaml(name, instNum = 0) { const actual = this.actual.read(); if (typeof actual === 'undefined') { throw new Error(`Actual result file not found: ${this.actual}`); } if (name) { let actualObj; if (this.isFlowResult) { actualObj = ResultPaths.extractById(yaml.load(this.actual.toString(), actual, true), name, instNum, this.options.prettyIndent); } else { actualObj = yaml.load(this.actual.toString(), actual, true)[name]; } if (!actualObj) { throw new Error(`Actual result not found: ${this.actual}#${name}`); } const actualLines = util.lines(actual); return { start: actualObj.__start || 0, text: actualLines.slice(actualObj.__start, actualObj.__end + 1).join('\n') + '\n' }; } else { return { start: 0, text: actual }; } } responsesFromActual() { if (this.actual.exists) { return new ResponseParser(this.actual, this.options).parse(); } else { return {}; } } flowInstanceFromActual(flowPath) { if (this.actual.exists) { return new ResultFlowParser(this.actual, this.options).parse(flowPath); } } } exports.ResultPaths = ResultPaths; /** * Parses a request's response from actual results. */ class ResponseParser { constructor(actualResult, options) { this.options = options; const contents = actualResult.read(); if (typeof contents === 'undefined') { throw new Error(`Actual result not found: ${actualResult}`); } this.actualYaml = contents; this.yamlLines = util.lines(this.actualYaml); this.actualObj = yaml.load(actualResult.toString(), this.actualYaml, true); } parse() { const responses = {}; for (const requestName of Object.keys(this.actualObj)) { let resultObj = this.actualObj[requestName]; if (resultObj) { let submitted; const submittedComment = util.lineComment(this.yamlLines[resultObj.__start]); if (submittedComment) { submitted = util.timeparse(submittedComment); } if (resultObj.response) { delete resultObj.status; // test status (for flows) // reparse result to get response line nums resultObj = yaml.load(requestName, yaml.dump(resultObj, this.options.prettyIndent || 2), true); const responseObj = resultObj.response; let elapsedMs; const elapsedMsComment = util.lineComment(this.yamlLines[resultObj.__start + responseObj.__start + 1]); if (elapsedMsComment) { elapsedMs = parseInt(elapsedMsComment.substring(0, elapsedMsComment.length - 2)); } const { __start, __end, ...response } = responseObj; response.source = `${requestName}:\n` + this.yamlLines .slice(resultObj.__start + responseObj.__start + 1, resultObj.__start + responseObj.__end) .join('\n'); if (submitted) { response.submitted = submitted; } if (elapsedMs) { response.time = elapsedMs; } responses[requestName] = response; } } } return responses; } } exports.ResponseParser = ResponseParser; /** * Parses a flow instance from actual results. */ class ResultFlowParser { constructor(actualResult, options) { this.options = options; const contents = actualResult.read(); if (typeof contents === 'undefined') { throw new Error(`Actual result not found: ${actualResult}`); } this.actualYaml = contents; this.yamlLines = util.lines(this.actualYaml); this.actualObj = yaml.load(actualResult.toString(), this.actualYaml, true); } parse(flowPath) { const flowInstance = { id: '', flowPath, status: 'Pending' }; for (const key of Object.keys(this.actualObj)) { const obj = this.actualObj[key]; if (obj.id.startsWith('f')) { const subflowInstance = { id: '', subflowId: obj.id, status: obj.status, flowInstanceId: '' }; this.parseStartEnd(subflowInstance, obj); // reparse subflow for step instance line numbers const subflowObj = yaml.load(key, yaml.dump(obj, 2), true); subflowInstance.stepInstances = this.getStepInstances(subflowObj, obj.__start + 1); if (!flowInstance.subflowInstances) flowInstance.subflowInstances = []; flowInstance.subflowInstances.push(subflowInstance); } } flowInstance.stepInstances = this.getStepInstances(this.actualObj); flowInstance.stepInstances.forEach((stepInst) => { if (flowInstance.status === 'Pending') flowInstance.status = 'In Progress'; if (flowInstance.status === 'In Progress' && stepInst.status !== 'Completed') { flowInstance.status = stepInst.status; } }); if (flowInstance.status === 'In Progress') flowInstance.status = 'Completed'; return flowInstance; } getStepInstances(obj, offset = 0) { var _a; const stepInstances = []; for (const stepKey of Object.keys(obj)) { const stepObj = obj[stepKey]; if ((_a = stepObj.id) === null || _a === void 0 ? void 0 : _a.startsWith('s')) { const stepInstance = { id: '', stepId: stepObj.id, status: stepObj.status, flowInstanceId: '' }; this.parseStartEnd(stepInstance, stepObj, offset); if (stepObj.request) { stepInstance.data = { request: yaml.dump(stepObj.request, this.options.prettyIndent || 2) }; if (stepObj.response) { stepInstance.data.response = yaml.dump(stepObj.response, this.options.prettyIndent || 2); } } else if (stepObj.data) { stepInstance.data = stepObj.data; } stepInstances.push(stepInstance); } } return stepInstances; } parseStartEnd(flowElementInstance, obj, offset = 0) { const startTimeComment = util.lineComment(this.yamlLines[obj.__start + offset]); if (startTimeComment) { flowElementInstance.start = util.timeparse(startTimeComment); if (flowElementInstance.start && this.yamlLines.length > obj.__end) { const elapsedMsComment = util.lineComment(this.yamlLines[obj.__end + offset]); if (elapsedMsComment) { const elapsedMs = parseInt(elapsedMsComment.substring(0, elapsedMsComment.length - 2)); flowElementInstance.end = new Date(flowElementInstance.start.getTime() + elapsedMs); } } } } } exports.ResultFlowParser = ResultFlowParser; //# sourceMappingURL=result.js.map