UNPKG

dredd

Version:

HTTP API Testing Framework

293 lines (237 loc) 9.65 kB
const clone = require('clone'); const generateUuid = require('uuid/v4'); const os = require('os'); const request = require('request'); const logger = require('../logger'); const reporterOutputLogger = require('./reporterOutputLogger'); const packageData = require('../../package.json'); const CONNECTION_ERRORS = [ 'ECONNRESET', 'ENOTFOUND', 'ESOCKETTIMEDOUT', 'ETIMEDOUT', 'ECONNREFUSED', 'EHOSTUNREACH', 'EPIPE', ]; function ApiaryReporter(emitter, stats, tests, config, runner) { this.type = 'apiary'; this.stats = stats; this.tests = tests; this.uuid = null; this.startedAt = null; this.endedAt = null; this.remoteId = null; this.config = config; this.runner = runner; this.reportUrl = null; this.errors = []; this.serverError = false; this.configuration = { apiUrl: (this._get('apiaryApiUrl', 'APIARY_API_URL', 'https://api.apiary.io')).replace(/\/$/, ''), apiToken: this._get('apiaryApiKey', 'APIARY_API_KEY', null), apiSuite: this._get('apiaryApiName', 'APIARY_API_NAME', null), }; this.configureEmitter(emitter); logger.debug(`Using '${this.type}' reporter.`); if (!this.configuration.apiToken && !this.configuration.apiSuite) { logger.warn(` Apiary API Key or API Project Subdomain were not provided. Configure Dredd to be able to save test reports alongside your Apiary API project: https://dredd.org/en/latest/how-to-guides/#using-apiary-reporter-and-apiary-tests `); } if (!this.configuration.apiSuite) { this.configuration.apiSuite = 'public'; } } // THIS IS HIIIIGHWAY TO HELL, HIIIIIGHWAY TO HELL. Everything should have one single interface ApiaryReporter.prototype._get = function _get(customProperty, envProperty, defaultVal) { let returnVal = defaultVal; // This will be deprecated if (this.config.custom && this.config.custom[customProperty]) { returnVal = this.config.custom[customProperty]; // This will be the ONLY supported way how to configure this reporter } else if (this.config.options && this.config.options.custom && this.config.options.custom[customProperty]) { returnVal = this.config.options.custom[customProperty]; // This will be deprecated } else if (this.config.custom && this.config.custom.apiaryReporterEnv && this.config.custom.apiaryReporterEnv[customProperty]) { returnVal = this.config.custom.apiaryReporterEnv[customProperty]; // This will be deprecated } else if (this.config.custom && this.config.custom.apiaryReporterEnv && this.config.custom.apiaryReporterEnv[envProperty]) { returnVal = this.config.custom.apiaryReporterEnv[envProperty]; // This will be supported for backward compatibility, but can be removed in future. } else if (process.env[envProperty]) { returnVal = process.env[envProperty]; } return returnVal; }; ApiaryReporter.prototype._getKeys = function _getKeys() { let returnKeys = []; returnKeys = returnKeys.concat(Object.keys((this.config.custom && this.config.custom.apiaryReporterEnv) || {})); return returnKeys.concat(Object.keys(process.env)); }; ApiaryReporter.prototype.configureEmitter = function configureEmitter(emitter) { emitter.on('start', (blueprintsData, callback) => { if (this.serverError === true) { return callback(); } this.uuid = generateUuid(); this.startedAt = Math.round(new Date().getTime() / 1000); // Cycle through all keys from // - config.custom.apiaryReporterEnv // - process.env keys const ciVars = /^(TRAVIS|CIRCLE|CI|DRONE|BUILD_ID)/; const envVarNames = this._getKeys(); const ciEnvVars = {}; for (const envVarName of envVarNames) { if (envVarName.match(ciVars)) { ciEnvVars[envVarName] = this._get(envVarName, envVarName); } } // Transform blueprints data to array const blueprints = []; for (const filename of Object.keys(blueprintsData || {})) { blueprints.push(blueprintsData[filename]); } const data = { blueprints, endpoint: this.config.server, agent: this._get('dreddAgent', 'DREDD_AGENT') || this._get('user', 'USER'), agentRunUuid: this.uuid, hostname: this._get('dreddHostname', 'DREDD_HOSTNAME') || os.hostname(), startedAt: this.startedAt, public: true, status: 'running', agentEnvironment: ciEnvVars, }; if (this.configuration.apiToken && this.configuration.apiSuite) { data.public = false; } const path = `/apis/${this.configuration.apiSuite}/tests/runs`; this._performRequestAsync(path, 'POST', data, (error, response, parsedBody) => { if (error) { callback(error); } else { this.remoteId = parsedBody._id; if (parsedBody.reportUrl) { this.reportUrl = parsedBody.reportUrl; } callback(); } }); }); emitter.on('test pass', this._createStep.bind(this)); emitter.on('test fail', this._createStep.bind(this)); emitter.on('test skip', this._createStep.bind(this)); emitter.on('test error', (error, test, callback) => { if (this.serverError === true) { return callback(); } const data = this._transformTestToReporter(test); if (!data.resultData.result) { data.resultData.result = {}; } if (!data.resultData.result.general) { data.resultData.result.general = []; } if (Array.from(CONNECTION_ERRORS).includes(error.code)) { data.resultData.result.general.push({ severity: 'error', message: 'Error connecting to server under test!', }); } else { data.resultData.result.general.push({ severity: 'error', message: 'Unhandled error occured when executing the transaction.', }); } const path = `/apis/${this.configuration.apiSuite}/tests/steps?testRunId=${this.remoteId}`; this._performRequestAsync(path, 'POST', data, (err) => { if (err) { return callback(err); } callback(); }); }); emitter.on('end', (callback) => { if (this.serverError === true) { return callback(); } const data = { endedAt: Math.round(new Date().getTime() / 1000), result: this.stats, status: (this.stats.failures > 0 || this.stats.errors > 0) ? 'failed' : 'passed', logs: (this.runner && this.runner.logs && this.runner.logs.length) ? this.runner.logs : undefined, }; const path = `/apis/${this.configuration.apiSuite}/tests/run/${this.remoteId}`; this._performRequestAsync(path, 'PATCH', data, (error) => { if (error) { return callback(error); } const reportUrl = this.reportUrl || `https://app.apiary.io/${this.configuration.apiSuite}/tests/run/${this.remoteId}`; reporterOutputLogger.complete(`See results in Apiary at: ${reportUrl}`); callback(); }); }); }; ApiaryReporter.prototype._createStep = function _createStep(test, callback) { if (this.serverError === true) { return callback(); } const data = this._transformTestToReporter(test); const path = `/apis/${this.configuration.apiSuite}/tests/steps?testRunId=${this.remoteId}`; this._performRequestAsync(path, 'POST', data, (error) => { if (error) { return callback(error); } callback(); }); }; ApiaryReporter.prototype._performRequestAsync = function _performRequestAsync(path, method, reqBody, callback) { const handleRequest = (err, res, resBody) => { let parsedBody; if (err) { this.serverError = true; logger.debug('Requesting Apiary API errored:', `${err}` || err.code); if (Array.from(CONNECTION_ERRORS).includes(err.code)) { return callback(new Error('Apiary reporter could not connect to Apiary API')); } return callback(err); } logger.debug('Handling HTTP response from Apiary API'); try { parsedBody = JSON.parse(resBody); } catch (error) { this.serverError = true; err = new Error(` Apiary reporter failed to parse Apiary API response body: ${error.message}\n${resBody} `); return callback(err); } const info = { headers: res.headers, statusCode: res.statusCode, body: parsedBody }; logger.debug('Apiary reporter response:', JSON.stringify(info, null, 2)); callback(null, res, parsedBody); }; const body = reqBody ? JSON.stringify(reqBody) : ''; const system = `${os.type()} ${os.release()}; ${os.arch()}`; const headers = { 'User-Agent': `Dredd Apiary Reporter/${packageData.version} (${system})`, 'Content-Type': 'application/json', }; const options = clone(this.config.http || {}); options.uri = this.configuration.apiUrl + path; options.method = method; options.headers = headers; options.body = body; if (this.configuration.apiToken) { options.headers.Authentication = `Token ${this.configuration.apiToken}`; } try { const protocol = options.uri.split(':')[0].toUpperCase(); logger.debug(` About to perform an ${protocol} request from Apiary reporter to Apiary API: ${options.method} ${options.uri} \ (${body ? 'with' : 'without'} body) `); logger.debug('Request details:', JSON.stringify({ options, body }, null, 2)); return request(options, handleRequest); } catch (error) { this.serverError = true; logger.debug('Requesting Apiary API errored:', error); return callback(error); } }; ApiaryReporter.prototype._transformTestToReporter = function _transformTestToReporter(test) { return { testRunId: this.remoteId, origin: test.origin, duration: test.duration, result: test.status, startedAt: test.startedAt, resultData: { request: test.request, realResponse: test.actual, expectedResponse: test.expected, result: test.results, }, }; }; module.exports = ApiaryReporter;