@ply-ct/ply
Version:
REST API Automated Testing
547 lines • 22.3 kB
JavaScript
"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