UNPKG

pactum

Version:

REST API Testing Tool for all levels in a Test Pyramid

333 lines (307 loc) 10.5 kB
const phinx = require('phinx'); const fs = require('fs'); const helper = require('../helpers/helper'); const log = require('../plugins/logger'); const rlc = require('../helpers/reporter.lifeCycle'); const requestProcessor = require('../helpers/requestProcessor'); const th = require('../helpers/toss.helper'); const utils = require('../helpers/utils'); const mock = require('../exports/mock'); const request = require('../exports/request'); const config = require('../config'); const hr = require('../helpers/handler.runner'); const { pactumEvents, EVENT_TYPES } = require('../exports/events'); class Tosser { constructor(spec) { this.spec = spec; this.request = spec._request; this.state = spec._state; this.expect = spec._expect; this.sleep = spec._sleep; this.interactions = spec.interactions; this.previousLogLevel = spec.previousLogLevel; this.response = {}; this.mockIds = []; } async toss() { try { this.spec.start = Date.now().toString(); this.request = requestProcessor.process(this.request, this.spec._data_maps); await this.setState(); await this.addInteractionsToServer(); // get interactions to check for background property await this.getInteractionsFromServer(); await this.setResponse(); this.inspect(); if (!hasBackgroundInteractions(this.interactions)) { await this.staticWait(); } await this.getInteractionsFromServer(); this.saveDataInFileSystem(); this.recordData(); this.storeSpecData(); await this.validate(); if (hasBackgroundInteractions(this.interactions) || (this.spec._wait && typeof this.spec._wait.arg1 === 'string')) { await this.dynamicWait(); await this.getInteractionsFromServer(); this.validateBackgroundInteractions(); } return th.getOutput(this.spec, this.spec._returns); } finally { this.sleep > 0 && await helper.sleep(this.sleep); await this.removeInteractionsFromServer(); this.setPreviousLogLevel(); } } setState() { return this.state.set(this.spec); } async addInteractionsToServer() { let mockIdPromises = []; for (let i = 0; i < this.interactions.length; i++) { const { interaction, data } = this.interactions[i]; const ids = mock.addInteraction(interaction, data); if (Array.isArray(ids)) { mockIdPromises = mockIdPromises.concat(ids); } else { mockIdPromises.push(ids); } } this.mockIds = this.mockIds.concat(await Promise.all(mockIdPromises)); } async setResponse() { this.response = await getResponse(this); this.spec._response = this.response; await th.runResponseHandler(this.spec); const options = this.request.retryOptions; if (options) { const count = typeof options.count === 'number' ? options.count : config.request.retry.count; const delay = typeof options.delay === 'number' ? options.delay : config.request.retry.delay; const strategy = options.strategy; const status = options.status; for (let i = 0; i < count; i++) { let err = null; let shouldRetry = false; const ctx = { req: this.request, res: this.response }; if (typeof strategy === 'function') { shouldRetry = !strategy(ctx); } else if (typeof strategy === 'string') { shouldRetry = !hr.retry(strategy, ctx); } else if (status) { if (Array.isArray(status)) { shouldRetry = status.includes(this.response.statusCode); } else { shouldRetry = status === ctx.res.statusCode; } } else { try { await this.validateResponse(); } catch (error) { err = error; shouldRetry = true; } } if (shouldRetry) { const scale = delay === 1000 ? 'second' : 'seconds'; if (err) { log.info(`Request retry initiated, waiting ${delay / 1000} ${scale} for attempt ${i + 1} of ${count} due to error: ${err.message}`); } else { log.info(`Request retry initiated, waiting ${delay / 1000} ${scale} for attempt ${i + 1} of ${count}`); } await helper.sleep(delay); this.response = await getResponse(this); this.spec._response = this.response; await th.runResponseHandler(this.spec); } else { break; } } } delete this.spec._request.data; this.spec._response = this.response; } inspect() { if (this.spec._inspect) { log.warn('Inspecting Request & Response'); if (typeof this.spec._inspect === 'boolean') { this.printRequestAndResponse(); } else { for (let i = 0; i < this.spec._inspect.length; i++) { const inspect_path = this.spec._inspect[i]; log.warn(inspect_path, th.getPathValueFromRequestResponse(inspect_path, this.spec._request, this.spec._response)); } } } } async staticWait() { const _wait = this.spec._wait; if (_wait && _wait.arg1) { if (typeof _wait.arg1 === 'number') { await helper.sleep(_wait.arg1); } else { await _wait.arg1; } } } async dynamicWait() { const _wait = this.spec._wait; if (_wait) { if (typeof _wait.arg1 === 'undefined' || typeof _wait.arg1 === 'number') { let duration = config.response.wait.duration; let polling = config.response.wait.polling; let waited = 0; if (typeof _wait.arg1 === 'number') { duration = _wait.arg1; polling = _wait.arg2 && _wait.arg2 > 0 ? _wait.arg2 : 100; } while (waited < duration) { waited = waited + polling; await this.getInteractionsFromServer(); try { this.validateBackgroundInteractions(); break; } catch (error) { if (waited > duration) throw error; } await helper.sleep(polling); } } else { await hr.wait(_wait.arg1, { req: this.request, res: this.response, data: _wait.arg2, rootData: this.spec._specHandlerData }); } } } setPreviousLogLevel() { if (this.previousLogLevel) { log.setLevel(this.previousLogLevel); } } saveDataInFileSystem() { if (this.spec._save && this.response.buffer) { fs.writeFileSync(this.spec._save, this.response.buffer); } } recordData() { const defaultRecorders = request.getDefaultRecorders(); th.recordSpecData(this.spec, defaultRecorders); th.recordSpecData(this.spec, this.spec._recorders); } async getInteractionsFromServer() { if (this.mockIds.length > 0) { this.interactions.length = 0; this.interactions = this.interactions.concat(await mock.getInteraction(this.mockIds)); this.spec.interactions = this.interactions; } } async removeInteractionsFromServer() { if (this.mockIds.length > 0) { await mock.removeInteraction(this.mockIds); } } async validate() { this.validateError(); try { this.validateNonBackgroundInteractions(); await this.validateResponse(); this.spec.status = 'PASSED'; this.storeSpecData(); this.runReport(); } catch (error) { this.spec.status = 'FAILED'; this.storeSpecData(); this.spec.failure = error.toString(); this.runReport(); this.printRequestAndResponse(); pactumEvents.emit(EVENT_TYPES.AFTER_RESPONSE_ERROR, { request: this.request, response: this.response, error }); throw error; } } validateError() { if (this.response instanceof Error && this.expect.errors.length === 0) { this.spec.status = 'ERROR'; this.spec.failure = this.response.toString(); this.runReport(); this.printRequestAndResponse(); this.expect.fail(this.response); } } validateNonBackgroundInteractions() { const nonBgInteractions = this.interactions.filter(interaction => !interaction.background); this.expect.validateInteractions(nonBgInteractions); } validateBackgroundInteractions() { const bgInteractions = this.interactions.filter(interaction => interaction.background); this.expect.validateInteractions(bgInteractions); } async validateResponse() { await this.expect.validate(this.request, this.response); } runReport() { if (config.reporter.autoRun && this.expect.errors.length === 0) { rlc.afterSpecReport(this.spec); } } printRequestAndResponse() { if (this.spec._inspect !== false) { utils.printReqAndRes(this.request, this.response); } } storeSpecData() { const stores = []; switch (this.spec.status) { case 'PASSED': for (const store of this.spec._stores) { if (store && store.options && store.options.status === 'PASSED') { stores.push(store); } } break; case 'FAILED': for (const store of this.spec._stores) { if (store && store.options && store.options.status === 'FAILED') { stores.push(store); } } break; default: for (const store of this.spec._stores) { if (store && (!store.options || !store.options.status)) { stores.push(store); } } break; } if (stores.length > 0) { th.storeSpecData(this.spec, stores); } } } async function getResponse(tosser) { const { request, expect } = tosser; let res = {}; const requestStartTime = Date.now(); try { pactumEvents.emit(EVENT_TYPES.BEFORE_REQUEST, { request }); log.debug(`${request.method} ${request.url}`); res = await phinx(request); res.buffer = res.body; res.text = helper.bufferToString(res.body) || ''; res.body = helper.bufferToString(res.body); res.json = helper.getJson(res.body); if (helper.isContentJson(res)) { res.body = res.json; } } catch (error) { if (expect.errors.length === 0) { log.error('Error performing request', error); } res = error; } finally { res.responseTime = Date.now() - requestStartTime; pactumEvents.emit(EVENT_TYPES.AFTER_RESPONSE, { request, response: res }); } return res; } function hasBackgroundInteractions(interactions) { return interactions.some(interaction => interaction.background); } module.exports = Tosser;