UNPKG

@ply-ct/ply

Version:

REST API Automated Testing

507 lines 21.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.PlyFlow = exports.FlowStepResults = void 0; const minimatch_1 = require("minimatch"); const values_1 = require("./values"); const flowbee = __importStar(require("./flowbee")); const event_1 = require("./event"); const log_1 = require("./log"); const step_1 = require("./step"); const names_1 = require("./names"); const util = __importStar(require("./util")); const replace_1 = require("./replace"); const names_2 = require("./names"); class FlowStepResults { constructor(flow) { this.flow = flow; this.results = []; } add(stepId, result) { this.results.push({ ...result, stepId }); } get latest() { if (this.results.length > 0) { return this.results[this.results.length - 1]; } else { return { name: this.flow, status: 'Pending', message: '' }; } } latestBad() { return this.latest.status === 'Failed' || this.latest.status === 'Errored'; } get overall() { let anySubmitted = false; for (const result of this.results) { if (result.status === 'Failed' || result.status === 'Errored') { return result; } else if (result.status === 'Submitted') { anySubmitted = true; } } return { name: this.flow, status: anySubmitted ? 'Submitted' : 'Passed', message: '' }; } /** * Step result values */ get values() { return this.results.reduce((values, result) => { if (result.data) { let data = result.data; if (typeof data === 'object') { data = { ...data }; // clone array or object } let plyResults = values[names_2.RESULTS]; if (!plyResults) { plyResults = {}; values[names_2.RESULTS] = plyResults; } let stepResult = plyResults[result.stepId]; if (!stepResult) { stepResult = {}; plyResults[result.stepId] = stepResult; } if (typeof data === 'object' && !Array.isArray(data)) { if (data.request) { stepResult.request = data.request; if (typeof stepResult.request.body === 'string' && util.isJson(stepResult.request.body)) { stepResult.request.body = JSON.parse(stepResult.request.body); } } if (data.response) { stepResult.response = data.response; if (typeof stepResult.response.body === 'string' && util.isJson(stepResult.response.body)) { stepResult.response.body = JSON.parse(stepResult.response.body); } } } else { stepResult.data = data; } } return values; }, {}); } } exports.FlowStepResults = FlowStepResults; class PlyFlow { constructor(flow, requestSuite, logger) { this.flow = flow; this.requestSuite = requestSuite; this.logger = logger; this.type = 'flow'; this.start = 0; this.maxLoops = 0; this._onFlow = new event_1.TypedEvent(); this.name = flowbee.getFlowName(flow); this.instance = this.newInstance(); this.results = new FlowStepResults(this.name); } onFlow(listener) { this._onFlow.on(listener); } clone() { return new PlyFlow(this.flow, this.requestSuite, this.logger); } newInstance() { const id = util.genId(); this.instance = { id, runId: id, flowPath: this.flow.path, status: 'Pending' }; return this.instance; } /** * Flow input values */ getFlowValues(values, runOptions) { var _a; const flowValues = {}; if ((_a = this.flow.attributes) === null || _a === void 0 ? void 0 : _a.values) { const rows = JSON.parse(this.flow.attributes.values); for (const row of rows) { let rowVal = row[1]; if (rowVal === null || rowVal === void 0 ? void 0 : rowVal.trim().length) { if ((0, values_1.isExpression)(rowVal)) { rowVal = (0, replace_1.replaceLine)(rowVal, values, { trusted: runOptions === null || runOptions === void 0 ? void 0 : runOptions.trusted, logger: this.logger }); } const numVal = Number(row[1]); if (!isNaN(numVal)) rowVal = numVal; else if (row[1] === 'true' || row[1] === 'false') rowVal = row[1] === 'true'; else if (util.isJson(row[1])) rowVal = JSON.parse(row[1]); flowValues[row[0]] = rowVal; } // run values override even flow-configured vals if (runOptions === null || runOptions === void 0 ? void 0 : runOptions.values) { const runValue = runOptions.values[row[0]] || runOptions.values[`\${${row[0]}}`]; if (runValue) flowValues[row[0]] = runValue; } } } return flowValues; } /** * returns missing value names */ validateValues(values) { var _a; const missingRequired = []; if ((_a = this.flow.attributes) === null || _a === void 0 ? void 0 : _a.values) { const rows = JSON.parse(this.flow.attributes.values); for (const row of rows) { if (row[2] === 'true' && (values[row[0]] === undefined || values[rows[0]] === null)) { missingRequired.push(row[0]); } } } return missingRequired; } /** * Flow output values */ getReturnValues(values, trusted = false) { var _a; if ((_a = this.flow.attributes) === null || _a === void 0 ? void 0 : _a.return) { const rows = JSON.parse(this.flow.attributes.return); const returnVals = {}; for (const row of rows) { const expr = row[1]; const val = (0, replace_1.replaceLine)(expr, values, { trusted, logger: this.logger }); if (val.trim().length) { returnVals[row[0]] = val; } } return returnVals; } } /** * Run a ply flow. */ async run(runtime, values, runOptions, runNum) { var _a, _b, _c; this.newInstance(); this.results = new FlowStepResults(this.name); values[names_1.RUN_ID] = this.instance.runId || util.genId(); // flow values supersede file-based const flowValues = this.getFlowValues(values, runOptions); for (const flowValKey of Object.keys(flowValues)) { values[flowValKey] = flowValues[flowValKey]; } this.maxLoops = runtime.options.maxLoops || 10; if ((_a = this.flow.attributes) === null || _a === void 0 ? void 0 : _a.maxLoops) { this.maxLoops = parseInt(this.flow.attributes.maxLoops); } if (((_b = this.flow.attributes) === null || _b === void 0 ? void 0 : _b.bail) === 'true') { runtime.options.bail = true; } runtime.results.actual.write(''); const expectedExists = await this.requestSuite.runtime.results.expectedExists(); if (!expectedExists && (runOptions === null || runOptions === void 0 ? void 0 : runOptions.submitIfExpectedMissing)) { runOptions.submit = true; } if ((runOptions === null || runOptions === void 0 ? void 0 : runOptions.createExpected) || (!expectedExists && (runOptions === null || runOptions === void 0 ? void 0 : runOptions.createExpectedIfMissing))) { this.logger.info(`Creating expected result: ${runtime.results.expected}`); runtime.results.expected.write(''); runOptions.createExpected = true; } const startStep = (_c = this.flow.steps) === null || _c === void 0 ? void 0 : _c.find((s) => s.path === 'start'); if (!startStep) { throw new Error(`Cannot find start step in flow: ${this.flow.path}`); } const runId = this.logger.level === log_1.LogLevel.debug ? ` (${this.instance.runId})` : ''; this.logger.info(`Running flow '${this.name}'${runId}`); this.instance.status = 'In Progress'; this.instance.values = values; this.instance.start = new Date(); this.emit('start', 'flow', this.instance); await this.runSubflows(this.getSubflows('Before'), runtime, values, runOptions, runNum); if (this.results.latestBad() && runtime.options.bail) { return this.endFlow(); } await this.exec(startStep, runtime, values, undefined, runOptions, runNum); if (this.results.latestBad() && runtime.options.bail) { return this.endFlow(); } await this.runSubflows(this.getSubflows('After'), runtime, values, runOptions, runNum); return this.endFlow(); } endFlow() { this.instance.end = new Date(); if (this.instance.status === 'Errored' || this.instance.status === 'Failed') { this.emit('error', 'flow', this.instance); } else { this.instance.status = 'Completed'; this.emit('finish', 'flow', this.instance); } return this.results.overall; } /** * Executes a step within a flow and recursively executes the following step(s). */ async exec(step, runtime, values, incomingLinkId, runOptions, runNum, subflow) { var _a, _b, _c, _d, _e, _f, _g; await this.runSubflows(this.getSubflows('Before', step), runtime, values, runOptions, runNum); if (this.results.latestBad() && runtime.options.bail) { return; } let insts = 0; if (this.instance.stepInstances && !runtime.options.parallel) { insts = (_a = this.instance.stepInstances.filter((si) => si.stepId === step.id)) === null || _a === void 0 ? void 0 : _a.length; } const plyStep = new step_1.PlyStep(step, this.requestSuite, this.logger, this.flow, this.instance, subflow === null || subflow === void 0 ? void 0 : subflow.subflow); if (insts) { let maxLoops = this.maxLoops; if (step.path === 'start') maxLoops = 1; if ((_b = step.attributes) === null || _b === void 0 ? void 0 : _b.maxLoops) { maxLoops = parseInt(step.attributes.maxLoops); } if (isNaN(maxLoops)) this.stepError(plyStep, `Invalid maxLoops: ${maxLoops}`); else if (insts + 1 > maxLoops) { this.stepError(plyStep, `Max loops (${maxLoops} reached for step: ${step.id}`); } } if (subflow) { if (!subflow.instance.stepInstances) { subflow.instance.stepInstances = []; } subflow.instance.stepInstances.push(plyStep.instance); } else { if (!this.instance.stepInstances) { this.instance.stepInstances = []; } this.instance.stepInstances.push(plyStep.instance); } this.emit('start', 'step', plyStep.instance); this.logger.info('Executing step', plyStep.name); this.emit('exec', 'step', plyStep.instance); let result; if (runtime.options.validate) { // perform any preflight validations if (plyStep.step.path === 'start') { const missing = this.validateValues(values); if (missing.length) { plyStep.instance.status = 'Errored'; result = { name: this.name, status: 'Errored', message: `Missing required value(s): ${missing.join(', ')}` }; } } } if (!result) { result = await plyStep.run(runtime, values, runOptions, runNum, insts); } result.start = (_c = plyStep.instance.start) === null || _c === void 0 ? void 0 : _c.getTime(); result.end = (_d = plyStep.instance.end) === null || _d === void 0 ? void 0 : _d.getTime(); result.data = plyStep.instance.data; this.results.add(plyStep.name, result); if (runtime.options.verbose) { this.requestSuite.logOutcome(plyStep, result, runNum, plyStep.stepName, values); } else if ((plyStep.step.path === 'start' || plyStep.step.path === 'stop') && !subflow) { // non-verbose only start/stop step values are logged const { [names_2.RESULTS]: _results, [names_1.RUN_ID]: _runId, ...loggedValues } = values; this.requestSuite.logOutcome(plyStep, result, runNum, plyStep.stepName, loggedValues); } else { this.requestSuite.logOutcome(plyStep, result, runNum, plyStep.stepName); } if (result.status === 'Waiting') { return; } if (this.results.latestBad()) { this.instance.status = plyStep.instance.status; if (subflow) subflow.instance.status = plyStep.instance.status; this.emit('error', 'step', plyStep.instance); if (result.status === 'Errored' || runtime.options.bail) { return; } } else { this.emit('finish', 'step', plyStep.instance); } await this.runSubflows(this.getSubflows('After', step), runtime, values, runOptions, runNum); if (this.results.latestBad() && runtime.options.bail) { return; } const outSteps = {}; if (step.links) { for (const link of step.links) { const result = (_e = plyStep.instance.result) === null || _e === void 0 ? void 0 : _e.trim(); if ((!result && !link.result) || result === link.result) { let outStep; if (subflow) { outStep = (_f = subflow.subflow.steps) === null || _f === void 0 ? void 0 : _f.find((s) => s.id === link.to); } else { outStep = (_g = this.flow.steps) === null || _g === void 0 ? void 0 : _g.find((s) => s.id === link.to); } if (outStep) { outSteps[link.id] = outStep; } else { this.stepError(plyStep, `No such step: ${link.to} (linked from ${link.id})`); } } } } if (Object.keys(outSteps).length === 0 && step.path !== 'stop') { this.stepError(plyStep, `No outbound link from step ${step.id} matches result: ${plyStep.instance.result}`); } // steps can execute in parallel await Promise.all(Object.keys(outSteps).map((linkId) => this.exec(outSteps[linkId], runtime, values, linkId, runOptions, runNum, subflow))); } getSubflows(type, step) { var _a; const subflows = ((_a = this.flow.subflows) === null || _a === void 0 ? void 0 : _a.filter((sub) => { var _a, _b; if (((_a = sub.attributes) === null || _a === void 0 ? void 0 : _a.when) === type) { const steps = (_b = sub.attributes) === null || _b === void 0 ? void 0 : _b.steps; if (step) { return steps ? (0, minimatch_1.minimatch)(step.name, steps) : false; } else { return !steps; } } })) || []; const flowInstanceId = this.instance.id; return subflows.map((subflow) => { return { subflow, instance: { id: Date.now().toString(16), flowInstanceId, subflowId: subflow.id, runId: Date.now().toString(16), flowPath: this.flow.path, status: 'Pending' } }; }); } async runSubflows(subflows, runtime, values, runOptions, runNum) { var _a; for (const subflow of subflows) { const startStep = (_a = subflow.subflow.steps) === null || _a === void 0 ? void 0 : _a.find((s) => s.path === 'start'); if (!startStep) { throw new Error(`Cannot find start step in subflow: ${subflow.subflow.id}`); } subflow.instance.status = 'In Progress'; subflow.instance.start = new Date(); if (!this.instance.subflowInstances) { this.instance.subflowInstances = []; } this.instance.subflowInstances.push(subflow.instance); this.emit('start', 'subflow', subflow.instance); this.logger.info('Executing subflow', subflow.subflow.name); const resOpts = { level: 0, withExpected: runOptions === null || runOptions === void 0 ? void 0 : runOptions.createExpected, subflow: subflow.subflow.name }; runtime.appendResult(`${subflow.subflow.name}:`, { ...resOpts, level: 0, comment: util.timestamp(subflow.instance.start) }); runtime.appendResult(`id: ${subflow.subflow.id}`, { ...resOpts, level: 1 }); await this.exec(startStep, runtime, values, undefined, runOptions, runNum, subflow); subflow.instance.end = new Date(); const elapsed = subflow.instance.end.getTime() - subflow.instance.start.getTime(); if (this.results.latestBad()) { subflow.instance.status = this.results.latest.status === 'Errored' ? 'Errored' : 'Failed'; runtime.updateResult(subflow.subflow.name, `status: ${subflow.instance.status}`, { ...resOpts, level: 1, comment: `${elapsed} ms` }); this.emit('error', 'subflow', subflow.instance); if (runtime.options.bail) { return; } } else { subflow.instance.status = 'Completed'; runtime.updateResult(subflow.subflow.name, `status: ${subflow.instance.status}`, { ...resOpts, level: 1, comment: `${elapsed} ms` }); this.emit('finish', 'subflow', subflow.instance); } } } stepError(plyStep, message) { plyStep.instance.status = 'Errored'; plyStep.instance.message = message; this.emit('error', 'step', plyStep.instance); this.emit('error', 'flow', this.instance); throw new Error(message); } flowEvent(eventType, elementType, instance) { return { eventType, elementType, flowPath: this.flow.path, flowInstanceId: this.instance.id, instance }; } emit(eventType, elementType, instance) { this._onFlow.emit(this.flowEvent(eventType, elementType, instance)); } } exports.PlyFlow = PlyFlow; //# sourceMappingURL=flow.js.map