swagger-police
Advanced tools
Comparing version 0.0.1-beta to 0.0.2-beta
@@ -22,2 +22,24 @@ 'use strict'; | ||
function runTests({programOptions, swaggerPath, swaggerObject}) { | ||
return Promise.resolve() | ||
.then(() => testCaseFactory.getTestCases(swaggerObject)) | ||
.then((testCases) => { | ||
const hooks = hooksParser.parse(programOptions.hookFiles); | ||
const serverBaseUrl = getServerBaseUrl({programOptions, swaggerObject, swaggerPath}); | ||
return mochaTestBuilder.buildTestSuite({testCases, hooks, serverBaseUrl}); | ||
}) | ||
.then((mochaTestSuite) => { | ||
mochaTestSuite.run((failures) => { | ||
process.exit(failures); // exit with non-zero status if there were failures | ||
}); | ||
}); | ||
} | ||
function printTests({swaggerObject}) { | ||
return Promise.resolve() | ||
.then(() => testCaseFactory.getTestCases(swaggerObject)) | ||
.then((testCases) => testCases.forEach((testCase) => console.log(`${testCase.name}`))); | ||
} | ||
function runCommand(programOptions) { | ||
@@ -31,18 +53,8 @@ | ||
.then((swaggerObject) => { | ||
const hooks = hooksParser.parse(programOptions.hookFiles); | ||
const serverBaseUrl = getServerBaseUrl({programOptions, swaggerObject, swaggerPath}); | ||
if (programOptions.testcaseNames) { | ||
return printTests({swaggerObject}); | ||
} | ||
console.log(serverBaseUrl); | ||
return Promise.resolve() | ||
.then(() => testCaseFactory.getTestCases(swaggerObject)) | ||
.then((testCases) => mochaTestBuilder.buildTestSuite({testCases, hooks, serverBaseUrl})); | ||
return runTests({programOptions, swaggerPath, swaggerObject}); | ||
}) | ||
.then((mochaTestSuite) => { | ||
mochaTestSuite.reporter().run((failures) => { | ||
// process.on('exit', () => { | ||
process.exit(failures); // exit with non-zero status if there were failures | ||
// }); | ||
}); | ||
}) | ||
.catch((error) => { | ||
@@ -49,0 +61,0 @@ console.log(error); |
@@ -5,3 +5,3 @@ 'use strict'; | ||
const path = require('path'); | ||
const glob = require("glob"); | ||
const glob = require('glob'); | ||
@@ -21,3 +21,3 @@ function parse(hooksPattern) { | ||
require(path.resolve(process.cwd(), file)); | ||
}) | ||
}); | ||
} | ||
@@ -24,0 +24,0 @@ |
@@ -18,23 +18,15 @@ 'use strict'; | ||
testCaseHooks.push({hookName, before, after}); | ||
testCaseHooks.push({name: hookName, before, after}); | ||
this.testCaseHooksMap[testCaseName] = testCaseHooks; | ||
}; | ||
this.beforeAll = (hook) => { | ||
this.globalHooks.beforeAll = hook; | ||
}; | ||
this.beforeAll = (hook) => this.globalHooks.beforeAll = hook; | ||
this.afterAll = (hook) => { | ||
this.afterAll = (hook) => this.globalHooks.afterAll = hook; | ||
}; | ||
this.beforeEach = (hook) => this.globalHooks.beforeEach = hook; | ||
this.beforeEach = (hook) => { | ||
}; | ||
this.afterEach = (hook) => { | ||
}; | ||
this.afterEach = (hook) => this.globalHooks.afterEach = hook; | ||
} | ||
module.exports = Hooks; |
@@ -10,18 +10,29 @@ 'use strict'; | ||
function buildTestSuite({testCases, hooks, serverBaseUrl}) { | ||
function buildUri(baseUrl, basePath, request) { | ||
const mocha = new Mocha(); | ||
const parentSuite = Mocha.Suite.create(mocha.suite, 'Swagger API Tests'); | ||
let path = request.path; | ||
testCases.forEach((testCase) => createMochaSuite({parentSuite, testCase, hooks, serverBaseUrl})); | ||
if (basePath && basePath !== '/') { | ||
path = basePath + request.path; | ||
} | ||
return mocha; | ||
if (!_.isEmpty(request.pathParams)) { | ||
_.entries(request.pathParams, ([key, value]) => { | ||
path.replace(`{${key}}`, value); | ||
}); | ||
} | ||
return url.resolve(baseUrl, path); | ||
} | ||
function prettyJSON(obj) { | ||
return JSON.stringify(obj, null, 2); | ||
} | ||
function createMochaSuite({parentSuite, testCase, hooks, serverBaseUrl}) { | ||
let testCaseHooks = hooks.testCaseHooksMap[testCase.name]; | ||
const testCaseHooks = hooks.testCaseHooksMap[testCase.name] || []; | ||
if (_.isEmpty(testCaseHooks)) { | ||
testCaseHooks = [{}]; | ||
testCaseHooks.push({}); | ||
} | ||
@@ -34,15 +45,10 @@ | ||
const suiteName = testCase.name + (hooksCount > 1 ? ` # ${hook.name || (index+1)}`:''); | ||
const mochaSuite = Mocha.Suite.create(parentSuite, suiteName); | ||
if (hook.before) { | ||
mochaSuite.beforeEach((done) => { | ||
hook.before(testCase, done); | ||
}); | ||
mochaSuite.beforeEach((done) => hook.before(testCase, done)); | ||
} | ||
if (hook.after) { | ||
mochaSuite.afterEach((done) => { | ||
hook.after(testCase, done); | ||
}); | ||
mochaSuite.afterEach((done) => hook.after(testCase, done)); | ||
} | ||
@@ -66,4 +72,3 @@ | ||
options.json = true; | ||
} | ||
else if (request.form) { | ||
} else if (request.form) { | ||
options.form = request.form; | ||
@@ -83,3 +88,3 @@ } | ||
if (!_.isEmpty(expectedResponse.headers)) { | ||
// TODO: add response header validations | ||
} | ||
@@ -93,4 +98,4 @@ | ||
`${prettyJSON(result.errors)} | ||
${prettyJSON(result.missing)} | ||
` | ||
${prettyJSON(result.missing)} | ||
` | ||
); | ||
@@ -101,3 +106,3 @@ } | ||
}) | ||
.catch(done) | ||
.catch(done); | ||
})); | ||
@@ -107,17 +112,19 @@ }); | ||
function buildUri(baseUrl, basePath, request) { | ||
function invokeGlobalHooks({testCases, hooks, parentSuite}) { | ||
let path = basePath + request.path; | ||
_.functions(hooks.globalHooks).forEach((hookName) => { | ||
const setHook = parentSuite[hookName].bind(parentSuite); | ||
setHook((done) => hooks.globalHooks[hookName](testCases, done)); | ||
}); | ||
} | ||
if (!_.isEmpty(request.pathParams)) { | ||
_.entries(request.pathParams, ([key, value]) => { | ||
path.replace(`{${key}}`, value); | ||
}); | ||
} | ||
function buildTestSuite({testCases, hooks, serverBaseUrl}) { | ||
return url.resolve(baseUrl, path); | ||
} | ||
const mocha = new Mocha(); | ||
const parentSuite = Mocha.Suite.create(mocha.suite, 'Swagger API Tests'); | ||
function prettyJSON(obj) { | ||
return JSON.stringify(obj, null, 2); | ||
invokeGlobalHooks({testCases, hooks, parentSuite}); | ||
testCases.forEach((testCase) => createMochaSuite({parentSuite, testCase, hooks, serverBaseUrl})); | ||
return mocha; | ||
} | ||
@@ -124,0 +131,0 @@ |
@@ -9,3 +9,4 @@ | ||
.option('--server [server]', 'The API endpoint') | ||
.option('--hook-files [hookFiles]', 'Specify pattern to match hook files'); | ||
.option('--hook-files [hookFiles]', 'Specify pattern to match hook files') | ||
.option('--testcase-names [testcaseNames]', 'Print all the testcase names (does not execute the tests)'); | ||
@@ -12,0 +13,0 @@ program |
'use strict'; | ||
const _ = require('lodash'); | ||
const SwagMock = require('swagmock'); | ||
function generateSampleData({param, mockParams}) { | ||
function parseRequestParams(operationObj) { | ||
const example = param['x-example']; | ||
if (example) { | ||
return example; | ||
} | ||
const mock = mockParams.parameters[param.in].find(({name}) => name === param.name); | ||
return mock.value; | ||
} | ||
function parseRequestParams({mockGen, path, operation, operationObj}) { | ||
const requestParams = { | ||
@@ -14,83 +26,92 @@ pathParams: {}, | ||
if (operationObj.parameters.length !== 0) { | ||
if (!operationObj.parameters) { | ||
return Promise.resolve(requestParams); | ||
} | ||
operationObj.parameters.forEach((param) => { | ||
return Promise.resolve() | ||
.then(() => mockGen.parameters({path, operation})) | ||
.then((mockParams) => { | ||
const sampleData = generateSampleData(param); | ||
operationObj.parameters.forEach((param) => { | ||
const sampleData = generateSampleData({param, mockParams}); | ||
switch (param.in) { | ||
case 'query': | ||
requestParams.query[param.name] = sampleData; | ||
break; | ||
switch (param.in) { | ||
case 'query': | ||
requestParams.query[param.name] = sampleData; | ||
break; | ||
case 'header': | ||
requestParams.headers[param.name] = sampleData; | ||
break; | ||
case 'header': | ||
requestParams.headers[param.name] = sampleData; | ||
break; | ||
case 'path': | ||
requestParams.pathParams[param.name] = sampleData; | ||
break; | ||
case 'path': | ||
requestParams.pathParams[param.name] = sampleData; | ||
break; | ||
case 'body': | ||
requestParams.body = sampleData; | ||
break; | ||
case 'body': | ||
requestParams.body = sampleData; | ||
break; | ||
case 'formData': | ||
requestParams.form = sampleData; | ||
break; | ||
} | ||
case 'formData': | ||
requestParams.form = sampleData; | ||
break; | ||
} | ||
}); | ||
return requestParams; | ||
}); | ||
} | ||
return requestParams; | ||
} | ||
function generateSampleData(param) { | ||
return param['x-example']; | ||
} | ||
function parseOperation({swaggerObject, path, operation, operationObj}) { | ||
const mockGen = SwagMock(swaggerObject, {validated: true}); | ||
function getTestCases(swaggerObject) { | ||
return Promise.all(_.entries(operationObj.responses) | ||
.map(([statusCode, responseObj]) => { | ||
const testCases = []; | ||
return Promise.resolve() | ||
.then(() => parseRequestParams({mockGen, path, operation, operationObj})) | ||
.then((requestParams) => { | ||
const request = Object.assign( | ||
{ | ||
path, | ||
method: operation | ||
}, | ||
requestParams | ||
); | ||
const basePath = swaggerObject.basePath; | ||
return { | ||
name: `${operation.toUpperCase()} ${path} -> ${statusCode}`, | ||
basePath: swaggerObject.basePath, | ||
request, | ||
response: { | ||
statusCode: Number(statusCode), | ||
schema: responseObj.schema, | ||
headers: responseObj.headers | ||
}, | ||
swaggerObject | ||
}; | ||
}); | ||
}) | ||
); | ||
} | ||
_.entries(swaggerObject.paths).forEach(([path, pathObj]) => { | ||
function parsePath({swaggerObject, path, pathObj}) { | ||
_.entries(pathObj).forEach(([operation, operationObj]) => { | ||
return Promise.resolve(_.entries(pathObj)) | ||
.then((entries) => Promise.all( | ||
entries.map(([operation, operationObj]) => parseOperation({swaggerObject, path, operation, operationObj})) | ||
)) | ||
.then(_.flatMap); | ||
} | ||
_.entries(operationObj.responses).forEach(([statusCode, responseObj]) => { | ||
function getTestCases(swaggerObject) { | ||
const request = Object.assign( | ||
{ | ||
path, | ||
method: operation | ||
}, | ||
parseRequestParams(operationObj) | ||
); | ||
const testCase = { | ||
name: `${operation.toUpperCase()} ${path} -> ${statusCode}`, | ||
basePath, | ||
request, | ||
response: { | ||
statusCode: Number(statusCode), | ||
schema: responseObj.schema, | ||
headers: responseObj.headers | ||
}, | ||
swaggerObject | ||
}; | ||
testCases.push(testCase); | ||
}); | ||
}); | ||
}); | ||
return testCases; | ||
return Promise.resolve(_.entries(swaggerObject.paths)) | ||
.then((entries) => Promise.all(entries.map(([path, pathObj]) => parsePath({swaggerObject, path, pathObj})))) | ||
.then(_.flatMap); | ||
} | ||
module.exports = { | ||
getTestCases | ||
}; |
{ | ||
"name": "swagger-police", | ||
"version": "0.0.1-beta", | ||
"version": "0.0.2-beta", | ||
"description": "Automatically validates the APIs against the published swagger specification", | ||
@@ -9,2 +9,3 @@ "bin": { | ||
"scripts": { | ||
"lint": "eslint .", | ||
"test": "node $NODE_DEBUG_OPTION build-tools/jasmine.js JASMINE_CONFIG_PATH=jasmine.json" | ||
@@ -22,2 +23,3 @@ }, | ||
"glob": "^7.1.1", | ||
"json-schema-faker": "^0.4.0", | ||
"lodash": "^4.17.2", | ||
@@ -28,5 +30,7 @@ "mocha": "^3.2.0", | ||
"swagger-parser": "^3.4.1", | ||
"swagmock": "^1.0.0", | ||
"tv4": "^1.2.7" | ||
}, | ||
"devDependencies": { | ||
"eslint": "^3.12.2", | ||
"jasmine": "^2.5.2", | ||
@@ -33,0 +37,0 @@ "jasmine-console-reporter": "^1.2.7", |
129
README.md
@@ -1,9 +0,26 @@ | ||
*swagger-police* is a command line tool for validating APIs against the published swagger specification. It helps | ||
to ensure that the APIs hosted by the server confirms to the behaviour mentioned in the published swagger specification. | ||
**swagger-police** is a command line tool for validating backend APIs against the published swagger specification. It can | ||
be plugged into a continuous integration system to ensure that the backend APIs confirms to the behaviour mentioned | ||
in the published swagger specification. | ||
### Installation ### | ||
This library is very similar (and inspired) from abao (https://github.com/cybertk/abao) which does similar validations for a RAML spec | ||
**Please Note:** The library is still in beta and is WIP ! | ||
# Features # | ||
* Calls every API defined in the swagger file by generating mock data from the specs (data can be customised, see usage). This ensures that the url params, query params, headers and the body defined in the spec is supported by the service. | ||
* Verifies the HTTP response status and headers agaisnt the specs. | ||
* Verifies the response body against the schema defined in specs (JSON schema validation). | ||
* Supports the following hook methods for customising the tests (see Hooks) | ||
* BeforeAll | ||
* AfterAll | ||
* BeforeEach | ||
* AfterEach | ||
* Test specific *Before* and *After* | ||
# Installation # | ||
`npm install -g swagger-police` | ||
### Usage ### | ||
# Usage # | ||
@@ -17,6 +34,102 @@ ``` | ||
-h, --help output usage information | ||
--server [server] The API endpoint | ||
--hook-files [hookFiles] Specify pattern to match hook files | ||
-h, --help output usage information | ||
--server [server] The API endpoint | ||
--hook-files [hookFiles] Specify pattern to match hook files | ||
--testcase-names [testcaseNames] Print all the testcase names (does not execute the tests) | ||
``` | ||
``` | ||
* **Swagger URL/path** - The path or the URL to the swagger file | ||
* **Server** - The API endpoint base url. No need to specify this if a swagger url is specified and the APIs are hosted in the same server. | ||
* **Hook Files** - A glob pattern (reletive to the execution directory) to load the hook files. | ||
# Hooks # | ||
The tool supports the following test hooks to enable setup/tear-down tasks or customising | ||
the individual tests. Hooks are simple JavaScript files which have access to a global `hooks` object with methods to add the specific hooks. | ||
## `BeforeAll` and `AfterAll` ## | ||
These will be executed once before the tests start and after all the tests have been | ||
executed. Note that only one of each type can be specified, there cannot be more than one beforeAll/afterAll hooks. However, testcase specific hooks can be specified, see below. | ||
``` | ||
hooks.beforeAll((testcases, done) => { | ||
done(); | ||
}); | ||
hooks.afterAll((testcases, done) => { | ||
done(); | ||
}); | ||
``` | ||
**testcases** - An array of `testcase` objects to be executed. This is generated from the swagger specs. Any customisations made to the objects in the `beforeAll` hook will be reflected in the tests. | ||
**done** - The callback function | ||
## `BeforeEach` and `AfterEach` ## | ||
These will be executed before and after every test. Note that only one of each type can be specified, there cannot be more than one beforeEach/afterEach hooks. However, testcase specific hooks can be specified, see below. | ||
``` | ||
hooks.beforeEach((testcases, done) => { | ||
done(); | ||
}); | ||
hooks.afterEach((testcases, done) => { | ||
done(); | ||
}); | ||
``` | ||
**testcases** - An array of `testcase` objects to be executed. This is generated from the swagger specs. Any customisations made to the objects in the `beforeAll` hook will be reflected in the tests. | ||
**done** - The callback function | ||
## Testcase specific hooks ## | ||
`before` and `after` testcase specific hooks can be specified which will only be executed before and after the | ||
specific testcase. The testcases are identified using a generated name. Run the tool with the `--testcase-names` | ||
option to print out all the testcase names. | ||
The hooks can be specified using the following method | ||
``` | ||
hooks.add('GET /pet/{petId} -> 200', { | ||
before: (testcase, done) => { | ||
done(); | ||
}, | ||
after: (testcase, done) => { | ||
done(); | ||
} | ||
}); | ||
``` | ||
** 1st Argument** - The testcase name. Please note that this is case sensitive. | ||
** 2nd argument** - An object with before and after functions which takes in testcase (The testcase object | ||
representing the specific test) and a callback. Any modifications made to the testcase object will reflect in the test. | ||
If more than one such hook is specified for a specific test, the test will be executed once for every hook specified. | ||
Custom test name can be added to identify each pass. See below | ||
``` | ||
hooks.add('GET /pet/{petId} -> 200 # Pass 1', { | ||
before: (testCase, done) => { | ||
done(); | ||
}, | ||
after: (testCase, done) => { | ||
done(); | ||
} | ||
}); | ||
hooks.add('GET /pet/{petId} -> 200 # Pass 2', { | ||
before: (testCase, done) => { | ||
done(); | ||
}, | ||
after: (testCase, done) => { | ||
done(); | ||
} | ||
}); | ||
``` | ||
@@ -1,4 +0,3 @@ | ||
hooks.add('GET /cloud/{cloudId}/settings -> 200', { | ||
hooks.add('GET /pet/{petId} -> 200', { | ||
before: (testCase, done) => { | ||
console.log('Before'); | ||
done(); | ||
@@ -8,5 +7,30 @@ }, | ||
after: (testCase, done) => { | ||
console.log('After'); | ||
done(); | ||
} | ||
}); | ||
hooks.add('GET /pet/{petId} -> 200 # Pass 2', { | ||
before: (testCase, done) => { | ||
done(); | ||
}, | ||
after: (testCase, done) => { | ||
done(); | ||
} | ||
}); | ||
hooks.beforeEach((testcases, done) => { | ||
done(); | ||
}); | ||
hooks.afterEach((testcases, done) => { | ||
done(); | ||
}); | ||
hooks.beforeAll((testcases, done) => { | ||
done(); | ||
}); | ||
hooks.afterAll((testcases, done) => { | ||
done(); | ||
}); |
@@ -20,7 +20,3 @@ 'use strict'; | ||
it('sample test', willResolve(() => { | ||
return runCommand('https://join-site-service.internal.atlassian.io/api.swagger', 'http://172.22.37.83:35364'); | ||
return runCommand('http://172.22.37.83:35364/swagger.json', 'http://172.22.37.83:35364'); | ||
})); | ||
@@ -27,0 +23,0 @@ }); |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
23805
19
412
135
11
5
2
1
+ Addedjson-schema-faker@^0.4.0
+ Addedswagmock@^1.0.0
+ Addedchance@1.1.12(transitive)
+ Addeddeep-extend@0.6.0(transitive)
+ Addedderef@0.7.6(transitive)
+ Addeddrange@1.1.1(transitive)
+ Addedfaker@4.1.0(transitive)
+ Addedjson-schema-faker@0.4.7(transitive)
+ Addedmoment@2.30.1(transitive)
+ Addedrandexp@0.4.9(transitive)
+ Addedret@0.2.2(transitive)
+ Addedswagmock@1.0.0(transitive)
+ Addedtslib@1.14.1(transitive)