UNPKG

oatts-cypress

Version:

An Open API Cypress Test Template Generator

625 lines (540 loc) 23 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>lib/process.js - Documentation</title> <script src="scripts/prettify/prettify.js"></script> <script src="scripts/prettify/lang-css.js"></script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc.css"> </head> <body> <input type="checkbox" id="nav-trigger" class="nav-trigger" /> <label for="nav-trigger" class="navicon-button x"> <div class="navicon"></div> </label> <label for="nav-trigger" class="overlay"></label> <nav> <h2><a href="index.html">Home</a></h2><h3>Namespaces</h3><ul><li><a href="compilation.html">compilation</a><ul class='methods'><li data-type='method'><a href="compilation.html#compile">compile</a></li><li data-type='method'><a href="compilation.html#compileOperationLevel">compileOperationLevel</a></li><li data-type='method'><a href="compilation.html#compilePathLevel">compilePathLevel</a></li><li data-type='method'><a href="compilation.html#compileTransactionLevel">compileTransactionLevel</a></li><li data-type='method'><a href="compilation.html#prepareTemplate">prepareTemplate</a></li></ul></li><li><a href="processing.html">processing</a><ul class='methods'><li data-type='method'><a href="processing.html#determineQueryType">determineQueryType</a></li><li data-type='method'><a href="processing.html#isNumberType">isNumberType</a></li><li data-type='method'><a href="processing.html#lookupCustomQueryValue">lookupCustomQueryValue</a></li><li data-type='method'><a href="processing.html#lookupCustomValue">lookupCustomValue</a></li><li data-type='method'><a href="processing.html#pathify">pathify</a></li><li data-type='method'><a href="processing.html#process">process</a></li><li data-type='method'><a href="processing.html#processHeaders">processHeaders</a></li><li data-type='method'><a href="processing.html#processOperations">processOperations</a></li><li data-type='method'><a href="processing.html#processParams">processParams</a></li><li data-type='method'><a href="processing.html#processPaths">processPaths</a></li><li data-type='method'><a href="processing.html#processResponse">processResponse</a></li><li data-type='method'><a href="processing.html#processTransactions">processTransactions</a></li><li data-type='method'><a href="processing.html#replaceNewlines">replaceNewlines</a></li></ul></li><li><a href="templateHelpers.html">templateHelpers</a><ul class='methods'><li data-type='method'><a href="templateHelpers.html#isNotDefaultStatusCode">isNotDefaultStatusCode</a></li><li data-type='method'><a href="templateHelpers.html#json">json</a></li><li data-type='method'><a href="templateHelpers.html#notEmptyObject">notEmptyObject</a></li></ul></li></ul><h3>Global</h3><ul><li><a href="global.html">generate</a></li><li><a href="global.html">merge2</a></li></ul> </nav> <div id="main"> <h1 class="page-title">lib/process.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>// Copyright 2018 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. 'use strict'; var util = require('util') var helpers = require('./handlebar-helpers') var merge2 = require('./util').merge2; /** * @namespace processing */ module.exports = process /** * ProcessedSpec is the fully traversed spec processed for unit test compilation * @typedef {object} ProcessedSpec * @memberof processing * @property {string} host Hostname to be used in the test requests; derived from the options or spec * @property {string} scheme HTTP schema to be used in the test requests; derived from the spec * @property {string} basePath Base path as defined by the spec * @property {string[]} consumes Global content type 'consumes' collection * @property {ProcessedPath[]} tests Collection of paths processed for test compilation */ /** * Processes the given API spec, creating test data artifacts * @function process * @memberof processing * @instance * @param {object} api API spec object to be processed * @param {object} options options to be used during processing * @return {processing.ProcessedSpec} */ function process(api, options) { var processedSpec = { 'host': (options.host !== undefined ? options.host : (api.host !== undefined ? api.host : 'localhost:5000')), 'scheme': (options.scheme !== undefined ? options.scheme : (api.schemes !== undefined ? api.schemes[0] : 'http')), 'basePath': (api.basePath !== undefined ? api.basePath : ''), 'consumes': (api.consumes !== undefined ? api.consumes : []), 'produces': (api.produces !== undefined ? api.produces : []) } var processedPaths = processPaths(api, processedSpec, options) if (processedPaths.length == 0) { console.log('no paths to process in spec') return null } processedSpec.tests = processedPaths return processedSpec } /** * ProcessedPath is the fully traversed path resource ready for unit test compilation * @typedef {object} ProcessedPath * @memberof processing * @property {string} name name to be used in generation of a file name; based on the path * @property {string} pathLevelDescription the brief description to use at the top level 'describe' block * @property {processing.ProcessedOp[]} operations Collection of operations processed for test compilation */ /** * Processes the paths defined by the api spec, or indicated by the options * @function processPaths * @memberof processing * @instance * @param {object} api parsed OpenAPI spec object * @param {object} topLevel global spec properties * @param {object} options options to use during processing * @return {processing.ProcessedPath[]} */ function processPaths(api, topLevel, options) { var data = [] var targetPaths = []; if (options.paths !== undefined) { options.paths.forEach(function (path, ndx, arr) { targetPaths.push(api.getPath(path)) }) } else { targetPaths = api.getPaths(); } if (options.statusCodes !== undefined) { var tempPaths = []; options.statusCodes.forEach(function (code, ndx, arr) { for (var ndx = 0; ndx &lt; targetPaths.length; ndx++) { var opList = targetPaths[ndx].getOperations(); for (var opNdx = 0; opNdx &lt; opList.length; opNdx++) { if (opList[opNdx].getResponse(code)) { tempPaths.push(targetPaths[ndx]) break; } } } }); targetPaths = tempPaths; } targetPaths.forEach(function (pathObj, ndx, arr) { var ops = processOperations(api, pathObj, topLevel, options) if (ops.length !== 0) { data.push({ 'name': pathObj.path.replace(/\//g, '-').substring(1), 'pathLevelDescription': 'tests for ' + pathObj.path, 'operations': ops }) } }) return data } /** * ProcessedOp is the fully traversed path operation ready for unit test compilation * @typedef {object} ProcessedOp * @memberof processing * @instance * @property {string} operationLevelDescription the brief description to use at the inner 'describe' block * @property {processing.ProcessedTransaction[]} transactions Collection of transactions processed for test compilation */ /** * Processes the operations of the given Path object * @function processOperations * @memberof processing * @instance * @param {object} api parsed OpenAPI spec object * @param {object} parentPath sway Path object being processed * @param {object} topLevel global spec properties * @param {object} options options to use during processing * @return {processing.ProcessedOp[]} */ function processOperations(api, parentPath, topLevel, options) { var ops = [] parentPath.getOperations().forEach(function (op, ndx, arr) { var transactions = processTransactions(api, op, parentPath, topLevel, options) if (transactions.length !== 0) { ops.push({ 'operationLevelDescription': 'tests for ' + op.method, 'transactions': transactions }) } }) return ops } /** * ProcessedTransaction is an HTTP transaction (request/response pair) processed for test compilation * @typedef {object} ProcessedTransaction * @memberof processing * @property {string} testLevelDescription the brief description to use in the test 'it' block * @property {string} scheme copied from the globally defined HTTP schema * @property {string} host copied from the globally defined hostname or as specified in the options * @property {string} path fully qualified path built with the base path; includes path param substitutions * @property {op} op the REST operation to use * @property {object} body the request body params * @property {object} query the request query params * @property {object} formData the request form data params * @property {ExpectedResponse} expected the expected response to use for test validation * @property {boolean} hasSamples flag indicating if the expected response includes sample values * @property {string[]} consumes based on globally defined conumes or operation defined types */ /** * Processes the transactions for a given Path + Operation * @function processTransactions * @memberof processing * @instance * @param {object} api parsed OpenAPI spec object * @param {object} parentOp parent sway Operation object being processed * @param {object} parentPath parent sway Path object being processed * @param {object} topLevel global spec properties * @param {object} options options to use during processing * @return {processing.ProcessedTransaction[]} */ function processTransactions(api, parentOp, parentPath, topLevel, options) { var transactions = [] parentOp.getResponses().forEach(function (res, ndx, arr) { if (options.statusCodes &amp;&amp; options.statusCodes.indexOf(res.statusCode) === -1) { // skip unwanted status codes return; } var params = processParams(res.statusCode, parentOp, parentPath, options) var expected = processResponse(res, parentOp.method, parentPath.path, options) var hasValue = options.samples if (expected.custom) { delete expected.custom hasValue = true } transactions.push({ 'testLevelDescription': util.format('should respond %s for "%s"', res.statusCode, replaceNewlines(res.description)), 'scheme': topLevel.scheme, 'host': topLevel.host, 'path': (topLevel.basePath + params.path), 'op': parentOp.method, 'body': params.body, 'query': params.query, 'formData': params.formData, 'expected': expected, 'hasValue': hasValue, 'headers': merge2(params.headers, processHeaders(res.statusCode, parentOp, parentPath, topLevel, options)) }) }) return transactions } /** * ProcessedParams is an object representing the processed request parameters for unit test compilation * @typedef {object} ProcessedParams * @memberof processing * @property {string} path processed path with path parameter sample substitutions * @property {object} body the request body params * @property {object} query the request query params * @property {object} formData the request form data params */ /** * Processes the parameters of a Path + Operation for use in a test * @function processParams * @memberof processing * @instance * @param {number} code response status code being processed * @param {object} op sway Operation object that's being processed * @param {object} path sway Path object that's being process * @param {object} options options to use during processing * @return {processing.ProcessedParams} */ function processParams(code, op, path, options) { var opCustomQuery = lookupCustomQueryValue(code, op.method, path.path, options); var params = { 'body': {}, 'query': opCustomQuery || {}, 'formData': {}, 'path': path.path, 'headers': {} } op.getParameters().forEach(function (param, ndx, arr) { var customValue = lookupCustomValue(param.name, param.in, code, op.method, path.path, options) // Skip optional parameter with specifically nulled custom value or in-line example if ((customValue === null || param.example === null) &amp;&amp; (!param.required)) { return } // If multiple examples are present, use the first one if (param.examples) { param.example = param.examples[Object.keys(param.examples)[0]].value; } if (param.in === 'body') { params.body = customValue !== undefined ? customValue : param.example !== undefined ? param.example : param.getSample() } else if (param.in === 'query' &amp;&amp; !opCustomQuery) { params.query[param.name] = param.example !== undefined ? param.example : param.getSample(); } else if (param.in === 'formData') { try { params.formData[param.name] = customValue !== undefined ? customValue : param.example !== undefined ? param.example : param.getSample() } catch (e) { // this will be because of formData property of type 'file' params.formData[param.name] = '{fileUpload}' } } else if (param.in === 'path') { params.path = pathify(params.path, param, customValue) } else if (param.in === 'header') { params.headers[param.name] = customValue !== undefined ? customValue : param.example !== undefined ? param.example : param.getSample() } }) path.getParameters().forEach(function (param, ndx, arr) { var customValue = lookupCustomValue(param.name, param.in, code, op.method, path.path, options) // Skip optional parameter with specifically nulled custom value or in-line example if ((customValue === null || param.example === null) &amp;&amp; (!param.required)) { return } params.path = pathify(params.path, param, customValue) }) return params } /** * resolves any custom values available for the given parameter * @function lookupCustomValue * @memberof processing * @instance * @param {string} name parameter name being looked up * @param {string} location where the parameter is in the request/response * @param {number} code response status code being evaluated * @param {string} op the operation method being processed * @param {string} path API path being processed * @param {object} options for use in looking up the custom value * @return {any} */ function lookupCustomValue(name, location, code, op, path, options) { var levels = [path, op, code] var val = undefined; if (options.customValues) { var curr = options.customValues var ndx = 0 do { if (curr[location]) { if (curr[location][name] !== undefined) { val = curr[location][name] } } curr = curr[levels[ndx]] } while (ndx++ &lt; levels.length &amp;&amp; curr) } return val } /** * resolves any custom query values available for the given operation and path * @function lookupCustomQueryValue * @memberof processing * @instance * @param {number} code response status code being evaluated * @param {string} op the operation method being processed * @param {string} path API path being processed * @param {object} options for use in looking up the custom value * @return {any} */ function lookupCustomQueryValue(code, op, path, options) { var levels = [path, op, code]; var val = undefined; if (options.customValues) { var curr = options.customValues; var ndx = 0 do { if (curr.query !== undefined) { val = curr.query } curr = curr[levels[ndx]]; } while (ndx++ &lt; levels.length &amp;&amp; curr) } return val } /** * Determines the request headers for a transaction * @function processHeaders * @memberof processing * @instance * @param {string} responseCode response code being processed * @param {object} op parent operation object * @param {object} path parent path object * @param {object} top global properties * @param {object} options for use in processing headers * @return {object} */ function processHeaders(responseCode, op, path, top, options) { var headers = {}; var levels = [path.path, op.method, responseCode] // handle consumes if (options.consumes) { // a specific content-type was targeted if (op.definitionFullyResolved.consumes) { // this operation overrides global consumes, and does contain the requested content-type if (op.definitionFullyResolved.consumes.indexOf(options.consumes) != -1) { headers['Content-Type'] = options.consumes; } else { // we will just use the first one because this doesn't match headers['Content-Type'] = op.definitionFullyResolved.consumes[0]; } } else if (top.consumes.length > 0 &amp;&amp; top.consumes.indexOf(options.consumes) != -1) { headers['Content-Type'] = options.consumes; } else if (top.consumes.length === 1) { // unless there is a singular different global consumes, just use that one headers['Content-Type'] = top.consumes[0]; } } else if (op.definitionFullyResolved.consumes) { // we will just use the first one if none are specified headers['Content-Type'] = op.definitionFullyResolved.consumes[0]; } else if (top.consumes.length > 0) { headers['Content-Type'] = top.consumes[0]; } // handle produces if (options.produces) { if (op.definitionFullyResolved.produces) { if (op.definitionFullyResolved.produces.indexOf(options.produces) != -1) { headers['Accept'] = options.produces; } else { headers['Accept'] = op.definitionFullyResolved.produces[0]; } } else if (top.produces.length > 0 &amp;&amp; top.produces.indexOf(options.produces) != -1) { headers['Accept'] = options.produces; } else if (top.produces.length === 1) { headers['Accept'] = top.produces[0]; } } else if (op.definitionFullyResolved.produces) { headers['Accept'] = op.definitionFullyResolved.produces[0]; } else if (top.produces.length > 0) { headers['Accept'] = top.produces[0]; } // check custom request values for any desired headers; cascading merge if (options.customValues) { var curr = options.customValues var ndx = 0 do { if (curr['header'] !== undefined) { headers = merge2(curr['header'], headers) } curr = curr[levels[ndx]] } while (ndx++ &lt; levels.length &amp;&amp; curr !== undefined) } return headers } /** * ExpectedResponse is the expected response generated based on the API spec * @typedef {object} ExpectedResponse * @memberof processing * @property {number} statusCode expected response status code * @property {boolean} custom indicates if the value was a custom injection * @property {object} res expected response body, if applicable; can be a generated sample */ /** * Processes a sway Response for use in a test * @function processResponse * @memberof processing * @instance * @param {object} res sway Response object being processed * @param {string} op the operation method being processed * @param {string} path API path being processed * @param {object} options options to use during processing * @return {processing.ExpectedResponse} */ function processResponse(res, op, path, options) { var expected = { 'statusCode': res.statusCode, 'custom': false } if (options.customValues &amp;&amp; options.customValues[path] &amp;&amp; options.customValues[path][op] &amp;&amp; options.customValues[path][op][res.statusCode] &amp;&amp; options.customValues[path][op][res.statusCode].response) { expected['res'] = options.customValues[path][op][res.statusCode].response expected.custom = true; return expected } var res = options.samples ? res.getSample() : res.definitionFullyResolved.schema if (res !== undefined) { expected['res'] = res } return expected } /** * replaces the path paremeter in the given URL path with a sample value * @function pathify * @memberof processing * @instance * @param {string} path URL path to be pathified * @param {object} param sway Parameter object to use in pathify * @param {(string|number)} value a custom value to use in the pathify * @return {string} */ function pathify(path, param, value) { var regex = new RegExp('(\/.*){' + param.name + '}(\/.*)*', 'g') var sample = value !== undefined ? value : param.example !== undefined ? param.example : param.getSample() if (isNumberType(param.definition.type)) { // prevent negative sample values for numbers while (sample &lt; 0) { sample = param.getSample() } } return path.replace(regex, '$1' + sample + '$2').replace(/ /g, ''); } /** * evaluates if the given type is an OpenAPI number type or not * @function isNumberType * @memberof processing * @instance * @param {string} type type to be evaluated * @return {boolean} */ function isNumberType(type) { return (type === 'integer' || type === 'float' || type === 'long' || type === 'double') } /** * Determines the proper type for the Query parameter * @function determineQueryType * @memberof processing * @instance * @param {object} param sway Parameter object to investigate * @return {string} */ function determineQueryType(param) { var val = param.name; // default to the name if all else fails if (param.definitionFullyResolved.type === 'array') { val = param.definitionFullyResolved.items.type + '[]' } else { val = param.definitionFullyResolved.type } return '{' + val + '}' } /** * Used to replace newlines with a space in each response.description * @function replaceNewlines * @memberof processing * @instance * @param {string} str string to replace newlines with spaces * @return {string} */ function replaceNewlines(str) { return str.replace(/\r?\n|\r/g, " ") } </code></pre> </article> </section> </div> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.3</a> on Sat Aug 24 2019 12:31:35 GMT+0100 (Western European Summer Time) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme. </footer> <script>prettyPrint();</script> <script src="scripts/linenumber.js"></script> </body> </html>