diff --git a/README.md b/README.md index 4c129b6..0ae5714 100644 --- a/README.md +++ b/README.md @@ -36,18 +36,63 @@ stt.testGen(swagger, config); `swagger-test-templates` module exports a function with following arguments and return values: -#### Arguments +### Arguments * **`assertionFormat`** *required*: One of `should`, `expect` or `assert`. Choose which assertion method should be used in output test code. * **`testModule`** *required*: One of `supertest` or `request`. Choose between direct API calls (`request`) vs. programatic access to your API (`supertest`). -* **`pathNames`** *required*: List of path names available in your Swagger API spec used to generate tests for. Empty array leads to **all paths**. +* **`pathName`** *required*: List of path names available in your Swagger API spec used to generate tests for. Empty array leads to **all paths**. * **`statusCodes`** *optional* Array with status codes to generate tests for. Useful for generating only happy-flow tests. Excluding this param will generate tests for all responses. * **`loadTest`** *optional*: List of objects info in your Swagger API spec used to generate stress tests. If specify, pathName & operation are **required**. Optional fields requests defaults to `1000`, concurrent defaults to `100`. -* **`maxLen`** *optional*: Maximum line length. Defaults to `80`. +* **`maxLen`** *optional*: Maximum line length. If set to `-1`, descriptions will not be truncated. Defaults to `80`. * **`pathParams`** *optional*: Object containing the values of a specific path parameters. * **`templatesPath`** *optional* String indicating a custom handlebars-template path for generating the tests. Note: copy all the templates to your custom directory, this is a 'all-or-nothing' path +* **`requestData`** *optional* Array containing data to send with the request See section on requestData for more details -#### Return value +### Return value An array in which each string is content of a test file and the file name. Use this information to write those files to disk. +## Sending requestData +Bases on your schema there a a few modules out there that allow you to generate mock request. +You can send this mock data along with the tests generated by this module by filling the `requestData` property of the module. + The mock data needs to have with the following structure: + +```javascript +{ + '/endpoint': { + operation: { + 'responseCode': [{ body: {}, description:'some description of the data'] + } + } + } + +``` + +so, for example this could be: + +```javascript +{ + '/pet': { + post: { + '200': [{ + body: { + id: 1, + otherProperty: 'some property that is a string' + }, + description: 'the description for this data' + }] + }, + get: { + '200': [ { + guid: 'some_string_to_place_in_path', + anotherPathParam: 100, + description: 'valid path or query parameters' + }] + } + } + } +``` + +Note: for get-requests matching data will be transferred to the pathParams. So setting config.pathParams directly will have the same effect (see above). + +Every mockData item in the `responseCode` array will be used to generate a test. The description will be added to the "it" function for reference. ##License [MIT](/LICENSE) diff --git a/index.js b/index.js index 6689b4f..3a173a8 100644 --- a/index.js +++ b/index.js @@ -25,16 +25,14 @@ 'use strict'; var TYPE_JSON = 'application/json'; - var handlebars = require('handlebars'); var sanitize = require('sanitize-filename'); var fs = require('fs'); -var read = require('fs').readFileSync; var _ = require('lodash'); -var strObj = require('string'); -var join = require('path').join; +var url = require('url'); +var path = require('path'); var deref = require('json-schema-deref-sync'); -var len; +var helpers = require('./lib/helpers.js'); /** * To check if it is an empty array or undefined @@ -50,22 +48,24 @@ function isEmpty(val) { * Populate property of the swagger project * @private * @param {json} swagger swagger file containing API - * @param {string} path API path to generate tests for + * @param {string} apiPath API path to generate tests for * @param {string} operation operation of the path to generate tests for * @param {string} response response type of operation of current path * @param {json} config configuration for testGen * @param {info} info for cascading properties * @returns {json} return all the properties information */ -function getData(swagger, path, operation, response, config, info) { - var childProperty = swagger.paths[path]; - var grandProperty = swagger.paths[path][operation]; +function getData(swagger, apiPath, operation, response, config, info) { + var childProperty = swagger.paths[apiPath]; + var grandProperty = swagger.paths[apiPath][operation]; var securityType; + + var responseDescription = (swagger.paths[apiPath][operation].responses[response]) ? + swagger.paths[apiPath][operation].responses[response].description : ''; var data = { // request payload responseCode: response, default: response === 'default' ? 'default' : null, - description: (response + ' ' + - swagger.paths[path][operation].responses[response].description), + description: (response + ' ' + responseDescription), assertion: config.assertionFormat, noSchema: true, bodyParameters: [], @@ -89,19 +89,19 @@ function getData(swagger, path, operation, response, config, info) { data.pathParams = config.pathParams; } + // used for checking requestData table + var requestPath = (swagger.basePath) ? path.join(swagger.basePath, apiPath) : apiPath; + + // cope with loadTest info if (info.loadTest != null) { _.forEach(info.loadTest, function(loadTestParam) { - if (loadTestParam.pathName === path - && loadTestParam.operation === operation) { - data.loadName = path.replace(/\//g, '_') + - '_' + operation + '_load_test'; + if (loadTestParam.pathName === apiPath && loadTestParam.operation === operation) { + data.loadName = apiPath.replace(/\//g, '_') + '_' + operation + '_load_test'; info.importArete = true; data.isLoadTest = true; - data.requests = loadTestParam.load.requests !== undefined ? - loadTestParam.load.requests : 1000; - data.concurrent = loadTestParam.load.concurrent !== undefined ? - loadTestParam.load.concurrent : 100; + data.requests = loadTestParam.load.requests !== undefined ? loadTestParam.load.requests : 1000; + data.concurrent = loadTestParam.load.concurrent !== undefined ? loadTestParam.load.concurrent : 100; } }); } @@ -182,8 +182,7 @@ function getData(swagger, path, operation, response, config, info) { }); } - if (grandProperty.responses[response] - .hasOwnProperty('schema')) { + if (grandProperty.responses[response].hasOwnProperty('schema')) { data.noSchema = false; data.schema = grandProperty.responses[response].schema; data.schema = JSON.stringify(data.schema, null, 2); @@ -191,38 +190,62 @@ function getData(swagger, path, operation, response, config, info) { // request url case if (config.testModule === 'request') { - data.path = (swagger.schemes !== undefined ? swagger.schemes[0] : 'http') - + '://' + (swagger.host !== undefined ? swagger.host : 'localhost:10010'); + data.path = url.format({ + protocol: swagger.schemes !== undefined ? swagger.schemes[0] : 'http', + host: swagger.host !== undefined ? swagger.host : 'localhost:10010', + pathname: requestPath + }); + } else { + data.path = requestPath; + } + + // get requestData from config if defined for this path:operation:response + if (config.requestData && + config.requestData[requestPath] && + config.requestData[requestPath][operation] && + config.requestData[requestPath][operation][response]) { + data.requestData = config.requestData[requestPath][operation][response]; + // if we have a GET request AND requestData, fill the path params accordingly + if (operation === 'get') { + var mockParameters = {}; + + data.pathParameters.forEach(function(parameter) { + // find the mock data for this parameter name + mockParameters[parameter.name] = data.requestData.filter(function(mock) { + return mock.hasOwnProperty(parameter.name); + })[0][parameter.name]; + }); + // only write parameters if they are not already defined in config + // @todo we should rework this with code above to be more readable + if (!config.pathParams) { + data.pathParams = mockParameters; + } + } } - - data.path += (((swagger.basePath !== undefined) && (swagger.basePath !== '/')) - ? swagger.basePath : '') + path; - return data; } /** - * Builds a unit test stubs for the response code of a path's operation + * Builds a unit test stubs for the response code of a apiPath's operation * @private * @param {json} swagger swagger file containing API - * @param {string} path API path to generate tests for - * @param {string} operation operation of the path to generate tests for - * @param {string} response response type of operation of current path + * @param {string} apiPath API apiPath to generate tests for + * @param {string} operation operation of the apiPath to generate tests for + * @param {string} response response type of operation of current apiPath * @param {json} config configuration for testGen * @param {string} consume content-type consumed by request * @param {string} produce content-type produced by the response * @param {info} info for cascading properties * @returns {string} generated test for response type */ -function testGenResponse(swagger, path, operation, response, config, - consume, produce, info) { +function testGenResponse(swagger, apiPath, operation, response, config, consume, produce, info) { var result; var templateFn; var source; var data; // get the data - data = getData(swagger, path, operation, response, config, info); + data = getData(swagger, apiPath, operation, response, config, info); if (produce === TYPE_JSON && !data.noSchema) { info.importValidator = true; } @@ -235,16 +258,26 @@ function testGenResponse(swagger, path, operation, response, config, data.returnType = produce; // compile template source and return test string - var templatePath = join(config.templatesPath, - config.testModule, operation, operation + '.handlebars'); + var templatePath = path.join(config.templatesPath, config.testModule, operation, operation + '.handlebars'); - source = read(templatePath, 'utf8'); + source = fs.readFileSync(templatePath, 'utf8'); templateFn = handlebars.compile(source, {noEscape: true}); - result = templateFn(data); + + if (data.requestData && data.requestData.length > 0) { + result = ''; + for (var i = 0; i < data.requestData.length; i++) { + data.request = JSON.stringify(data.requestData[i].body); + data.requestMessage = data.requestData[i].description.replace(/'/g, "\\'"); // eslint-disable-line quotes + result += templateFn(data); + } + } else { + result = templateFn(data); + } + return result; } -function testGenContentTypes(swagger, path, operation, res, config, info) { +function testGenContentTypes(swagger, apiPath, operation, res, config, info) { var result = []; var ndxC; var ndxP; @@ -254,30 +287,22 @@ function testGenContentTypes(swagger, path, operation, res, config, info) { if (!isEmpty(info.produces)) { // produces is defined for (ndxP in info.produces) { if (info.produces[ndxP] !== undefined) { - result.push(testGenResponse( - swagger, path, operation, res, config, - info.consumes[ndxC], info.produces[ndxP], info)); + result.push(testGenResponse(swagger, apiPath, operation, res, config, info.consumes[ndxC], info.produces[ndxP], info)); } } } else { // produces is not defined - result.push(testGenResponse( - swagger, path, operation, res, config, - info.consumes[ndxC], TYPE_JSON, info)); + result.push(testGenResponse(swagger, apiPath, operation, res, config, info.consumes[ndxC], TYPE_JSON, info)); } } } else if (!isEmpty(info.produces)) { // consumes is undefined but produces is defined for (ndxP in info.produces) { if (info.produces[ndxP] !== undefined) { - result.push(testGenResponse( - swagger, path, operation, res, config, - TYPE_JSON, info.produces[ndxP], info)); + result.push(testGenResponse(swagger, apiPath, operation, res, config, TYPE_JSON, info.produces[ndxP], info)); } } } else { // neither produces nor consumes are defined - result.push(testGenResponse( - swagger, path, operation, res, config, - TYPE_JSON, TYPE_JSON, info)); + result.push(testGenResponse(swagger, apiPath, operation, res, config, TYPE_JSON, TYPE_JSON, info)); } return result; @@ -285,24 +310,24 @@ function testGenContentTypes(swagger, path, operation, res, config, info) { /** * Builds a set of unit test stubs for all response codes of a - * path's operation + * apiPath's operation * @private * @param {json} swagger swagger file containing API - * @param {string} path API path to generate tests for - * @param {string} operation operation of the path to generate tests for + * @param {string} apiPath API apiPath to generate tests for + * @param {string} operation operation of the apiPath to generate tests for * @param {json} config configuration for testGen * @param {info} info for cascading properties - * @returns {string|Array} set of all tests for a path's operation + * @returns {string|Array} set of all tests for a apiPath's operation */ -function testGenOperation(swagger, path, operation, config, info) { +function testGenOperation(swagger, apiPath, operation, config, info) { - var responses = swagger.paths[path][operation].responses; + var responses = swagger.paths[apiPath][operation].responses; // filter out the wanted codes if (config.statusCodes) { responses = {}; config.statusCodes.forEach(function(code) { - responses[code] = swagger.paths[path][operation].responses[code]; + responses[code] = swagger.paths[apiPath][operation].responses[code]; }); } @@ -310,13 +335,12 @@ function testGenOperation(swagger, path, operation, config, info) { var source; var innerDescribeFn; - source = read(join(config.templatesPath, - '/innerDescribe.handlebars'), 'utf8'); + source = fs.readFileSync(path.join(config.templatesPath, '/innerDescribe.handlebars'), 'utf8'); innerDescribeFn = handlebars.compile(source, {noEscape: true}); // determines which produce types to use - if (!isEmpty(swagger.paths[path][operation].produces)) { - info.produces = swagger.paths[path][operation].produces; + if (!isEmpty(swagger.paths[apiPath][operation].produces)) { + info.produces = swagger.paths[apiPath][operation].produces; } else if (!isEmpty(swagger.produces)) { info.produces = swagger.produces; } else { @@ -324,8 +348,8 @@ function testGenOperation(swagger, path, operation, config, info) { } // determines which consumes types to use - if (!isEmpty(swagger.paths[path][operation].consumes)) { - info.consumes = swagger.paths[path][operation].consumes; + if (!isEmpty(swagger.paths[apiPath][operation].consumes)) { + info.consumes = swagger.paths[apiPath][operation].consumes; } else if (!isEmpty(swagger.consumes)) { info.consumes = swagger.consumes; } else { @@ -333,8 +357,8 @@ function testGenOperation(swagger, path, operation, config, info) { } // determines which security to use - if (!isEmpty(swagger.paths[path][operation].security)) { - info.security = swagger.paths[path][operation].security; + if (!isEmpty(swagger.paths[apiPath][operation].security)) { + info.security = swagger.paths[apiPath][operation].security; } else if (!isEmpty(swagger.security)) { info.security = swagger.security; } else { @@ -342,8 +366,7 @@ function testGenOperation(swagger, path, operation, config, info) { } _.forEach(responses, function(response, responseCode) { - result = result.concat(testGenContentTypes(swagger, path, operation, - responseCode, config, info)); + result = result.concat(testGenContentTypes(swagger, apiPath, operation, responseCode, config, info)); }); var output; @@ -359,15 +382,15 @@ function testGenOperation(swagger, path, operation, config, info) { } /** - * Builds a set of unit test stubs for all of a path's operations + * Builds a set of unit test stubs for all of a apiPath's operations * @private * @param {json} swagger swagger file containing API - * @param {string} path API path to generate tests for + * @param {string} apiPath API apiPath to generate tests for * @param {json} config configuration for testGen - * @returns {string|Array} set of all tests for a path + * @returns {string|Array} set of all tests for a apiPath */ -function testGenPath(swagger, path, config) { - var childProperty = swagger.paths[path]; +function testGenPath(swagger, apiPath, config) { + var childProperty = swagger.paths[apiPath]; var result = []; var validOps = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']; var allDeprecated = true; @@ -387,15 +410,13 @@ function testGenPath(swagger, path, config) { info.loadTest = config.loadTest; } - source = read(join(config.templatesPath, - '/outerDescribe.handlebars'), 'utf8'); + source = fs.readFileSync(path.join(config.templatesPath, '/outerDescribe.handlebars'), 'utf8'); outerDescribeFn = handlebars.compile(source, {noEscape: true}); _.forEach(childProperty, function(property, propertyName) { if (_.includes(validOps, propertyName) && !property.deprecated) { allDeprecated = false; - result.push( - testGenOperation(swagger, path, propertyName, config, info)); + result.push(testGenOperation(swagger, apiPath, propertyName, config, info)); } }); @@ -403,7 +424,7 @@ function testGenPath(swagger, path, config) { var customFormats = fs.readFileSync(require.resolve('./custom-formats'), 'utf-8'); var data = { - description: path, + description: apiPath, assertion: config.assertionFormat, testmodule: config.testModule, customFormats: customFormats, @@ -441,26 +462,23 @@ function testGen(swagger, config) { var environment; var ndx = 0; - // see if templatePath is set by user in config. - // else set it te the default location so we can pass it on. - config.templatesPath = (config.templatesPath) ? - config.templatesPath : join(__dirname, 'templates'); + config.templatesPath = (config.templatesPath) ? config.templatesPath : path.join(__dirname, 'templates'); swagger = deref(swagger); - source = read(join(config.templatesPath, '/schema.handlebars'), 'utf8'); + source = fs.readFileSync(path.join(config.templatesPath, '/schema.handlebars'), 'utf8'); schemaTemp = handlebars.compile(source, {noEscape: true}); handlebars.registerPartial('schema-partial', schemaTemp); - source = read(join(config.templatesPath, '/environment.handlebars'), 'utf8'); + source = fs.readFileSync(path.join(config.templatesPath, '/environment.handlebars'), 'utf8'); environment = handlebars.compile(source, {noEscape: true}); - len = 80; + helpers.setLen(80); if (config.maxLen && !isNaN(config.maxLen)) { - len = config.maxLen; + helpers.setLen(config.maxLen); } if (!targets || targets.length === 0) { // builds tests for all paths in API - _.forEach(paths, function(path, pathName) { + _.forEach(paths, function(apipath, pathName) { result.push(testGenPath(swagger, pathName, config)); }); } else { @@ -480,7 +498,7 @@ function testGen(swagger, config) { }); // build file names with paths - _.forEach(paths, function(path, pathName) { + _.forEach(paths, function(apipath, pathName) { // for output file name, replace / with -, and truncate the first / // eg: /hello/world -> hello-world filename = sanitize((pathName.replace(/\//g, '-').substring(1)) @@ -526,127 +544,14 @@ function testGen(swagger, config) { return output; } +handlebars.registerHelper('is', helpers.is); +handlebars.registerHelper('ifCond', helpers.ifCond); +handlebars.registerHelper('validateResponse', helpers.validateResponse); +handlebars.registerHelper('length', helpers.length); +handlebars.registerHelper('pathify', helpers.pathify); +handlebars.registerHelper('printJSON', helpers.printJSON); + + module.exports = { testGen: testGen }; - -// http://goo.gl/LFoiYG -handlebars.registerHelper('is', function(lvalue, rvalue, options) { - if (arguments.length < 3) { - throw new Error('Handlebars Helper \'is\' needs 2 parameters'); - } - - if (lvalue !== rvalue) { - return options.inverse(this); - } else { - return options.fn(this); - } -}); - -// http://goo.gl/LFoiYG -handlebars.registerHelper('ifCond', function(v1, v2, options) { - if (arguments.length < 3) { - throw new Error('Handlebars Helper \'ifCond\' needs 2 parameters'); - } - if (v1.length > 0 || v2) { - return options.fn(this); - } - return options.inverse(this); -}); - -/** - * determines if content types are able to be validated - * @param {string} type content type to be evaluated - * @param {boolean} noSchema whether or not there is a defined schema - * @param {Object} options handlebars built-in options - * @returns {boolean} whether or not the content can be validated - */ -handlebars.registerHelper('validateResponse', function(type, noSchema, - options) { - if (arguments.length < 3) { - throw new Error('Handlebars Helper \'validateResponse\'' + - 'needs 2 parameters'); - } - - if (!noSchema && type === TYPE_JSON) { - return options.fn(this); - } else { - return options.inverse(this); - } -}); - -/** - * replaces path params with obvious indicator for filling values - * (i.e. if any part of the path is surrounded in curly braces {}) - * @param {string} path request path to be pathified - * @param {object} pathParams contains path parameters to replace with - * @returns {string} pathified string - */ -handlebars.registerHelper('pathify', function(path, pathParams) { - var r; - - if (arguments.length < 3) { - throw new Error('Handlebars Helper \'pathify\'' + - ' needs 2 parameters'); - } - - if ((typeof path) !== 'string') { - throw new TypeError('Handlebars Helper \'pathify\'' + - 'requires path to be a string'); - } - - if ((typeof pathParams) !== 'object') { - throw new TypeError('Handlebars Helper \'pathify\'' + - 'requires pathParams to be an object'); - } - - if (Object.keys(pathParams).length > 0) { - var re = new RegExp(/(?:\{+)(.*?(?=\}))(?:\}+)/g); - var re2; - var matches = []; - var m = re.exec(path); - var i; - - while (m) { - matches.push(m[1]); - m = re.exec(path); - } - - for (i = 0; i < matches.length; i++) { - var match = matches[i]; - - re2 = new RegExp('(\\{+)' + match + '(?=\\})(\\}+)'); - - if (typeof (pathParams[match]) !== 'undefined' && - pathParams[match] !== null) { - // console.log("Match found for "+match+": "+pathParams[match]); - path = path.replace(re2, pathParams[match]); - } else { - // console.log("No match found for "+match+": "+pathParams[match]); - path = path.replace(re2, '{' + match + ' PARAM GOES HERE}'); - } - } - return path; - } - - r = new RegExp(/(?:\{+)(.*?(?=\}))(?:\}+)/g); - return path.replace(r, '{$1 PARAM GOES HERE}'); -}); - -/** - * split the long description into multiple lines - * @param {string} description request description to be splitted - * @returns {string} multiple lines - */ -handlebars.registerHelper('length', function(description) { - if (arguments.length < 2) { - throw new Error('Handlebar Helper \'length\'' + - ' needs 1 parameter'); - } - - if ((typeof description) !== 'string') { - throw new TypeError('Handlebars Helper \'length\'' + - 'requires path to be a string'); - } - return strObj(description).truncate(len - 50).s; -}); diff --git a/lib/helpers.js b/lib/helpers.js new file mode 100644 index 0000000..1323ff5 --- /dev/null +++ b/lib/helpers.js @@ -0,0 +1,192 @@ +'use strict'; + +var _ = require('lodash'); +var strObj = require('string'); +var url = require('url'); +var TYPE_JSON = 'application/json'; +var len; + +module.exports = { + is: is, + ifCond: ifCond, + validateResponse: validateResponse, + length: length, + pathify: pathify, + printJSON: printJSON, + len: len, + setLen: setLen +}; + +function setLen(descriptionLength) { + len = descriptionLength +} + +// http://goo.gl/LFoiYG +function is(lvalue, rvalue, options) { + if (arguments.length < 3) { + throw new Error('Handlebars Helper \'is\' needs 2 parameters'); + } + + if (lvalue !== rvalue) { + return options.inverse(this); + } else { + return options.fn(this); + } +} + +// http://goo.gl/LFoiYG +function ifCond(v1, v2, options) { + if (arguments.length < 3) { + throw new Error('Handlebars Helper \'ifCond\' needs 2 parameters'); + } + if (v1.length > 0 || v2) { + return options.fn(this); + } + return options.inverse(this); +} + +/** + * determines if content types are able to be validated + * @param {string} type content type to be evaluated + * @param {boolean} noSchema whether or not there is a defined schema + * @param {Object} options handlebars built-in options + * @returns {boolean} whether or not the content can be validated + */ +function validateResponse(type, noSchema, + options) { + if (arguments.length < 3) { + throw new Error('Handlebars Helper \'validateResponse\'' + + 'needs 2 parameters'); + } + + if (!noSchema && type === TYPE_JSON) { + return options.fn(this); + } else { + return options.inverse(this); + } +} + +/** + * replaces path params with obvious indicator for filling values + * (i.e. if any part of the path is surrounded in curly braces {}) + * @param {string} path request path to be pathified + * @param {object} pathParams contains path parameters to replace with + * @returns {string} pathified string + */ +function pathify(path, pathParams) { + var r; + + if (arguments.length < 3) { + throw new Error('Handlebars Helper \'pathify\'' + + ' needs 2 parameters'); + } + + if ((typeof path) !== 'string') { + throw new TypeError('Handlebars Helper \'pathify\'' + + 'requires path to be a string'); + } + + if ((typeof pathParams) !== 'object') { + throw new TypeError('Handlebars Helper \'pathify\'' + + 'requires pathParams to be an object'); + } + + if (Object.keys(pathParams).length > 0) { + var re = new RegExp(/(?:\{+)(.*?(?=\}))(?:\}+)/g); + var re2; + var matches = []; + var m = re.exec(path); + var i; + + while (m) { + matches.push(m[1]); + m = re.exec(path); + } + + for (i = 0; i < matches.length; i++) { + var match = matches[i]; + + re2 = new RegExp('(\\{+)' + match + '(?=\\})(\\}+)'); + + if (typeof (pathParams[match]) !== 'undefined' && pathParams[match] !== null) { + path = path.replace(re2, pathParams[match]); + } else { + path = path.replace(re2, '{' + match + ' PARAM GOES HERE}'); + } + } + return path; + } + + r = new RegExp(/(?:\{+)(.*?(?=\}))(?:\}+)/g); + return path.replace(r, '{$1 PARAM GOES HERE}'); +} + +/** + * split the long description into multiple lines + * @param {string} description request description to be splitted + * @returns {string} multiple lines + */ +function length(description) { + if (arguments.length < 2) { + throw new Error('Handlebar Helper \'length\'' + + ' needs 1 parameter'); + } + + if ((typeof description) !== 'string') { + throw new TypeError('Handlebars Helper \'length\'' + + 'requires path to be a string'); + } + + if (len === -1) { // toggle off truncation, this is a noop + return description; + } + + return strObj(description).truncate(len - 50).s; +} + +function printJSON(data) { + if (arguments.length < 2) { + throw new Error('Handlebar Helper \'printJSON\'' + + ' needs at least 1 parameter'); + } + + if (data !== null) { + if ((typeof data) === 'string') { + return '\'' + data + '\''; + } else if ((typeof data) === 'object') { + return '{\n'+prettyPrintJson(data, ' ')+'\n }'; + } else { + return data; + } + } else { + return null; + } +} + +// http://goo.gl/7DbFS +function prettyPrintJson(obj, indent) { + var result = ''; + + if (indent == null) indent = ''; + + for (var property in obj) { + if (property.charAt(0) !== '_') { + var value = obj[property]; + + if (typeof value === 'string') { + value = '\'' + value + '\''; + } else if (typeof value === 'object') { + if (value instanceof Array) { + value = '[ ' + value + ' ]'; + } else { + // Recursive dump + var od = prettyPrintJson(value, indent + ' '); + + value = '{\n' + od + '\n' + indent + '}'; + } + } + result += indent + property + ': ' + value + ',\n'; + } + } + return result.replace(/,\n$/, ''); +} diff --git a/templates/request/delete/delete.handlebars b/templates/request/delete/delete.handlebars index 889f11d..e30001e 100644 --- a/templates/request/delete/delete.handlebars +++ b/templates/request/delete/delete.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if requestMessage}} and {{requestMessage}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} diff --git a/templates/request/get/get.handlebars b/templates/request/get/get.handlebars index 5710187..7f49744 100644 --- a/templates/request/get/get.handlebars +++ b/templates/request/get/get.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if requestMessage}} and {{requestMessage}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} diff --git a/templates/request/head/head.handlebars b/templates/request/head/head.handlebars index f73d02c..abcd9a4 100644 --- a/templates/request/head/head.handlebars +++ b/templates/request/head/head.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if requestMessage}} and {{requestMessage}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} diff --git a/templates/request/patch/patch.handlebars b/templates/request/patch/patch.handlebars index 873a03b..ab84edc 100644 --- a/templates/request/patch/patch.handlebars +++ b/templates/request/patch/patch.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if requestMessage}} and {{requestMessage}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} @@ -20,11 +20,14 @@ Authorization: '{{headerSecurity.type}} ' + process.env.{{headerSecurity.name}}{{/if}} }, {{#is contentType 'application/json'}} - json: { + {{#if request}}json: { + {{ request }} + } + {{else}}json: { {{#each bodyParameters}} {{this.name}}: 'DATA GOES HERE'{{#unless @last}},{{/unless}} {{/each}} - } + }{{/if}} }, {{/is}} {{#is contentType 'application/x-www-form-urlencoded'}} diff --git a/templates/request/post/post.handlebars b/templates/request/post/post.handlebars index c5df6d4..4308823 100644 --- a/templates/request/post/post.handlebars +++ b/templates/request/post/post.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if requestMessage}} and {{requestMessage}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} @@ -21,11 +21,11 @@ Authorization: '{{headerSecurity.type}} ' + process.env.{{headerSecurity.name}}{{/if}} }, {{#is contentType 'application/json'}} - json: { + {{#if request}}json: {{ request }}{{else}}json: { {{#each bodyParameters}} {{this.name}}: 'DATA GOES HERE'{{#unless @last}},{{/unless}} {{/each}} - } + }{{/if}} }, {{/is}} {{#is contentType 'application/x-www-form-urlencoded'}} diff --git a/templates/request/put/put.handlebars b/templates/request/put/put.handlebars index b0cdc1a..b7645cf 100644 --- a/templates/request/put/put.handlebars +++ b/templates/request/put/put.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if requestMessage}} and {{requestMessage}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} @@ -21,11 +21,11 @@ Authorization: '{{headerSecurity.type}} ' + process.env.{{headerSecurity.name}}{{/if}} }, {{#is contentType 'application/json'}} - json: { - {{#each bodyParameters}} + {{#if request}}json: {{ request }}{{else}}json: { + {{#each bodyParameters}} {{this.name}}: 'DATA GOES HERE'{{#unless @last}},{{/unless}} - {{/each}} - } + {{/each}} + }{{/if}} }, {{/is}} {{#is contentType 'application/x-www-form-urlencoded'}} diff --git a/templates/supertest/delete/delete.handlebars b/templates/supertest/delete/delete.handlebars index 5439c0d..6b762e4 100644 --- a/templates/supertest/delete/delete.handlebars +++ b/templates/supertest/delete/delete.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if requestMessage}} and {{requestMessage}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} diff --git a/templates/supertest/get/get.handlebars b/templates/supertest/get/get.handlebars index 552aac9..3e270c8 100644 --- a/templates/supertest/get/get.handlebars +++ b/templates/supertest/get/get.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if requestMessage}} and {{requestMessage}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} diff --git a/templates/supertest/head/head.handlebars b/templates/supertest/head/head.handlebars index 23fa60f..f96633e 100644 --- a/templates/supertest/head/head.handlebars +++ b/templates/supertest/head/head.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if inputs}}, {{inputs.body.message}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} diff --git a/templates/supertest/options/options.handlebars b/templates/supertest/options/options.handlebars index ba09c28..91e65fb 100644 --- a/templates/supertest/options/options.handlebars +++ b/templates/supertest/options/options.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if requestMessage}} and {{requestMessage}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} diff --git a/templates/supertest/patch/patch.handlebars b/templates/supertest/patch/patch.handlebars index 25fe75a..24633a1 100644 --- a/templates/supertest/patch/patch.handlebars +++ b/templates/supertest/patch/patch.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if requestMessage}} and {{requestMessage}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} @@ -28,11 +28,15 @@ .set('{{headerApiKey.type}}', process.env.{{headerApiKey.name}}) {{/if}} {{#is contentType 'application/json'}} + {{#if request}} + .send({{ request }}) + {{else}} .send({ {{#each bodyParameters}} {{this.name}}: 'DATA GOES HERE'{{#unless @last}},{{/unless}} {{/each}} }) + {{/if}} {{/is}} {{#is contentType 'application/x-www-form-urlencoded'}} .send({ diff --git a/templates/supertest/post/post.handlebars b/templates/supertest/post/post.handlebars index 14ecca0..fc992e5 100644 --- a/templates/supertest/post/post.handlebars +++ b/templates/supertest/post/post.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if requestMessage}} and {{requestMessage}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} @@ -27,11 +27,15 @@ .set('{{headerApiKey.type}}', process.env.{{headerApiKey.name}}) {{/if}} {{#is contentType 'application/json'}} + {{#if request}} + .send({{ request}}) + {{else}} .send({ {{#each bodyParameters}} {{this.name}}: 'DATA GOES HERE'{{#unless @last}},{{/unless}} {{/each}} }) + {{/if}} {{/is}} {{#is contentType 'application/x-www-form-urlencoded'}} .send({ diff --git a/templates/supertest/put/put.handlebars b/templates/supertest/put/put.handlebars index 5cd6321..039718b 100644 --- a/templates/supertest/put/put.handlebars +++ b/templates/supertest/put/put.handlebars @@ -1,4 +1,4 @@ - it('should respond with {{length description}}', function(done) { + it('should respond with {{length description}}{{#if requestMessage}} and {{requestMessage}}{{/if}}', function(done) { {{#validateResponse returnType noSchema}} /*eslint-disable*/ {{> schema-partial this}} @@ -27,11 +27,15 @@ .set('{{headerApiKey.type}}', process.env.{{headerApiKey.name}}) {{/if}} {{#is contentType 'application/json'}} + {{#if request}} + .send({{ request }}) + {{else}} .send({ {{#each bodyParameters}} {{this.name}}: 'DATA GOES HERE'{{#unless @last}},{{/unless}} {{/each}} }) + {{/if}} {{/is}} {{#is contentType 'application/x-www-form-urlencoded'}} .send({ diff --git a/test/.eslintrc b/test/.eslintrc index 0a89fcf..dc772b3 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -5,4 +5,8 @@ rules: no-unused-expressions: false valid-jsdoc: 0; + quotes: [0] + key-spacing: false + + // we genrerate code with json stringify which uses single quotes diff --git a/test/length/compare/request/base-path-test.js b/test/length/compare/request/base-path-test.js new file mode 100644 index 0000000..8af26db --- /dev/null +++ b/test/length/compare/request/base-path-test.js @@ -0,0 +1,29 @@ +'use strict'; +var chai = require('chai'); +var request = require('request'); + +chai.should(); + +describe('/', function() { + describe('get', function() { + it('should respond with 200 OK, and this description is extra long for a simple length toggle test!', function(done) { + request({ + url: 'http://basic.herokuapp.com/', + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }, + function(error, res, body) { + if (error) return done(error); + + res.statusCode.should.equal(200); + + body.should.equal(null); // non-json response or no schema + done(); + }); + }); + + }); + +}); diff --git a/test/length/swagger.json b/test/length/swagger.json new file mode 100644 index 0000000..baf0ab4 --- /dev/null +++ b/test/length/swagger.json @@ -0,0 +1,29 @@ +{ + "swagger": "2.0", + "info": { + "version": "0.0.0", + "title": "Simple API" + }, + "host": "basic.herokuapp.com", + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "OK, and this description is extra long for a simple length toggle test!" + }, + "400": { + "description": "NOK" + }, + "404": { + "description": "not found" + }, + "500": { + "description": "very bad things happening" + } + } + } + + } + } +} diff --git a/test/length/test.js b/test/length/test.js new file mode 100644 index 0000000..f57faaf --- /dev/null +++ b/test/length/test.js @@ -0,0 +1,77 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 Apigee Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + +/** + * This files tests is the user can set the template location + */ +'use strict'; + +var assert = require('chai').assert; +var testGen = require('../../index.js').testGen; +var swagger = require('./swagger.json'); +var yaml = require('js-yaml'); +var join = require('path').join; +var rules; + +var fs = require('fs'); + +rules = yaml.safeLoad(fs.readFileSync(join(__dirname, + '/../../.eslintrc'), 'utf8')); +rules.env = {mocha: true}; + +describe('Toggle description truncation', function() { + var output = testGen(swagger, { + assertionFormat: 'should', + pathNames: [], + testModule: 'request', + statusCodes: [200], + maxLen: -1 + + }); + + it('should not truncate the description', function() { + + var paths1 = []; + var ndx; + + for (ndx in output) { + if (output) { + paths1.push(join(__dirname, '/compare/request/' + output[ndx].name)); + } + } + + assert.isArray(output); + assert.lengthOf(output, 1); + + var generatedCode; + + for (ndx in paths1) { + if (paths1 !== undefined) { + generatedCode = fs.readFileSync(paths1[ndx], 'utf8'); + assert.equal(output[ndx].test, generatedCode); + } + } + }); +}); diff --git a/test/request-data/compare/request/expect/user-test.js b/test/request-data/compare/request/expect/user-test.js new file mode 100644 index 0000000..3b6c94a --- /dev/null +++ b/test/request-data/compare/request/expect/user-test.js @@ -0,0 +1,80 @@ +'use strict'; +var chai = require('chai'); +var request = require('request'); +var expect = chai.expect; + +describe('/user', function() { + describe('post', function() { + it('should respond with 200 OK and some description', function(done) { + request({ + url: 'https://api.uber.com/user', + qs: { + longitude: 'DATA GOES HERE' + }, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + json: {"my-id":2} + }, + function(error, res, body) { + if (error) return done(error); + + expect(res.statusCode).to.equal(200); + + expect(body).to.equal(null); // non-json response or no schema + done(); + }); + }); + + it('should respond with 400 NOT OK', function(done) { + request({ + url: 'https://api.uber.com/user', + qs: { + longitude: 'DATA GOES HERE' + }, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + json: { + latitude: 'DATA GOES HERE' + } + }, + function(error, res, body) { + if (error) return done(error); + + expect(res.statusCode).to.equal(400); + + expect(body).to.equal(null); // non-json response or no schema + done(); + }); + }); + + it('should respond with 500 SERVER ERROR', function(done) { + request({ + url: 'https://api.uber.com/user', + qs: { + longitude: 'DATA GOES HERE' + }, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + json: { + latitude: 'DATA GOES HERE' + } + }, + function(error, res, body) { + if (error) return done(error); + + expect(res.statusCode).to.equal(500); + + expect(body).to.equal(null); // non-json response or no schema + done(); + }); + }); + + }); + +}); diff --git a/test/request-data/swagger.json b/test/request-data/swagger.json new file mode 100644 index 0000000..b0560ee --- /dev/null +++ b/test/request-data/swagger.json @@ -0,0 +1,59 @@ +{ + "swagger": "2.0", + "info": { + "version": "0.0.0", + "title": "Simple API" + }, + "host": "api.uber.com", + "schemes": [ + "https" + ], + "paths": { + "/user": { + "post": { + "parameters": [ + { + "name": "latitude", + "in": "body", + "description": "Latitude component of location.", + "required": true, + "type": "number", + "format": "double" + }, + { + "name": "longitude", + "in": "query", + "description": "longitude component of location.", + "required": true, + "type": "number", + "format": "double" + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "NOT OK" + }, + "500": { + "description": "SERVER ERROR" + } + } + } + } + }, + "definitions": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + } + } +} diff --git a/test/request-data/test.js b/test/request-data/test.js new file mode 100644 index 0000000..17e9a02 --- /dev/null +++ b/test/request-data/test.js @@ -0,0 +1,81 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 Apigee Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +'use strict'; + +var assert = require('chai').assert; +var testGen = require('../../index.js').testGen; +var swagger = require('./swagger.json'); +var yaml = require('js-yaml'); +var join = require('path').join; +var rules; +var read = require('fs').readFileSync; + +rules = yaml.safeLoad(read(join(__dirname, '/../../.eslintrc'), 'utf8')); +rules.env = {mocha: true}; + +describe('request data population', function() { + describe('with descriptipn', function() { + describe('expect', function() { + var output1 = testGen(swagger, { + assertionFormat: 'expect', + pathName: [], + testModule: 'request', + maxLen: -1, + requestData: { + '/user': { + post: { + 200: [{body: {"my-id": 2}, description: 'some description'}] + } + } + } + }); + + var paths1 = []; + var ndx; + + for (ndx in output1) { + if (output1) { + paths1.push(join(__dirname, '/compare/request/expect/' + output1[ndx].name)); + } + } + + it('should have a extended description and test data in the json request', function() { + + assert.isArray(output1); + assert.lengthOf(output1, 1); + + var generatedCode; + + for (ndx in paths1) { + if (paths1 !== undefined) { + generatedCode = read(paths1[ndx], 'utf8').replace(/\r\n/g, '\n'); + assert.equal(output1[ndx].test.replace(/\r\n/g, '\n'), generatedCode); + } + } + }); + }); + }); + +});