UNPKG

@ply-ct/ply

Version:

REST API Automated Testing

369 lines 17.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.FlowLoader = exports.FlowSuite = void 0; const flow_1 = require("./flow"); const log_1 = require("./log"); const logger_1 = require("./logger"); const retrieval_1 = require("./retrieval"); const runtime_1 = require("./runtime"); const result_1 = require("./result"); const suite_1 = require("./suite"); const step_1 = require("./step"); const skip_1 = require("./skip"); const util = __importStar(require("./util")); const yaml = __importStar(require("./yaml")); const ply_1 = require("./ply"); /** * Suite representing a ply flow. */ class FlowSuite extends suite_1.Suite { /** * @param plyFlow PlyFlow * @param path relative path from tests location (forward slashes) * @param runtime info * @param logger * @param start zero-based start line * @param end zero-based end line */ constructor(plyFlow, path, runtime, logger, start = 0, end) { super(plyFlow.name, 'flow', path, runtime, logger, start, end); this.plyFlow = plyFlow; this.path = path; this.runtime = runtime; this.logger = logger; this.start = start; this.end = end; } /** * Override to execute flow itself if all steps are specified * @param steps */ async runTests(steps, values, runOptions, runNum) { if (runOptions && Object.keys(runOptions).length > 0) { this.log.debug('RunOptions', runOptions); } if (runOptions === null || runOptions === void 0 ? void 0 : runOptions.requireTsNode) { require('ts-node/register'); } this.emitSuiteStarted(); // runtime values are a deep copy of passed values const runValues = JSON.parse(JSON.stringify(values)); this.runtime.responseMassagers = undefined; let results; if (this.isFlowSpec(steps)) { results = [await this.runFlow(runValues, runOptions, runNum)]; } else { results = await this.runSteps(steps, runValues, runOptions); } this.emitSuiteFinished(); return results; } getStep(stepId) { const step = this.all().find((step) => step.step.id === stepId); if (!step) throw new Error(`Step not found: ${stepId}`); return step; } async runFlow(values, runOptions, runNum) { var _a, _b; if ((_a = this.runtime.options) === null || _a === void 0 ? void 0 : _a.parallel) { this.plyFlow = this.plyFlow.clone(); } this.plyFlow.onFlow((flowEvent) => { var _a, _b, _c; if (flowEvent.eventType === 'exec') { // emit test event (not for request -- emitted in requestSuite) const stepInstance = flowEvent.instance; const step = this.getStep(stepInstance.stepId); if (step.step.path !== 'request') { this.emitTest(step); } } else { // exec not applicable for ply subscribers (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit('flow', flowEvent); if (flowEvent.elementType === 'step' && (flowEvent.eventType === 'finish' || flowEvent.eventType === 'error')) { const stepInstance = flowEvent.instance; const step = this.getStep(stepInstance.stepId); if (step.step.path !== 'request') { if (flowEvent.eventType === 'error') { (_b = this.emitter) === null || _b === void 0 ? void 0 : _b.emit('outcome', { plyee: new ply_1.Plyee(this.runtime.options.testsLocation + '/' + this.path, step).path, outcome: { status: 'Errored', message: stepInstance.message || '' } }); } else if (flowEvent.eventType === 'finish') { (_c = this.emitter) === null || _c === void 0 ? void 0 : _c.emit('outcome', { plyee: new ply_1.Plyee(this.runtime.options.testsLocation + '/' + this.path, step).path, outcome: { status: (runOptions === null || runOptions === void 0 ? void 0 : runOptions.submit) ? 'Submitted' : 'Passed', message: '' } }); } } } } }); this.plyFlow.requestSuite.emitter = this.emitter; const start = Date.now(); const res = await this.plyFlow.run(this.runtime, values, runOptions, runNum); const flowResult = { flow: this.path, ...res, start, end: Date.now() }; if ((_b = this.plyFlow.flow.attributes) === null || _b === void 0 ? void 0 : _b.return) { // don't evaluate result values if not used const retVals = this.plyFlow.getReturnValues({ ...values, ...this.plyFlow.results.values }, runOptions === null || runOptions === void 0 ? void 0 : runOptions.trusted); if (retVals) { this.log.debug(`${this.name} return values`, retVals); flowResult.return = retVals; } } return flowResult; } /** * Run steps independently */ async runSteps(steps, values, runOptions) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; // flow values supersede file-based const flowValues = this.plyFlow.getFlowValues(values, runOptions); for (const flowValKey of Object.keys(flowValues)) { values[flowValKey] = flowValues[flowValKey]; } const requestSuite = new suite_1.Suite(this.plyFlow.name, 'request', this.path, this.runtime, this.logger, 0, 0); requestSuite.callingFlowPath = this.plyFlow.flow.path; requestSuite.emitter = this.emitter; this.runtime.results.actual.clear(); const expectedExists = await this.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: ${this.runtime.results.expected}`); this.runtime.results.expected.write(''); runOptions.createExpected = true; } const results = []; // emit start event for synthetic flow (_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit('flow', this.plyFlow.flowEvent('start', 'flow', this.plyFlow.instance)); this.plyFlow.instance.stepInstances = []; for (const step of steps) { this.emitTest(step); let subflow; const dot = step.name.indexOf('.'); if (dot > 0) { const subflowId = step.name.substring(0, dot); subflow = (_b = this.plyFlow.flow.subflows) === null || _b === void 0 ? void 0 : _b.find((sub) => sub.id === subflowId); } const insts = (_c = this.plyFlow.instance.stepInstances) === null || _c === void 0 ? void 0 : _c.filter((stepInst) => { return stepInst.stepId === step.step.id; }).length; const plyStep = new step_1.PlyStep(step.step, requestSuite, this.logger, this.plyFlow.flow, this.plyFlow.instance, subflow); if (insts) { let maxLoops = this.runtime.options.maxLoops || 10; if ((_d = this.plyFlow.flow.attributes) === null || _d === void 0 ? void 0 : _d.maxLoops) { maxLoops = parseInt(this.plyFlow.flow.attributes.maxLoops); } if ((_e = step.step.attributes) === null || _e === void 0 ? void 0 : _e.maxLoops) { maxLoops = parseInt(step.step.attributes.maxLoops); } if (isNaN(maxLoops)) { this.plyFlow.stepError(plyStep, `Invalid maxLoops: ${maxLoops}`); } else if (insts + 1 > maxLoops) { this.plyFlow.stepError(plyStep, `Max loops (${maxLoops} reached for step: ${step.step.id}`); } } (_f = this.emitter) === null || _f === void 0 ? void 0 : _f.emit('flow', this.plyFlow.flowEvent('start', 'step', plyStep.instance)); const result = await plyStep.run(this.runtime, values, runOptions, 0, insts); if (result.status === 'Failed' || result.status === 'Errored') { (_g = this.emitter) === null || _g === void 0 ? void 0 : _g.emit('flow', this.plyFlow.flowEvent('error', 'step', plyStep.instance)); } else { (_h = this.emitter) === null || _h === void 0 ? void 0 : _h.emit('flow', this.plyFlow.flowEvent('finish', 'step', plyStep.instance)); } if (step.step.path !== 'request') { super.logOutcome(step, { status: result.status, message: result.message, start: (_j = plyStep.instance.start) === null || _j === void 0 ? void 0 : _j.getTime(), diffs: result.diffs }, 0, 'Step'); } results.push(result); if (plyStep.instance) this.plyFlow.instance.stepInstances.push(plyStep.instance); } // stop event for synthetic flow const evt = results.reduce((overall, res) => res.status === 'Failed' || res.status === 'Errored' ? 'error' : overall, 'finish'); (_k = this.emitter) === null || _k === void 0 ? void 0 : _k.emit('flow', this.plyFlow.flowEvent(evt, 'flow', this.plyFlow.instance)); return results; } /** * True if steps array is identical to flow steps. */ isFlowSpec(steps) { if (steps.length !== this.size()) { return false; } const flowStepNames = Object.keys(this.tests); for (let i = 0; i < steps.length; i++) { if (steps[i].name !== flowStepNames[i]) { return false; } } return true; } /** * Returns all reachable unique steps in this.plyFlow. * These are the tests in this FlowSuite. */ getSteps() { var _a, _b, _c, _d, _e; const steps = []; const addSteps = (startStep, subflow) => { var _a, _b; const already = steps.find((step) => step.step.id === startStep.id); if (!already) { steps.push({ step: startStep, subflow }); if (startStep.links) { for (const link of startStep.links) { let outStep; if (subflow) { outStep = (_a = subflow.steps) === null || _a === void 0 ? void 0 : _a.find((s) => s.id === link.to); } else { outStep = (_b = this.plyFlow.flow.steps) === null || _b === void 0 ? void 0 : _b.find((s) => s.id === link.to); } if (outStep) { addSteps(outStep, subflow); } } } } }; (_b = (_a = this.plyFlow.flow.subflows) === null || _a === void 0 ? void 0 : _a.filter((sub) => { var _a; return ((_a = sub.attributes) === null || _a === void 0 ? void 0 : _a.when) === 'Before'; })) === null || _b === void 0 ? void 0 : _b.forEach((before) => { var _a; (_a = before.steps) === null || _a === void 0 ? void 0 : _a.forEach((step) => addSteps(step, before)); }); (_c = this.plyFlow.flow.steps) === null || _c === void 0 ? void 0 : _c.forEach((step) => addSteps(step)); (_e = (_d = this.plyFlow.flow.subflows) === null || _d === void 0 ? void 0 : _d.filter((sub) => { var _a; return ((_a = sub.attributes) === null || _a === void 0 ? void 0 : _a.when) === 'After'; })) === null || _e === void 0 ? void 0 : _e.forEach((after) => { var _a; (_a = after.steps) === null || _a === void 0 ? void 0 : _a.forEach((step) => addSteps(step, after)); }); return steps.map((step) => { return { name: step.subflow ? `${step.subflow.id}-${step.step.id}` : step.step.id, type: 'flow', step: step.step, ...(step.subflow && { subflow: step.subflow }) }; }); } } exports.FlowSuite = FlowSuite; class FlowLoader { constructor(locations, options, logger) { this.locations = locations; this.options = options; this.logger = logger; if (options.skip) { this.skip = new skip_1.Skip(options.skip); } } async load() { const retrievals = this.locations.map((loc) => new retrieval_1.Retrieval(loc)); // load flow files in parallel const promises = retrievals.map((retr) => this.loadSuite(retr)); const suites = await Promise.all(promises); suites.sort((s1, s2) => s1.name.localeCompare(s2.name)); return suites; } async loadSuite(retrieval) { const contents = await retrieval.read(); if (typeof contents === 'undefined') { throw new Error('Cannot retrieve: ' + retrieval.location.absolute); } const resultPaths = await result_1.ResultPaths.create(this.options, retrieval); resultPaths.isFlowResult = true; return this.buildSuite(retrieval, contents, resultPaths); } buildSuite(retrieval, contents, resultPaths) { var _a; const runtime = new runtime_1.Runtime(this.options, retrieval, resultPaths); const logger = this.logger || new logger_1.Logger({ level: this.options.verbose ? log_1.LogLevel.debug : this.options.quiet ? log_1.LogLevel.error : log_1.LogLevel.info, prettyIndent: this.options.prettyIndent }, runtime.results.log); // request suite comprising all requests configured in steps const requestSuite = new suite_1.Suite(retrieval.location.base, 'request', retrieval.location.relativeTo(this.options.testsLocation), runtime, logger, 0, 0); const flowbeeFlow = FlowLoader.parse(contents, retrieval.location.path); requestSuite.callingFlowPath = flowbeeFlow.path; const plyFlow = new flow_1.PlyFlow(flowbeeFlow, requestSuite, logger); const suite = new FlowSuite(plyFlow, retrieval.location.relativeTo(this.options.testsLocation), runtime, logger, 0, util.lines(contents).length - 1); for (const step of suite.getSteps()) { suite.add(step); } // mark if skipped if ((_a = this.skip) === null || _a === void 0 ? void 0 : _a.isSkipped(suite.path)) { suite.skip = true; } return suite; } /** * Parse a flowbee flow from text (reproduced from flowbee.FlowDiagram) * @param text json or yaml * @param file file name */ static parse(text, file) { let flow; if (text.startsWith('{')) { try { flow = JSON.parse(text); } catch (err) { throw new Error(`Failed to parse ${file}: ${err}`); } } else { flow = yaml.load(file, text); } if (!flow) throw new Error(`Unable to load from empty: ${file}`); flow.type = 'flow'; flow.path = file.replace(/\\/g, '/'); return flow; } } exports.FlowLoader = FlowLoader; //# sourceMappingURL=flows.js.map