diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..24808e5
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,94 @@
+module.exports = {
+ env: {
+ browser: true,
+ es6: true,
+ node: true
+ },
+ extends: 'eslint:recommended',
+ globals: {
+ Atomics: 'readonly',
+ SharedArrayBuffer: 'readonly'
+ },
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: 'module'
+ },
+ rules: {
+ 'linebreak-style': ['error', 'unix'],
+ quotes: ['error', 'single'],
+ semi: ['error', 'always'],
+ 'no-var': 'error',
+ 'block-scoped-var': 'error',
+ curly: 'error',
+ 'default-case': 'error',
+ 'dot-notation': 'error',
+ 'no-empty-function': 'error',
+ 'no-else-return': 'error',
+ 'no-eval': 'error',
+ 'no-extra-bind': 'error',
+ 'no-extra-label': 'error',
+ 'no-implied-eval': 'error',
+ 'no-invalid-this': 'error',
+ 'no-multi-spaces': 'error',
+ 'no-new': 'error',
+ 'no-new-func': 'error',
+ 'no-new-wrappers': 'error',
+ 'no-return-assign': 'error',
+ 'no-return-await': 'error',
+ 'no-self-compare': 'error',
+ 'no-sequences': 'error',
+ 'no-throw-literal': 'error',
+ 'no-useless-call': 'error',
+ 'no-useless-catch': 'error',
+ 'no-useless-concat': 'error',
+ 'no-with': 'error',
+ 'no-void': 'error',
+ 'prefer-promise-reject-errors': 'error',
+ yoda: 'error',
+ 'prefer-const': 'error',
+ //"prefer-arrow-callback": "error",
+ 'handle-callback-err': 'error',
+ 'no-async-promise-executor': 'error',
+ 'no-case-declarations': 'error',
+ 'no-shadow': 'error',
+ 'no-undef-init': 'error',
+ 'no-undefined': 'error',
+ 'no-use-before-define': 'error',
+ 'no-new-require': 'error',
+ 'no-process-env': 'error',
+ // 'no-sync': 'error',
+ strict: 'error',
+ 'arrow-spacing': 'error',
+ 'no-floating-decimal': 'error',
+ 'no-buffer-constructor': 'error',
+ // "no-mixed-requires": ["error", { "grouping": true }],
+ 'max-classes-per-file': ['error', 1],
+ 'arrow-body-style': ['error', 'as-needed'],
+ 'no-confusing-arrow': ['error', { allowParens: true }],
+ 'no-duplicate-imports': 'error',
+ 'no-useless-computed-key': 'error',
+ 'no-useless-constructor': 'error',
+ 'no-useless-rename': 'error',
+ // "prefer-destructuring": "error",
+ // "prefer-template": "error",
+ 'capitalized-comments': ['error'],
+ 'consistent-this': ['error', 'oThis'],
+ 'eol-last': ['error', 'always'],
+ 'id-length': ['error', { min: 2 }],
+ 'key-spacing': ['error', { beforeColon: false, afterColon: true }],
+ // 'lines-around-comment': ['error', { beforeBlockComment: false, beforeLineComment: false }],
+ 'lines-between-class-members': ['error', 'always'],
+ 'no-array-constructor': 'error',
+ 'max-depth': ['error', 4],
+ 'max-lines-per-function': ['error', { max: 80, skipBlankLines: true, skipComments: true }],
+ 'no-lonely-if': 'error',
+ 'no-multiple-empty-lines': 'error',
+ 'no-negated-condition': 'error',
+ 'no-new-object': 'error',
+ 'no-trailing-spaces': 'error',
+ 'no-unneeded-ternary': 'error',
+ 'operator-assignment': ['error', 'always'],
+ 'padding-line-between-statements': ['error', { blankLine: 'always', prev: '*', next: 'return' }],
+ 'spaced-comment': ['error', 'always', { exceptions: ['-', '+'] }]
+ }
+};
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6c5d546
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,67 @@
+# Ignore tmp files and folders
+/tmp/*
+!/log/.keep
+!/tmp/.keep
+
+*.log
+
+# Editor / IDE files
+/.idea
+/.idea/*
+.vscode
+
+# Output of 'npm pack'
+*.tgz
+
+# dotenv environment variables file
+.env
+.env.test
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directory
+# https://docs.npmjs.com/cli/shrinkwrap#caveats
+node_modules
+package-lock.json
+
+# Optional npm cache directory
+.npm
+npm-debug.log*
+
+# Optional eslint cache
+.eslintcache
+
+# Dev Folder
+.dev
+
+.DS_Store
+
+localTests
+shared-local-instance.db
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..cc284a1
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,12 @@
+{
+ "printWidth": 120,
+ "tabWidth": 2,
+ "useTabs": false,
+ "semi": true,
+ "singleQuote": true,
+ "trailingComma": "none",
+ "bracketSpacing": true,
+ "arrowParens": "always",
+ "parser": "flow",
+ "proseWrap": "preserve"
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..056cb88
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+## Openapi-test-suite v1.0.0
+This is the very first release of this package.
+- runTests method added which starts the DRY test suite based on openapi.json file.
+- SchemaValidator exposed to validate any data vis-a-vis it's schema.
+- Running tests for SQL injections and executable command injections.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..65c5ca8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..efd0cdb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,326 @@
+# Openapi Test Suite
+![npm version](https://img.shields.io/npm/v/@plgworks/openapi-test-suite.svg?style=flat)
+
+## Objective
+This package aims to solve the following two problems:
+1. Maintenance is a big problem to solve in any test suite. As the APIs evolve at a fast pace, maintenance becomes more difficult.
+2. Usually when negative test cases are written, all combinations of parameters are not covered. Writing all the combinations manually is difficult and gets neglected.
+
+We solve point 1, by consuming the openapi.json file to generate test cases in DRY manner.
+Openapi.json is auto-generated using a generator script, as described in this [blog](https://plgworks.com/blog/dry-api-docs-and-validations/).
+
+To solve point 2, we use the cartesian product of possible (correct as well as incorrect) sets of values for each parameter to get to the set of all negative test cases.
+
+## Approach
+- For each route, we figure out the possible set of values (correct as well as incorrect) for each parameter. Now, we take the
+ cartesian product of these sets, to get all possible combinations of parameters. Correctness of the values is based
+ on the parameter type and parameter constraints given in openapi.json
+- For all the incorrect combinations, we fire API calls and check whether the API response has the correct param level error.
+- In each route, every optional parameter is tested with its correct, incorrect as well as null value.
+- For cookie protected routes, we can include a Security Scheme object as a third argument while initializing the package. This object will be automatically consumed
+ based on the specific security requirements of a route. If the cookies provided in Security Scheme object cannot be validated then http error 401 Unauthorized is expected.
+
+## Install
+For installing the npm package, run following command in terminal:
+```
+ npm install @plgworks/openapi-test-suite
+```
+
+## Initialize
+
+```node
+ const openapiObj = require('@plgworks/openapi-test-suite');
+
+ const ApiTestSuite = require('@plgworks/openapi-test-suite');
+ const serverIndex = 0; // Index of the server (to be hit) from the servers block of openapi.json
+ const securityScheme = require('./examples/securityScheme.json');
+ const apiSuiteObj = new ApiTestSuite(openApiObj, serverIndex, securityScheme);
+```
+
+## Run Test Suite
+```node
+ // To run the test suite, use following command
+ apiSuiteObj.runTest();
+
+ // After the test suite run is over, it is recommended to call cleanup because the openApiObj and serverIndex are in-memory cached.
+ apiSuiteObj.cleanup();
+```
+
+### Examples
+
+#### Case 1: Expected success true but received false.
+In the following, all params were passed correctly, but the API response received `success` as false.
+```
+ GET /api/admin/web/login/phone/otp
+ params: {"country_code":1,"raw_phone_number":"9878654325"}
+ Expected response success: true
+ Incorrect Parameters: {}
+ Response HTTP code: 200
+ API response: {
+ "success": false,
+ "err": {
+ "code": "BAD_REQUEST",
+ "msg": "Something went wrong.",
+ "error_data": [
+ {
+ "parameter": "phone_number",
+ "msg": "Invalid phone number."
+ }
+ ],
+ "internal_id": "INVALID_PHONE_NUMBER"
+ }
+ }
+ Response validation error: {"kind":"errorRespForCorrectCase"}
+```
+
+#### Case 2: Expected success false but received true.
+In the following, `raw_phone_number` was passed incorrectly, but the API response received `success` as true.
+```
+ GET /api/admin/web/login/phone/otp
+ params: {"country_code":91,"raw_phone_number":null}
+ Expected response success: false
+ Incorrect Parameters: {"raw_phone_number":"Value cannot be null"}
+ Response HTTP code: 200
+ API response: {
+ "success": true,
+ "data": {
+ "otp_detail": {
+ "id": 69,
+ "sms_identifier": "69:dfdc1b36-3fa8-4b9c-8c09-a61f334dfd9f",
+ "uts": 1653291961
+ }
+ }
+ }
+```
+
+#### Case 3: Incorrect parameter passed but not obtained back as an error
+In the following, `raw_phone_number` was passed incorrectly, but the API response `error_data` does not convey the same.
+```
+ GET /api/admin/web/login/phone/otp
+ params: {"country_code":"959bb2","raw_phone_number":null}
+ Expected response success: false
+ incorrectParamsMap: {"country_code":"Value cannot be string","raw_phone_number":"Value cannot be null"}
+ Response HTTP code: 200
+ API response: {
+ "success": false,
+ "err": {
+ "code": "BAD_REQUEST",
+ "msg": "Something went wrong.",
+ "error_data": [
+ {
+ "parameter": "country_code",
+ "msg": "Invalid parameter country_code. Please ensure the input is well formed."
+ },
+ {
+ "parameter": "phone_number",
+ "msg": "Invalid phone number."
+ }
+ ],
+ "internal_id": "v_ap_rd_1"
+ }
+ }
+ Response validation error: {"kind":"parameterErrorNotObtained","parameter":"phone_number"}
+```
+
+#### Case 4: Correct parameter passed but got API error.
+In the following, all params were passed correctly, but got the api error "Something went wrong".
+```
+ GET /api/admin/web/login/phone/otp
+ params: {"country_code":91,"raw_phone_number":"9876543239"}
+ Expected response success: true
+ Incorrect Parameters: {}
+ Response HTTP code: 500
+ API response: {
+ "success": false,
+ "err": {
+ "code": "INTERNAL_SERVER_ERROR",
+ "msg": "Something went wrong.",
+ "error_data": [],
+ "internal_id": "PHONE_NUMBER_DOES_NOT_EXIST"
+ }
+ }
+ Response validation error: {"kind":"errorRespForCorrectCase"}
+```
+
+#### Case 5: Correct parameter passed but error in schema validation.
+In the following, all params were passed correctly, but got `respEntityTypeMismatch` error.
+```
+ GET /api/admin/web/login/phone/otp
+ params: {"country_code":91,"raw_phone_number":"9876543233"}
+ Expected response success: true
+ Incorrect Parameters: {}
+ Response HTTP code: 200
+ API response: {
+ "success": true,
+ "data": {
+ "otp_detail": {
+ "id": "abc",
+ "sms_identifier": "72:83c91ffc-9741-492f-8fbf-f61d6e9b9ba8",
+ "uts": 1653293902
+ }
+ }
+ }
+ Response validation error: {"kind":"respEntityTypeMismatch","debugLevel":"response.data.otp_detail.id","schemaType":"string"}
+```
+
+#### Case 6: Incorrect cookie value and correct parameters passed.
+In the following, incorrect cookie value was passed, so the expected response http code was 401; but the API response received `success` as true.
+```
+ GET /api/consumer/v1/user/current
+ params: {"country_code":91,"raw_phone_number":"9876543233"}
+ Expected response success: false
+ Incorrect Parameters: {}
+ Response HTTP code: 200
+ API response: {
+ "success": true,
+ "data": {
+ "current_user": {
+ "id": 100000,
+ "first_name": "ishaphone",
+ "last_name": "lastn",
+ "basic_user_detail_id": 100000,
+ "status": "ACTIVE",
+ "uts": 1651651178
+ }
+ }
+ }
+ Response validation error: {"kind":"mandatoryCookieValidationFailed"}
+ Request headers: {"Cookie":"aulc=25de238247286fa7fc7b9cbd071a45eda87f8690dd524cbf71c104d1d7e8252ef2dc4a698a389368dceb673238ccc47a342b611fdd00e3522183c25d6fb866faf368a7f9ec716e9df4b446d3541cd"}
+```
+
+#### Case 7: Correct cookie value and incorrect parameters passed.
+In the following, correct cookie value was passed with incorrect combination of parameters, but got http code 401.
+```
+ GET /api/consumer/v1/user/current
+ params: {"country_code":91,"raw_phone_number":null}
+ Expected response success: false
+ Incorrect Parameters: {"raw_phone_number":"Value cannot be null"}
+ Response HTTP code: 401
+ API response: {
+ "success": false,
+ "err": {
+ "code": "UNAUTHORIZED",
+ "msg": "Access denied due to invalid credentials.",
+ "error_data": [],
+ "internal_id": "l_ch_vclcr_2"
+ }
+ }
+ Response validation error: {"kind":"unauthorizedApiRequestForValidCookie"}
+ Request headers: {"Cookie":"aulc=25de238247286fa7fc7b9cbd071a45eda87f8690dd524cbf71c104d1d7e8252ef2dc4a698a389368dceb673238ccc47a342b611fdd00e3522183c25d6fb866faf368a7f9ec716e9df4b446d3541cd"}
+```
+
+## Schema Validator
+Using schema validator, the data can be recursively cross validated against its schema.
+
+### Examples
+Get schema validator class using apiSuiteObj.
+```
+ const schemaValidator = apiSuiteObj.SchemaValidator;
+```
+Then call validateObjectBySchema(data,dataSchema,debugLevel) method of schemaValidator class with following arguments:
+1) data = Data to be validated (dataType : object).
+2) dataSchema = Schema against which the given data should be validated. (dataType : object).
+3) debugLevel = String value which helps in debugging (dataType : string).
+
+Note: validateObjectBySchema function throws error only in case of incorrect data.
+#### Case 1: Validate object data.
+```
+ const data = require('./examples/dataToBeValidated_1.json');
+ const dataSchema = require('./examples/dataSchema_1.json');
+ const correctData = data.correctData;
+ const incorrectData = data.incorrectData;
+```
+With correct data
+```
+new schemaValidator().validateObjectBySchema(correctData,dataSchema,'Response');
+```
+With incorrect data
+```
+new schemaValidator().validateObjectBySchema(incorrectData,dataSchema,'Response');
+```
+Logs:
+```
+{
+ kind: 'respEntityTypeMismatch',
+ debugLevel: 'VALUE(Response).status',
+ schemaType: 'string'
+}
+```
+Here kind indicates the type of validation error, debugLevel indicates the level at which the validation failed and schemaType indicates expected schema.
+#### Case 2: Validate array of object data.
+```
+ const data = require('./examples/dataToBeValidated_2.json');
+ const dataSchema = require('./examples/dataSchema_2.json');
+ const correctData = data.correctData;
+ const incorrectData = data.incorrectData;
+```
+```
+new schemaValidator().validateObjectBySchema(incorrectData,dataSchema,'Response');
+```
+Logs:
+```
+{
+ kind: 'respEntityTypeMismatch',
+ debugLevel: 'Response.[0].name',
+ schemaType: 'string'
+}
+```
+#### Case 3: Validate array of integer data.
+```
+ const data = require('./examples/dataToBeValidated_3.json');
+ const dataSchema = require('./examples/dataSchema_3.json');
+
+ const correctData = data.correctData;
+ const incorrectData = data.incorrectData;
+```
+```
+new schemaValidator().validateObjectBySchema(incorrectData,dataSchema,'Response');
+```
+Logs:
+```
+{
+ kind: 'respEntityTypeMismatch',
+ debugLevel: 'Response',
+ schemaType: 'array'
+}
+```
+#### Case 4: Validate string data.
+```
+ const data = require('./examples/dataToBeValidated_4.json');
+ const dataSchema = require('./examples/dataSchema_4.json');
+
+ const correctData = data.correctData;
+ const incorrectData = data.incorrectData;
+```
+```
+new schemaValidator().validateObjectBySchema(incorrectData,dataSchema,'Response');
+```
+Logs:
+```
+{
+ kind: 'respEntityTypeMismatch',
+ debugLevel: 'Response',
+ schemaType: 'string'
+}
+```
+## SQL & Executable Commands Injection.
+Using Start security test you can test your API parameters' vulnerabilities against SQL and Command Injection attacks.
+
+Manual monitoring of API logs is needed to check if any resources are affected by SQL or Commands injection queries.
+
+Execute security test class using apiSuiteObj.
+```
+ apiSuiteObj.runSecurityTest();
+```
+### Example
+API request will be executed for injection queries against each parameter.
+
+```
+GET /api/consumer/v1/signup/phone/otp
+Request headers: {}
+params: {"country_code":"OR 1=1#","raw_phone_number":"1111111111"}
+```
+
+
+## Future Scope
+- Error schema in component section and it's use.
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..3eefcb9
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+1.0.0
diff --git a/examples/dataSchema_1.json b/examples/dataSchema_1.json
new file mode 100644
index 0000000..5c63cbe
--- /dev/null
+++ b/examples/dataSchema_1.json
@@ -0,0 +1,35 @@
+{
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "example": 123,
+ "description": "BE notes: this is the id of users table"
+ },
+ "email": {
+ "type": "string",
+ "example": "david@example.com"
+ },
+ "name": {
+ "type": "string",
+ "example": "David"
+ },
+ "status": {
+ "type": "string",
+ "example": "ACTIVE"
+ },
+ "uts": {
+ "type": "integer",
+ "example": 1651666861
+ }
+ },
+ "required": [
+ "id",
+ "email",
+ "status",
+ "uts"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/examples/dataSchema_2.json b/examples/dataSchema_2.json
new file mode 100644
index 0000000..5025d4d
--- /dev/null
+++ b/examples/dataSchema_2.json
@@ -0,0 +1,29 @@
+{
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "example": 123,
+ "description": "BE notes: this is the id of users table"
+ },
+ "email": {
+ "type": "string",
+ "example": "david@example.com"
+ },
+ "name": {
+ "type": "string",
+ "example": "David"
+ },
+ "status": {
+ "type": "string",
+ "example": "ACTIVE"
+ },
+ "uts": {
+ "type": "integer",
+ "example": 1651666861
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/dataSchema_3.json b/examples/dataSchema_3.json
new file mode 100644
index 0000000..42db1e8
--- /dev/null
+++ b/examples/dataSchema_3.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+}
\ No newline at end of file
diff --git a/examples/dataSchema_4.json b/examples/dataSchema_4.json
new file mode 100644
index 0000000..5651f64
--- /dev/null
+++ b/examples/dataSchema_4.json
@@ -0,0 +1,4 @@
+{
+ "type": "string",
+ "example": "david@example.com"
+}
\ No newline at end of file
diff --git a/examples/dataToBeValidated_1.json b/examples/dataToBeValidated_1.json
new file mode 100644
index 0000000..b81f35a
--- /dev/null
+++ b/examples/dataToBeValidated_1.json
@@ -0,0 +1,34 @@
+{
+ "correctData":{
+ "1": {
+ "id":1,
+ "email":"abc@xyz.com",
+ "name":"mnq",
+ "status":"ACTIVE",
+ "uts":1234567890
+ },
+ "2": {
+ "id":2,
+ "email":"def@xyz.com",
+ "name":"efg",
+ "status":3,
+ "uts":1234567890
+ }
+ },
+ "incorrectData":{
+ "1": {
+ "id":1,
+ "email":"abc@xyz.com",
+ "name":"mnq",
+ "status":"ACTIVE",
+ "uts":1234567890
+ },
+ "2": {
+ "id":2,
+ "email":"def@xyz.com",
+ "name":"efg",
+ "status":"ACTIVE",
+ "uts":1234567890
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/dataToBeValidated_2.json b/examples/dataToBeValidated_2.json
new file mode 100644
index 0000000..7facab3
--- /dev/null
+++ b/examples/dataToBeValidated_2.json
@@ -0,0 +1,32 @@
+{
+ "correctData" : [
+ {
+ "id":1,
+ "email":"abc@xyz.com",
+ "name":"abc",
+ "status":"ACTIVE",
+ "uts":1234567890
+ }, {
+ "id":2,
+ "email":"efg@xyz.com",
+ "name":"efg",
+ "status":"ACTIVE",
+ "uts":1234567890
+ }
+ ],
+ "incorrectData" : [
+ {
+ "id":1,
+ "email":"abc@xyz.com",
+ "name":0,
+ "status":"ACTIVE",
+ "uts":1234567890
+ }, {
+ "id":2,
+ "email":"efg@xyz.com",
+ "name":"efg",
+ "status":"ACTIVE",
+ "uts":1234567890
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/dataToBeValidated_3.json b/examples/dataToBeValidated_3.json
new file mode 100644
index 0000000..935ff0e
--- /dev/null
+++ b/examples/dataToBeValidated_3.json
@@ -0,0 +1,6 @@
+{
+ "correctData": [1,2,3,4,5,6],
+ "incorrectData": {
+ "a": 101
+ }
+}
\ No newline at end of file
diff --git a/examples/dataToBeValidated_4.json b/examples/dataToBeValidated_4.json
new file mode 100644
index 0000000..3c11b53
--- /dev/null
+++ b/examples/dataToBeValidated_4.json
@@ -0,0 +1,8 @@
+{
+ "correctData": {
+ "email": "abc@xyz.com"
+ },
+ "incorrectData": {
+ "email": 1234
+ }
+}
\ No newline at end of file
diff --git a/examples/openapi_1.json b/examples/openapi_1.json
new file mode 100644
index 0000000..22725fb
--- /dev/null
+++ b/examples/openapi_1.json
@@ -0,0 +1,179 @@
+{
+ "definition": {
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Node JS API",
+ "description": "REST API implemented using Node JS",
+ "version": "1.0.0"
+ },
+ "servers": [
+ {
+ "url": "http://localhost:3000",
+ "description": "Local dev server"
+ }
+ ],
+ "components": {
+ "schemas": {
+ "otp_detail": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "example": 1
+ },
+ "sms_identifier": {
+ "type": "string",
+ "example": "1:abc-111-rrr"
+ },
+ "uts": {
+ "type": "integer",
+ "example": 1651666861
+ }
+ },
+ "required": [
+ "id",
+ "sms_identifier",
+ "uts"
+ ]
+ },
+ "current_admin": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "example": 1,
+ "description": "This is the id of admin table"
+ },
+ "first_name": {
+ "type": "string",
+ "example": "Alex"
+ },
+ "last_name": {
+ "type": "string",
+ "example": "Morgan"
+ },
+ "profile_image_id": {
+ "type": "integer",
+ "example": 1651666861,
+ "description": "profile_image_id is images table id"
+ },
+ "default_profile_image_id": {
+ "type": "integer",
+ "example": -1,
+ "description": "default_profile_image_id is static map id (-1,-2)"
+ },
+ "status": {
+ "type": "integer",
+ "example": 1
+ },
+ "uts": {
+ "type": "integer",
+ "example": 1651666861
+ }
+ },
+ "required": [
+ "id",
+ "status",
+ "default_profile_image_id",
+ "uts"
+ ]
+ },
+ "images": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "example": 100000
+ },
+ "resolutions": {
+ "type": "string",
+ "example": "{\"520w\":{\"s\":40330,\"h\":\"520\",\"w\":\"520\"},\"260w\":{\"s\":20391,\"h\":\"260\",\"w\":\"260\"}}",
+ "description": "This is the resolution of image. it contains size, height and width"
+ },
+ "status": {
+ "type": "string",
+ "example": "ACTIVE"
+ },
+ "uts": {
+ "type": "integer",
+ "example": 1651666861
+ }
+ },
+ "required": [
+ "id",
+ "status",
+ "uts"
+ ]
+ },
+ "example": {
+ "100000": {
+ "id": 100000,
+ "resolutions": "{\"520w\":{\"s\":40330,\"h\":\"520\",\"w\":\"520\"},\"260w\":{\"s\":20391,\"h\":\"260\",\"w\":\"260\"}}",
+ "status": "ACTIVE",
+ "uts": 1651666861
+ }
+ }
+ }
+ }
+ },
+ "paths": {
+ "/api/admin/web/login/phone/otp": {
+ "get": {
+ "summary": "Get login otp for admin",
+ "tags": [
+ "Admin Auth"
+ ],
+ "parameters": [
+ {
+ "in": "query string",
+ "name": "country_code",
+ "required": true,
+ "schema": {
+ "type": "number"
+ }
+ },
+ {
+ "in": "query string",
+ "name": "raw_phone_number",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "string",
+ "example": true
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "otp_detail": {
+ "$ref": "#/components/schemas/otp_detail"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "apis": [
+ "./routes/api/admin/web/index"
+ ]
+}
diff --git a/examples/securityScheme.json b/examples/securityScheme.json
new file mode 100644
index 0000000..d4352b6
--- /dev/null
+++ b/examples/securityScheme.json
@@ -0,0 +1,18 @@
+{
+ "adminAuth": {
+ "cookies": {
+ "aulc": "s%3A1%3Aapp%3A1%3Aphone%3A1653897346.477%3A11e495083f8a1bb338ce6e10d9f2eed39252521d84e75aa5bf254c97911d9550.962O00SLpO0mEmsypUGDglrN7wgoItB0EKhSxe6t0c8"
+ },
+ "headers": {
+ "Accept-Encoding": "gzip,deflate,br",
+ "Pragma": "no-cache",
+ "keep-Alive": 300
+ }
+ },
+ "adminAuth2": {
+ "header": {
+ "Accept-Encoding": "gzip,deflate",
+ "Accept-Language": "en-US"
+ }
+ }
+}
diff --git a/index.js b/index.js
index 44ff820..4c7cdf9 100644
--- a/index.js
+++ b/index.js
@@ -1,18 +1,63 @@
const rootPrefix = '.',
- configProvider = require(rootPrefix + '/lib/configProvider');
+ configProvider = require(rootPrefix + '/lib/configProvider'),
+ StartSuite = require(rootPrefix + '/lib/Suite/Start'),
+ StartSecurityTest = require(rootPrefix + '/lib/Suite/StartSecurityTest'),
+ SchemaValidator = require(rootPrefix + '/lib/schema/Validate');
+/**
+ * Class exposed by this package
+ *
+ * @class ApiTestSuite
+ */
class ApiTestSuite {
- constructor(openapiObj,serverIndex) {
+ constructor(openapiObj, serverIndex, securityInfo) {
const oThis = this;
oThis.openapiObj = openapiObj;
oThis.serverIndex = serverIndex;
- configProvider.setConfig('openapiObj', openapiObj);
- configProvider.setConfig('serverIndex', serverIndex);
+ oThis.securityInfo = securityInfo;
+
+ // Saving the params in-memory via configProvider
+ configProvider.setConfig('openapiObj', oThis.openapiObj);
+ configProvider.setConfig('serverIndex', oThis.serverIndex);
+ configProvider.setConfig('securityInfo', oThis.securityInfo);
}
+ /**
+ * Start the suite to run test cases
+ *
+ * @return {Promise}
+ */
runTest() {
- console.log('inside runTest');
+ new StartSuite().perform();
+ }
+
+ /**
+ * Start the suite to run test for security params
+ *
+ * @return {Promise}
+ */
+ runSecurityTest() {
+ new StartSecurityTest().perform();
+ }
+
+ /**
+ * Cleanup the in-memory saved config
+ *
+ * @return {Promise}
+ */
+ cleanup() {
+ configProvider.deleteConfig();
+ }
+
+ /**
+ * Schema Validator to validate data vs dataSchema
+ *
+ * @returns {*}
+ * @constructor
+ */
+ get SchemaValidator() {
+ return SchemaValidator;
}
}
diff --git a/lib/HttpRequest.js b/lib/HttpRequest.js
new file mode 100644
index 0000000..26d8045
--- /dev/null
+++ b/lib/HttpRequest.js
@@ -0,0 +1,183 @@
+const queryString = require('qs'),
+ https = require('https'),
+ http = require('http'),
+ url = require('url');
+
+const rootPrefix = '..',
+ // logger = require(rootPrefix + '/lib/logger/customConsoleLogger'),
+ responseHelper = require(rootPrefix + '/lib/formatter/response');
+
+/**
+ * Class for HTTP request.
+ *
+ * @class HttpRequest
+ */
+class HttpRequest {
+ /**
+ * Constructor for HTTP request.
+ *
+ * @param {object} params
+ * @param {string} params.serverUrl
+ * @param {string} params.path
+ * @param {string} params.method
+ * @param {object} params.apiParameters
+ * @param {object} params.header
+ *
+ * @constructor
+ */
+ constructor(params) {
+ const oThis = this;
+
+ oThis.serverUrl = params.serverUrl;
+ oThis.path = params.path;
+ oThis.method = params.method;
+ oThis.apiParameteres = params.apiParameters;
+ oThis.header = params.header;
+
+ oThis.isJsonResp = false;
+ }
+
+ /**
+ * Async perform.
+ *
+ * @returns {Promise}
+ * @private
+ */
+ async perform() {
+ const oThis = this;
+
+ return oThis._setRequestType();
+ }
+
+ /**
+ * Set request type
+ *
+ * @sets oThis.requestType
+ *
+ */
+ async _setRequestType() {
+ const oThis = this;
+
+ if (oThis.method == 'get') {
+ return oThis._send('GET', oThis.apiParameteres);
+ } else if (oThis.method == 'post') {
+ return oThis._send('POST', oThis.apiParameteres);
+ }
+ }
+
+ /**
+ * Get parsed URL
+ *
+ * @param {string} serverUrl: API server Url
+ *
+ * @return {object} - parsed url object
+ * @private
+ */
+ _parseURL(serverUrl) {
+ return url.parse(serverUrl);
+ }
+
+ /**
+ * Send request.
+ *
+ * @param {string} requestType: API request type
+ * @param {object} queryParams: resource query parameters
+ *
+ * @returns {Promise<*>}
+ * @private
+ */
+ async _send(requestType, queryParams) {
+ const oThis = this;
+
+ const parsedURL = oThis._parseURL(oThis.serverUrl + oThis.path),
+ requestData = oThis.formatQueryParams(queryParams);
+
+ const options = {
+ host: parsedURL.hostname,
+ port: parsedURL.port,
+ path: parsedURL.path,
+ method: requestType,
+ timeout: 300000,
+ headersTimeout: 300000,
+ keepAliveTimeout: 300000
+ };
+
+ options.headers = oThis.header
+ ? oThis.header
+ : {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ };
+
+ if (requestType === 'GET' && requestData) {
+ options.path = options.path + '?' + requestData;
+ } else if (requestType === 'POST' && requestData) {
+ options.headers['Content-Length'] = Buffer.byteLength(requestData);
+ }
+
+ if (parsedURL.auth) {
+ options.auth = parsedURL.auth;
+ }
+
+ return new Promise(function(onResolve, onReject) {
+ let chunkedResponseData = '';
+
+ const request = (parsedURL.protocol === 'https:' ? https : http).request(options, function(response) {
+ response.setEncoding('utf8');
+
+ response.on('data', function(chunk) {
+ chunkedResponseData += chunk;
+ });
+
+ response.on('end', function() {
+ const apiResp = JSON.parse(chunkedResponseData);
+
+ if (typeof apiResp === 'object' && apiResp !== null) {
+ oThis.isJsonResp = true;
+ }
+
+ onResolve(
+ responseHelper.successWithData({
+ rawResponse: response,
+ respHeaders: response.headers,
+ httpCode: response.statusCode,
+ apiResponse: apiResp,
+ isJsonResp: oThis.isJsonResp
+ })
+ );
+ });
+ });
+
+ request.on('error', function(err) {
+ onReject(
+ responseHelper.error({
+ internal_error_identifier: 'l_hr_1',
+ api_error_identifier: 'something_went_wrong',
+ debug_options: { error: err }
+ })
+ );
+ });
+
+ // Write data to server
+ if (requestType === 'POST') {
+ request.write(requestData);
+ }
+
+ request.end();
+ });
+ }
+
+ /**
+ * Format query params.
+ *
+ * @param {object/string} queryParams: query params
+ *
+ * @returns {void | string | never}
+ */
+ formatQueryParams(queryParams) {
+ const oThis = this;
+
+ return queryString.stringify(queryParams).replace(/%20/g, '+');
+ }
+}
+
+module.exports = HttpRequest;
diff --git a/lib/ParameterValue/String.js b/lib/ParameterValue/String.js
deleted file mode 100644
index 7fbcdec..0000000
--- a/lib/ParameterValue/String.js
+++ /dev/null
@@ -1,14 +0,0 @@
-class StringParameterValue {
- correctValue() {
- // returns a random string
- }
-
- incorrectValues() {
- return [
- {
- value: 1,
- description: "Value cannot be interger"
- }
- ];
- }
-}
diff --git a/lib/Suite/Start.js b/lib/Suite/Start.js
index e69de29..0e3a0b3 100644
--- a/lib/Suite/Start.js
+++ b/lib/Suite/Start.js
@@ -0,0 +1,369 @@
+const rootPrefix = '../..',
+ HttpLibrary = require(rootPrefix + '/lib/HttpRequest'),
+ helper = require(rootPrefix + '/lib/helper'),
+ configProvider = require(rootPrefix + '/lib/configProvider'),
+ ParamValueSetFactory = require(rootPrefix + '/lib/paramValueSet/Factory'),
+ logger = require(rootPrefix + '/lib/logger/customConsoleLogger'),
+ CommonValidator = require(rootPrefix + '/lib/validators/Common'),
+ util = require(rootPrefix + '/lib/util');
+
+/**
+ * Class for starting openapi-test-suite
+ *
+ * @class Start
+ */
+class Start {
+ /**
+ * Constructor
+ *
+ * @constructor
+ */
+ constructor(params) {
+ const oThis = this;
+
+ oThis.openapiObj = configProvider.getConfig('openapiObj');
+ oThis.serverIndex = configProvider.getConfig('serverIndex');
+
+ oThis.serverUrl = oThis.openapiObj.definition.servers[oThis.serverIndex].url;
+ oThis.allPaths = oThis.openapiObj.definition.paths;
+ oThis.globalSecurity = oThis.openapiObj.definition.security || {};
+ oThis.securitySchemes = oThis.openapiObj.definition.components.securitySchemes;
+ oThis.securityInfo = configProvider.getConfig('securityInfo');
+ }
+
+ /**
+ * Main performer for class.
+ *
+ * @returns {Promise}
+ */
+ async perform() {
+ const oThis = this;
+
+ await oThis.testAllApis();
+ }
+
+ /**
+ * Test all apis
+ *
+ * @returns {Promise}
+ */
+ async testAllApis() {
+ const oThis = this;
+
+ logger.log('--------------------------- STARTED TESTING APIS --------------------------- ');
+
+ for (const path in oThis.allPaths) {
+ const currentPath = oThis.allPaths[path];
+ for (const method in currentPath) {
+ const methodData = currentPath[method],
+ methodParams = methodData.parameters,
+ methodSecurity = methodData.security || oThis.globalSecurity;
+
+ const paramsCombinations = await oThis.generateApiParamsCombination(methodParams),
+ incorrectCombinations = paramsCombinations.incorrectCombinations,
+ correctCombinations = paramsCombinations.correctCombinations;
+
+ if (CommonValidator.validateNonEmptyObject(methodSecurity)) {
+ // To test each security auth test cases with valid api params
+ const sampleCorrectCombination = correctCombinations[0];
+
+ for (const securityAuth in methodSecurity) {
+ const securityScheme = oThis.securitySchemes[securityAuth];
+ if (!securityScheme) {
+ throw new Error('No security scheme found for - ' + securityAuth);
+ }
+ const securityAuthObj = oThis.securityInfo[securityAuth] || null;
+
+ if (!securityAuthObj) {
+ throw new Error('No security auth found for - ' + securityAuth);
+ }
+
+ // Test with cookie, headers and correct api param combination. Expected http code other than 401.
+ await oThis.sendRequestAndValidateResponse(
+ method,
+ path,
+ sampleCorrectCombination,
+ {
+ areAllParamsCorrect: true,
+ areCookiesIncorrect: false
+ },
+ securityAuthObj.cookies,
+ securityAuthObj.headers
+ );
+
+ // Test with cookie, headers and incorrect api param combinations. Expected http code other than 401 and proper param level error.
+ for (let combinationIndex = 0; combinationIndex < incorrectCombinations.length; combinationIndex++) {
+ const paramsElement = incorrectCombinations[combinationIndex];
+ await oThis.sendRequestAndValidateResponse(
+ method,
+ path,
+ paramsElement,
+ {
+ areAllParamsCorrect: false,
+ areCookiesIncorrect: false
+ },
+ securityAuthObj.cookies,
+ securityAuthObj.headers
+ );
+ }
+
+ const invalidCookiesMap = oThis.generateInvalidCookieValues(securityAuthObj.cookies);
+
+ // Test with invalid cookie values, headers and correct api param combination. Expected http code 401.
+ await oThis.sendRequestAndValidateResponse(
+ method,
+ path,
+ sampleCorrectCombination,
+ {
+ areAllParamsCorrect: true,
+ areCookiesIncorrect: true
+ },
+ invalidCookiesMap,
+ securityAuthObj.headers
+ );
+ }
+
+ // Test with headers, correct api param combination and no cookies. Expected http code 401.
+ await oThis.sendRequestAndValidateResponse(method, path, sampleCorrectCombination, {
+ areAllParamsCorrect: true,
+ areCookiesIncorrect: true
+ });
+ } else {
+ // Test all incorrect api param combinations, expect proper param level error.
+ for (let combinationIndex = 0; combinationIndex < incorrectCombinations.length; combinationIndex++) {
+ const paramsElement = incorrectCombinations[combinationIndex];
+ await oThis.sendRequestAndValidateResponse(method, path, paramsElement, {
+ areAllParamsCorrect: false,
+ areCookiesIncorrect: false
+ });
+ }
+ }
+ }
+ logger.log('--------------------------- ENDED TESTING APIS ---------------------------');
+ }
+ }
+
+ /**
+ * Send request and validate response.
+ *
+ * @param {string} method
+ * @param {string} path
+ * @param {object} paramsCombination
+ * @param {object} validationMeta
+ * @param {object} [cookies]
+ * @param {object} [headers]
+ * @returns {Promise}
+ */
+ async sendRequestAndValidateResponse(
+ method,
+ path,
+ paramsCombination,
+ validationMeta,
+ cookies = null,
+ headers = null
+ ) {
+ const oThis = this;
+ const reqHeaders = Object.assign({}, headers);
+
+ if (CommonValidator.validateNonEmptyObject(cookies)) {
+ reqHeaders['Cookie'] = oThis.getCookieString(cookies);
+ }
+
+ const apiParams = paramsCombination.apiParams;
+ const params = {
+ serverUrl: oThis.serverUrl,
+ path: path,
+ method: method,
+ apiParameters: apiParams,
+ header: CommonValidator.validateNonEmptyObject(reqHeaders) ? reqHeaders : null
+ };
+
+ const responseData = await new HttpLibrary(params).perform();
+ await helper.sleep(500);
+
+ try {
+ const dataSchema = oThis.allPaths[path][method].responses['200'].content['application/json'].schema;
+
+ await oThis.validateResponse(
+ responseData,
+ validationMeta.areAllParamsCorrect,
+ paramsCombination.incorrectParamsMap || {},
+ dataSchema,
+ validationMeta.areCookiesIncorrect
+ );
+ } catch (error) {
+ logger.error('---------------------------');
+ logger.error(method.toUpperCase(), path);
+ logger.error('params:', apiParams);
+ logger.error(
+ 'Expected response success:',
+ validationMeta.areAllParamsCorrect && !validationMeta.areCookiesIncorrect
+ );
+ logger.error('Incorrect Parameters:', paramsCombination.incorrectParamsMap || {});
+ logger.error('Response HTTP code:', responseData.data.httpCode);
+ logger.error('API response:', responseData.data.apiResponse);
+ logger.error('Response validation error:', error);
+ logger.error('Request headers:', reqHeaders);
+ }
+ }
+
+ /**
+ * Validate response.
+ *
+ * @param {object} responseData
+ * @param {boolean} areAllParamsCorrect
+ * @param {object} incorrectParamsMap
+ * @param {object} dataSchema
+ * @param {boolean} areCookiesIncorrect
+ *
+ * @returns {Promise}
+ */
+ async validateResponse(responseData, areAllParamsCorrect, incorrectParamsMap, dataSchema, areCookiesIncorrect) {
+ const oThis = this,
+ apiResponse = responseData.data.apiResponse,
+ statusCode = responseData.data.httpCode;
+
+ if (areAllParamsCorrect) {
+ if (!areCookiesIncorrect && statusCode === 401) {
+ throw {
+ kind: 'unauthorizedApiRequestForValidCookie'
+ };
+ }
+
+ if (areCookiesIncorrect && statusCode != 401) {
+ throw {
+ kind: 'mandatoryCookieValidationFailed'
+ };
+ }
+ } else {
+ if (!areCookiesIncorrect && statusCode == 401) {
+ throw {
+ kind: 'unauthorizedApiRequestForValidCookie'
+ };
+ }
+ const errorData = apiResponse.err.error_data;
+ for (const error of errorData) {
+ const parameter = error.parameter;
+ if (CommonValidator.isVarNullOrUndefined(incorrectParamsMap[parameter])) {
+ throw {
+ kind: 'parameterErrorNotObtained',
+ parameter: parameter
+ };
+ }
+ }
+ }
+ }
+
+ /**
+ * Method to generate combinations of api params.
+ *
+ * @param {array} params
+ * @returns {Promise}
+ */
+ async generateApiParamsCombination(params) {
+ const oThis = this;
+
+ const paramNames = [],
+ allParamsPossibleValuesArray = [],
+ paramNameToValidValues = {},
+ incorrectApiParamsCombinations = [],
+ correctApiParamsCombinations = [];
+
+ if (!params || params.length == 0) {
+ return { incorrectCombinations: incorrectApiParamsCombinations, correctCombinations: [{ apiParams: {} }] };
+ }
+
+ for (const paramObj of params) {
+ const schema = paramObj.schema,
+ paramName = paramObj.name;
+ paramNames.push(paramName);
+
+ const schemaTypeObj = new ParamValueSetFactory().getInstance(schema, paramObj.required),
+ paramCorrectValues = schemaTypeObj.correctValues(),
+ paramIncorrectValues = schemaTypeObj.incorrectValues();
+
+ paramNameToValidValues[paramName] = paramCorrectValues;
+ const possibleParamValues = paramCorrectValues.concat(paramIncorrectValues);
+
+ allParamsPossibleValuesArray.push(possibleParamValues);
+ }
+
+ const allCombinations = helper.cartesianProduct(allParamsPossibleValuesArray);
+
+ for (let index = 0; index < allCombinations.length; index++) {
+ const paramsCombination = allCombinations[index],
+ apiParams = {};
+
+ let areAllParamsCorrect = true;
+ const incorrectParamsMap = {};
+ for (let paramIndex = 0; paramIndex < paramNames.length; paramIndex++) {
+ const paramName = paramNames[paramIndex];
+ const paramValue = paramsCombination[paramIndex].value,
+ paramDescription = paramsCombination[paramIndex].description;
+ apiParams[paramName] = paramValue;
+ const correctValuesArray = paramNameToValidValues[paramName];
+
+ const checkValue = (obj) => obj.value === paramValue;
+
+ if (!correctValuesArray.some(checkValue)) {
+ incorrectParamsMap[paramName] = paramDescription;
+ areAllParamsCorrect = false;
+ }
+ }
+
+ if (areAllParamsCorrect) {
+ correctApiParamsCombinations.push({
+ apiParams: apiParams
+ });
+ } else {
+ incorrectApiParamsCombinations.push({
+ apiParams: apiParams,
+ incorrectParamsMap: incorrectParamsMap
+ });
+ }
+ }
+
+ return { incorrectCombinations: incorrectApiParamsCombinations, correctCombinations: correctApiParamsCombinations };
+ }
+
+ /**
+ * Get cookie string for request headers.
+ *
+ * @param {object} cookiesMap
+ *
+ * @returns {string}
+ */
+ getCookieString(cookiesMap) {
+ const oThis = this;
+
+ const cookieResponseArr = [];
+
+ for (const cookieName in cookiesMap) {
+ const cookieValue = cookiesMap[cookieName];
+ const cookieString = cookieName + '=' + cookieValue;
+ cookieResponseArr.push(cookieString);
+ }
+
+ return cookieResponseArr.join('; ');
+ }
+
+ /**
+ * Get invalid cookie values.
+ *
+ * @param {object} cookiesMap
+ *
+ * @returns {object}
+ */
+ generateInvalidCookieValues(cookiesMap) {
+ const oThis = this;
+
+ const cookieResponseMap = {};
+ for (const cookieName in cookiesMap) {
+ cookieResponseMap[cookieName] = util.getRandomString(cookiesMap[cookieName].length);
+ }
+
+ return cookieResponseMap;
+ }
+}
+
+module.exports = Start;
diff --git a/lib/Suite/StartSecurityTest.js b/lib/Suite/StartSecurityTest.js
new file mode 100644
index 0000000..8360bab
--- /dev/null
+++ b/lib/Suite/StartSecurityTest.js
@@ -0,0 +1,235 @@
+const rootPrefix = '../..',
+ HttpLibrary = require(rootPrefix + '/lib/HttpRequest'),
+ helper = require(rootPrefix + '/lib/helper'),
+ configProvider = require(rootPrefix + '/lib/configProvider'),
+ ParamValueSetFactory = require(rootPrefix + '/lib/paramValueSet/Factory'),
+ securityParamValueSet = require(rootPrefix + '/lib/paramValueSet/securityParamValueSet'),
+ logger = require(rootPrefix + '/lib/logger/customConsoleLogger'),
+ CommonValidator = require(rootPrefix + '/lib/validators/Common');
+
+/**
+ * Class for start security test
+ *
+ * @class StartSecurityTest
+ */
+class StartSecurityTest {
+ /**
+ * Constructor
+ *
+ * @constructor
+ */
+ constructor(params) {
+ const oThis = this;
+
+ oThis.openapiObj = configProvider.getConfig('openapiObj');
+ oThis.serverIndex = configProvider.getConfig('serverIndex');
+ oThis.securityInfo = configProvider.getConfig('securityInfo') || {};
+
+ oThis.serverUrl = oThis.openapiObj.definition.servers[oThis.serverIndex].url;
+ oThis.allPaths = oThis.openapiObj.definition.paths;
+ oThis.globalSecurity = oThis.openapiObj.definition.security;
+ oThis.securitySchemes = oThis.openapiObj.definition.components.securitySchemes;
+ }
+
+ /**
+ * Main performer for class.
+ *
+ * @returns {Promise}
+ */
+ async perform() {
+ const oThis = this;
+
+ await oThis.testAllApis();
+ }
+
+ /**
+ * Test all apis
+ *
+ * @returns {Promise}
+ */
+ async testAllApis() {
+ const oThis = this;
+
+ logger.log('--------------------------- STARTED TESTING APIS FOR SECURITY PARAMS --------------------------- ');
+
+ for (const path in oThis.allPaths) {
+ const currentPath = oThis.allPaths[path];
+ for (const method in currentPath) {
+ const methodData = currentPath[method],
+ methodParams = methodData.parameters,
+ methodSecurity = methodData.security || oThis.globalSecurity || {};
+
+ if (methodParams.length == 0) {
+ logger.info('No parameters for the path --', method.toUpperCase(), path);
+ continue;
+ }
+
+ const paramsArray = await oThis.generateApiParamsCombination(methodParams),
+ incorrectCombinationsArray = paramsArray.incorrectCombinations;
+
+ if (CommonValidator.validateNonEmptyObject(methodSecurity)) {
+ for (const securityAuth in methodSecurity) {
+ const securityScheme = oThis.securitySchemes[securityAuth];
+ if (!securityScheme) {
+ throw new Error('No security scheme found for - ' + securityAuth);
+ }
+ const securityAuthObj = oThis.securityInfo[securityAuth] || null;
+
+ if (!securityAuthObj) {
+ throw new Error('No security auth found for - ' + securityAuth);
+ }
+
+ // Test with cookie, headers and incorrect api param combinations. Expected http code other than 401 and proper param level error.
+ for (let combinationIndex = 0; combinationIndex < incorrectCombinationsArray.length; combinationIndex++) {
+ const paramsElement = incorrectCombinationsArray[combinationIndex];
+ await oThis.sendRequest(method, path, paramsElement, securityAuthObj.cookies, securityAuthObj.headers);
+ }
+ }
+ } else {
+ // Test with invalid cookie values, headers and correct api param combination. Expected http code 401.
+ for (let combinationIndex = 0; combinationIndex < incorrectCombinationsArray.length; combinationIndex++) {
+ const paramsElement = incorrectCombinationsArray[combinationIndex];
+ await oThis.sendRequest(method, path, paramsElement);
+ }
+ }
+ }
+ logger.log('--------------------------- ENDED TESTING APIS FOR SECURITY PARAMS ---------------------------');
+ }
+ }
+
+ /**
+ * Send request and validate response.
+ *
+ * @param {string} method
+ * @param {string} path
+ * @param {object} paramsCombination
+ * @param {object} cookies
+ * @param {object} headers
+ * @returns {Promise}
+ */
+ async sendRequest(method, path, paramsCombination, cookies = null, headers = null) {
+ const oThis = this;
+ const reqHeaders = Object.assign({}, headers);
+
+ if (CommonValidator.validateNonEmptyObject(cookies)) {
+ reqHeaders['Cookie'] = oThis.getCookieString(cookies);
+ }
+
+ const apiParams = paramsCombination.apiParams;
+ const params = {
+ serverUrl: oThis.serverUrl,
+ path: path,
+ method: method,
+ apiParameters: apiParams,
+ header: CommonValidator.validateNonEmptyObject(reqHeaders) ? reqHeaders : null
+ };
+
+ const responseData = await new HttpLibrary(params).perform();
+
+ logger.info('---------------------------');
+ logger.info(method.toUpperCase(), path);
+ logger.info('Request headers:', reqHeaders);
+ logger.info('params:', apiParams);
+ logger.info('API response:', responseData.data.apiResponse);
+
+ await helper.sleep(500);
+ }
+
+ /**
+ * Method to generate combinations of api params.
+ *
+ * @param {array} params
+ * @returns {Promise}
+ */
+ async generateApiParamsCombination(params) {
+ const oThis = this;
+
+ const paramNames = [],
+ allParamsPossibleValuesArray = [],
+ paramNameToValidValues = {},
+ incorrectApiParamsCombinations = [],
+ correctApiParamsCombinations = [];
+
+ if (!params || params.length == 0) {
+ return { incorrectCombinations: incorrectApiParamsCombinations, correctCombinations: [{ apiParams: {} }] };
+ }
+
+ for (const paramObj of params) {
+ const schema = paramObj.schema,
+ paramName = paramObj.name;
+ paramNames.push(paramName);
+
+ const schemaTypeObj = new ParamValueSetFactory().getInstance(schema, paramObj.required),
+ paramCorrectValues = [schemaTypeObj.correctValues()[0]];
+
+ paramNameToValidValues[paramName] = paramCorrectValues;
+ const possibleParamValues = paramCorrectValues;
+
+ allParamsPossibleValuesArray.push(possibleParamValues);
+ }
+
+ const allCombinations = helper.cartesianProduct(allParamsPossibleValuesArray);
+
+ const securityParamValues = new securityParamValueSet().getValues();
+
+ const combinationsArray = helper.prepareCombinationsOfArray(allCombinations[0], securityParamValues);
+
+ for (let index = 0; index < combinationsArray.length; index++) {
+ const paramsCombination = combinationsArray[index],
+ apiParams = {};
+
+ let areAllParamsCorrect = true;
+ const incorrectParamsMap = {};
+ for (let paramIndex = 0; paramIndex < paramNames.length; paramIndex++) {
+ const paramName = paramNames[paramIndex];
+ const paramValue = paramsCombination[paramIndex].value,
+ paramDescription = paramsCombination[paramIndex].description;
+ apiParams[paramName] = paramValue;
+ const correctValuesArray = paramNameToValidValues[paramName];
+
+ const checkValue = (obj) => obj.value === paramValue;
+
+ if (!correctValuesArray.some(checkValue)) {
+ incorrectParamsMap[paramName] = paramDescription;
+ areAllParamsCorrect = false;
+ }
+ }
+
+ if (areAllParamsCorrect) {
+ correctApiParamsCombinations.push({
+ apiParams: apiParams
+ });
+ } else {
+ incorrectApiParamsCombinations.push({
+ apiParams: apiParams,
+ incorrectParamsMap: incorrectParamsMap
+ });
+ }
+ }
+
+ return { incorrectCombinations: incorrectApiParamsCombinations, correctCombinations: correctApiParamsCombinations };
+ }
+
+ /**
+ * Get cookie string for request headers.
+ *
+ * @param {object} cookiesMap
+ *
+ * @returns {string}
+ */
+ getCookieString(cookiesMap) {
+ const oThis = this;
+
+ const cookieResponseArr = [];
+
+ for (const cookieName in cookiesMap) {
+ const cookieValue = cookiesMap[cookieName];
+ const cookieString = cookieName + '=' + cookieValue;
+ cookieResponseArr.push(cookieString);
+ }
+
+ return cookieResponseArr.join('; ');
+ }
+}
+
+module.exports = StartSecurityTest;
diff --git a/lib/configProvider.js b/lib/configProvider.js
index 2b35890..e988832 100644
--- a/lib/configProvider.js
+++ b/lib/configProvider.js
@@ -1,5 +1,10 @@
let _inMemoryConfig = {};
+/**
+ * Class to provide config
+ *
+ * @class ConfigProvider
+ */
class ConfigProvider {
constructor() {
}
diff --git a/lib/formatter/response.js b/lib/formatter/response.js
new file mode 100644
index 0000000..7eb34e8
--- /dev/null
+++ b/lib/formatter/response.js
@@ -0,0 +1,11 @@
+/**
+ * Standard response formatter
+ *
+ * @module lib/formatter/response
+ */
+const Base = require('@plgworks/base'),
+ responseHelper = new Base.responseHelper({
+ module_name: 'openapi-test-suite'
+ });
+
+module.exports = responseHelper;
diff --git a/lib/helper.js b/lib/helper.js
new file mode 100644
index 0000000..b5fd745
--- /dev/null
+++ b/lib/helper.js
@@ -0,0 +1,68 @@
+/**
+ * Class for basic helper methods.
+ *
+ * @class BasicHelper
+ */
+class BasicHelper {
+ /**
+ * Compare two arrays.
+ *
+ * @param {array} firstArray
+ * @param {array} secondArray
+ *
+ * @returns {boolean}
+ */
+ cartesianProduct(inputArray) {
+ var r = [],
+ max = inputArray.length - 1;
+ function helper(arr, i) {
+ for (var j = 0, l = inputArray[i].length; j < l; j++) {
+ var a = arr.slice(0); // clone arr
+ a.push(inputArray[i][j]);
+ if (i == max) r.push(a);
+ else helper(a, i + 1);
+ }
+ }
+ helper([], 0);
+ return r;
+ }
+
+ /**
+ * Prepare Combinations of Array.
+ *
+ * @param {array} firstArray
+ * @param {array} secondArray
+ *
+ * @returns {boolean}
+ */
+ prepareCombinationsOfArray(inputArray1, inputArray2) {
+ const res = [];
+ for (var i = 0; i < inputArray2.length; i++) {
+ const arrVal = inputArray2[i].value;
+ for (var j = 0; j < inputArray1.length; j++) {
+ let arr = JSON.parse(JSON.stringify(inputArray1));
+ arr[j].value = arrVal;
+ res.push(arr);
+ }
+ }
+ return res;
+ }
+
+ /**
+ * Sleep for particular time.
+ *
+ * @param {number} ms: time in ms
+ *
+ * @returns {Promise}
+ */
+ sleep(ms) {
+ // eslint-disable-next-line no-console
+ //logger.log(`Sleeping for ${ms} ms.`);
+
+ return new Promise(function(resolve) {
+ setTimeout(resolve, ms);
+ });
+ }
+}
+
+module.exports = new BasicHelper();
diff --git a/lib/logger/customConsoleLogger.js b/lib/logger/customConsoleLogger.js
new file mode 100644
index 0000000..0dc7f00
--- /dev/null
+++ b/lib/logger/customConsoleLogger.js
@@ -0,0 +1,22 @@
+const PkgBase = require('@plgworks/base'),
+ Logger = PkgBase.Logger;
+
+/**
+ * Class for custom console logger.
+ *
+ * @class LoggerExtended
+ */
+class LoggerExtended extends Logger {}
+
+// Following is to ensure that INFO logs are printed when debug is off.
+let loggerLevel;
+const envVal = 'debug';
+const strEnvVal = String(envVal).toUpperCase();
+
+if (Object.prototype.hasOwnProperty.call(Logger.LOG_LEVELS, strEnvVal)) {
+ loggerLevel = Logger.LOG_LEVELS[strEnvVal];
+} else {
+ loggerLevel = Logger.LOG_LEVELS.INFO;
+}
+
+module.exports = new LoggerExtended('openapi-test-suite', loggerLevel);
diff --git a/lib/paramValueSet/Factory.js b/lib/paramValueSet/Factory.js
new file mode 100644
index 0000000..8d16071
--- /dev/null
+++ b/lib/paramValueSet/Factory.js
@@ -0,0 +1,29 @@
+const rootPrefix = '../..',
+ IntegerParamValueSet = require(rootPrefix + '/lib/paramValueSet/Integer'),
+ StringParamValueSet = require(rootPrefix + '/lib/paramValueSet/String');
+/**
+ * Class for Factory
+ *
+ * @class Factory
+ */
+class Factory {
+ /**
+ * Return instance of schema type
+ *
+ * @returns {*}
+ */
+ getInstance(schema, isMandatory) {
+ const type = schema.type;
+ switch (type) {
+ case 'number':
+ return new IntegerParamValueSet(schema, isMandatory);
+ case 'string':
+ return new StringParamValueSet(schema, isMandatory);
+ default: {
+ throw new Error('Unsupported Param type');
+ }
+ }
+ }
+}
+
+module.exports = Factory;
diff --git a/lib/paramValueSet/integer.js b/lib/paramValueSet/integer.js
new file mode 100644
index 0000000..8a79e04
--- /dev/null
+++ b/lib/paramValueSet/integer.js
@@ -0,0 +1,120 @@
+const rootPrefix = '../..',
+ util = require(rootPrefix + '/lib/util');
+
+/**
+ * Class for getting integer param value set
+ *
+ * @class IntegerParamValueSet
+ */
+class IntegerParamValueSet {
+ /**
+ * Constructor
+ *
+ * @param {object} schema
+ * @param {bool} isMandatory
+ *
+ * @constructor
+ */
+ constructor(schema, isMandatory) {
+ const oThis = this;
+
+ oThis.schema = schema;
+ oThis.isMandatory = isMandatory;
+ }
+
+ /**
+ * Correct values for param int
+ *
+ * @returns {*[]}
+ */
+ correctValues() {
+ const oThis = this;
+
+ const correctValuesArray = [];
+ const minLength = oThis.schema.minLength,
+ maxLength = oThis.schema.maxLength;
+ let paramLength = 0;
+
+ if (minLength && maxLength) {
+ paramLength = Math.floor((minLength + maxLength) / 2);
+ } else if (minLength) {
+ paramLength = minLength;
+ } else if (maxLength) {
+ paramLength = maxLength;
+ }
+
+ const correctValueObj = {
+ value: util.getRandomNumber(paramLength),
+ description: 'Correct random integer'
+ };
+ correctValuesArray.push(correctValueObj);
+
+ if (!oThis.isMandatory) {
+ correctValuesArray.push(oThis.undefinedValueObj());
+ }
+ return correctValuesArray;
+ }
+
+ /**
+ * Incorrect values for param int
+ *
+ * @returns {*[]}
+ */
+ incorrectValues() {
+ const oThis = this;
+
+ const incorrectValuesArray = [];
+ const minLength = oThis.schema.minLength,
+ maxLength = oThis.schema.maxLength;
+
+ if (minLength) {
+ incorrectValuesArray.push({
+ value: util.getRandomNumber(minLength - 1),
+ description: 'Value cannot be smaller than required length'
+ });
+ }
+ if (maxLength) {
+ incorrectValuesArray.push({
+ value: util.getRandomNumber(maxLength + 1),
+ description: 'Value cannot be greater than required length'
+ });
+ }
+ if (oThis.isMandatory) {
+ incorrectValuesArray.push(oThis.nullValueObj());
+ incorrectValuesArray.push(oThis.undefinedValueObj());
+ }
+
+ incorrectValuesArray.push({
+ value: util.getRandomString(5),
+ description: 'Value cannot be string'
+ });
+
+ return incorrectValuesArray;
+ }
+
+ /**
+ * Returns object for undefined value.
+ *
+ * @returns {*}
+ */
+ undefinedValueObj() {
+ return {
+ value: undefined,
+ description: 'Value is not defined'
+ };
+ }
+
+ /**
+ * Returns object for null value.
+ *
+ * @returns {*}
+ */
+ nullValueObj() {
+ return {
+ value: null,
+ description: 'Value cannot be null'
+ };
+ }
+}
+
+module.exports = IntegerParamValueSet;
diff --git a/lib/paramValueSet/securityParamValueSet.js b/lib/paramValueSet/securityParamValueSet.js
new file mode 100644
index 0000000..7c809f4
--- /dev/null
+++ b/lib/paramValueSet/securityParamValueSet.js
@@ -0,0 +1,140 @@
+/**
+ * Class for getting security param value set
+ *
+ * @class SecurityParamValueSet
+ */
+class SecurityParamValueSet {
+ /**
+ * Get values for param int
+ *
+ * @returns {*[]}
+ */
+ getValues() {
+ const oThis = this;
+
+ const valuesArray = [];
+
+ const securityValues = oThis.securityValuesArray();
+
+ for (let index = 0; index < securityValues.length; index++) {
+ const securityVal = securityValues[index];
+
+ valuesArray.push({
+ value: securityVal,
+ description: ''
+ });
+ }
+
+ return valuesArray;
+ }
+
+ /**
+ * Returns security param values array
+ *
+ * @returns {string[]}
+ */
+ // NOTE: Following is list with SQL and command injection queries which will be tested against all api parameters.
+ // More queries can be added according to potential vulnerabilities in project.
+ securityValuesArray() {
+ return [
+ // Auth Bypass SQL Payloads
+ " '-' ",
+ " ' ' ",
+ " '&' ",
+ " '^' ",
+ " '*' ",
+ " ' or ''-' ",
+ " ' or '' ' ",
+ " ' or ''&' ",
+ " ' or ''^' ",
+ " ' or ''*' ",
+
+ // Generic injection payloads
+ ' / ',
+ ' // ',
+ ' ',
+ ' \\ ',
+ ' ; ',
+ ' -- or # ',
+ " ' OR '1 ",
+ " ' OR 1 -- - ",
+ " ' OR '' = ' ",
+ " '=' ",
+ " 'LIKE' ",
+ " '=0--+ ",
+ ' OR 1=1 ',
+ " ' OR 'x'='x ",
+ " ' AND id IS NULL; -- ",
+ " '''''''''''''UNION SELECT '2 ",
+ ' %00 ',
+ ' /*…*/ ',
+ ' + ',
+ ' || ',
+ ' % ',
+ ' @variable ',
+ ' @@variable ',
+ ' ',
+ ' AND 1 ',
+ ' AND 0 ',
+ ' AND true ',
+ ' AND false ',
+ ' 1-false ',
+ ' 1-true ',
+ ' 1*56 ',
+ ' -2 ',
+
+ // Error based SQL Payloads
+ ' OR 1=1 ',
+ ' OR 1=0 ',
+ ' OR x=x ',
+ ' OR 1=1# ',
+ ' OR 1=0# ',
+ ' OR x=y# ',
+ ' OR 1=0-- ',
+ ' OR x=x-- ',
+ ' HAVING 1=1 ',
+ ' HAVING 1=0 ',
+ ' HAVING 1=1# ',
+ ' HAVING 1=0# ',
+ ' AND 1=1 ',
+ ' AND 1=0 ',
+ ' AND 1=1-- ',
+ ' AND 1=0-- ',
+ ' AND 1=1# ',
+ " AND 1=1 AND '%'=' ",
+ " AND 1=0 AND '%'=' ",
+ ' AS INJECTX WHERE 1=1 AND 1=1 ',
+ ' AS INJECTX WHERE 1=1 AND 1=0# ',
+ ' AS INJECTX WHERE 1=1 AND 1=1-- ',
+ ' WHERE 1=1 AND 1=1 ',
+ ' WHERE 1=1 AND 1=0 ',
+ ' WHERE 1=1 AND 1=1# ',
+ ' WHERE 1=1 AND 1=0-- ',
+ ' ORDER BY 1-- ',
+ ' ORDER BY 1# ',
+ ' ORDER BY 1 ',
+
+ // Time Based SQL Injection Payloads
+ ' or SLEEP(5) ',
+ ' pg_SLEEP(5)-- ',
+ ' sleep(5)# ',
+ ' 1 or sleep(5)# ',
+ " ;waitfor delay '0:0:5'-- ",
+ ' benchmark(10000000,MD5(1))# ',
+
+ // Union Select SQL Injection Payloads
+ ' ORDER BY SLEEP(5) ',
+ ' ORDER BY 1,SLEEP(5) ',
+ ' UNION ALL SELECT 1 ',
+ ' UNION ALL SELECT 1,2 ',
+ ' UNION SELECT @@VERSION,SLEEP(5),3 ',
+ ' UNION ALL SELECT NULL# ',
+
+ // Linux command injection payloads
+ 'ls -alt ',
+ 'echo $cmd'
+ ];
+ }
+}
+
+module.exports = SecurityParamValueSet;
diff --git a/lib/paramValueSet/string.js b/lib/paramValueSet/string.js
new file mode 100644
index 0000000..c0cbea0
--- /dev/null
+++ b/lib/paramValueSet/string.js
@@ -0,0 +1,119 @@
+const rootPrefix = '../..',
+ util = require(rootPrefix + '/lib/util');
+
+/**
+ * Class for getting string param value set
+ *
+ * @class StringParamValueSet
+ */
+class StringParamValueSet {
+ /**
+ * Constructor
+ *
+ * @param {object} schema
+ * @param {bool} isMandatory
+ *
+ * @constructor
+ */
+ constructor(schema, isMandatory) {
+ const oThis = this;
+
+ oThis.schema = schema;
+ oThis.isMandatory = isMandatory;
+ }
+
+ /**
+ * Correct values for param string
+ *
+ * @returns {*[]}
+ */
+ correctValues() {
+ const oThis = this;
+
+ const correctValuesArray = [];
+ const minLength = oThis.schema.minLength,
+ maxLength = oThis.schema.maxLength;
+ let paramLength = 0;
+
+ if (minLength && maxLength) {
+ paramLength = Math.floor((minLength + maxLength) / 2);
+ } else if (minLength) {
+ paramLength = minLength;
+ } else if (maxLength) {
+ paramLength = maxLength;
+ }
+
+ const correctValueObj = {
+ value: util.getRandomString(paramLength),
+ description: 'Correct random string'
+ };
+ correctValuesArray.push(correctValueObj);
+
+ if (!oThis.isMandatory) {
+ correctValuesArray.push(oThis.undefinedValueObj());
+ }
+ return correctValuesArray;
+ }
+
+ /**
+ * Incorrect values for param int
+ *
+ * @returns {*[]}
+ */
+ incorrectValues() {
+ const oThis = this;
+
+ const incorrectValuesArray = [];
+ const minLength = oThis.schema.minLength,
+ maxLength = oThis.schema.maxLength;
+
+ if (minLength) {
+ incorrectValuesArray.push({
+ value: util.getRandomString(minLength - 1),
+ description: 'String length cannot be smaller than required length'
+ });
+ }
+ if (maxLength) {
+ incorrectValuesArray.push({
+ value: util.getRandomString(maxLength + 1),
+ description: 'String length cannot be greater than required length'
+ });
+ }
+ if (oThis.isMandatory) {
+ incorrectValuesArray.push(oThis.nullValueObj());
+ incorrectValuesArray.push(oThis.undefinedValueObj());
+ }
+
+ incorrectValuesArray.push({
+ value: util.getRandomNumber(5),
+ description: 'Value cannot be integer'
+ });
+
+ return incorrectValuesArray;
+ }
+
+ /**
+ * Returns object for undefined value.
+ *
+ * @returns {*}
+ */
+ undefinedValueObj() {
+ return {
+ value: undefined,
+ description: 'Value is not defined'
+ };
+ }
+
+ /**
+ * Returns object for null value.
+ *
+ * @returns {*}
+ */
+ nullValueObj() {
+ return {
+ value: null,
+ description: 'Value cannot be null'
+ };
+ }
+}
+module.exports = StringParamValueSet;
diff --git a/lib/schema/Validate.js b/lib/schema/Validate.js
new file mode 100644
index 0000000..fc87ff8
--- /dev/null
+++ b/lib/schema/Validate.js
@@ -0,0 +1,125 @@
+const rootPrefix = '../..',
+ CommonValidators = require(rootPrefix + '/lib/validators/Common'),
+ configProvider = require(rootPrefix + '/lib/configProvider');
+
+/**
+ * Class for schema validation
+ *
+ * @class Validate
+ */
+class Validate {
+ /**
+ * Constructor
+ *
+ * @constructor
+ */
+ constructor() {
+ const oThis = this;
+
+ oThis.openapiObj = configProvider.getConfig('openapiObj');
+ }
+
+ /**
+ * Validate object by schema
+ *
+ * @param {object|array} dataToBeValidated
+ * @param {object} dataSchema
+ * @param {string} debugLevel
+ *
+ * @returns {Promise}
+ */
+ validateObjectBySchema(dataToBeValidated, dataSchema, debugLevel) {
+ const oThis = this;
+
+ // If schema is referring to components section, fetch the actual schema.
+ if (dataSchema.$ref) {
+ const reference = dataSchema.$ref;
+ const refArray = reference.split('/');
+ const schemaName = refArray[refArray.length - 1];
+ dataSchema = oThis.openapiObj.definition.components.schemas[schemaName];
+ }
+
+ if (dataSchema.type === 'integer') {
+ // Recursion base case 1: if type is integer.
+ if (!CommonValidators.validateInteger(dataToBeValidated)) {
+ // eslint-disable-next-line no-throw-literal
+ throw {
+ kind: 'respEntityTypeMismatch',
+ debugLevel: debugLevel,
+ schemaType: dataSchema.type
+ };
+ }
+ } else if (dataSchema.type === 'string') {
+ // Recursion base case 2: if type is string.
+ if (!CommonValidators.validateString(dataToBeValidated)) {
+ // eslint-disable-next-line no-throw-literal
+ throw {
+ kind: 'respEntityTypeMismatch',
+ debugLevel: debugLevel,
+ schemaType: dataSchema.type
+ };
+ }
+ } else if (dataSchema.type === 'boolean') {
+ // Recursion base case 3: if type is boolean.
+ if (!CommonValidators.validateBoolean(dataToBeValidated)) {
+ // eslint-disable-next-line no-throw-literal
+ throw {
+ kind: 'respEntityTypeMismatch',
+ debugLevel: debugLevel,
+ schemaType: dataSchema.type
+ };
+ }
+ } else if (dataSchema.type === 'object') {
+ // If type is object, then check schema for keys and values as well.
+ if (!CommonValidators.validateObject(dataToBeValidated)) {
+ // eslint-disable-next-line no-throw-literal
+ throw {
+ kind: 'respEntityTypeMismatch',
+ debugLevel: debugLevel,
+ schemaType: dataSchema.type
+ };
+ }
+ const requiredPropertiesArr = dataSchema.required || [];
+ const requiredPropertiesMap = {};
+ for (const prop of requiredPropertiesArr) {
+ requiredPropertiesMap[prop] = 1;
+ }
+
+ if (dataSchema.hasOwnProperty('additionalProperties')) {
+ for (const prop in dataToBeValidated) {
+ if (CommonValidators.isVarNullOrUndefined(dataToBeValidated[prop]) && !requiredPropertiesMap[prop]) {
+ continue;
+ }
+ oThis.validateObjectBySchema(
+ dataToBeValidated[prop],
+ dataSchema.additionalProperties,
+ `VALUE(${debugLevel})`
+ );
+ }
+ } else if (dataSchema.hasOwnProperty('properties')) {
+ for (const prop in dataToBeValidated) {
+ if (CommonValidators.isVarNullOrUndefined(dataToBeValidated[prop]) && !requiredPropertiesMap[prop]) {
+ continue;
+ }
+ oThis.validateObjectBySchema(dataToBeValidated[prop], dataSchema.properties[prop], `${debugLevel}.${prop}`);
+ }
+ }
+ } else if (dataSchema.type === 'array') {
+ // If type is array, then check schema for each element.
+ if (!CommonValidators.validateArray(dataToBeValidated)) {
+ // eslint-disable-next-line no-throw-literal
+ throw {
+ kind: 'respEntityTypeMismatch',
+ debugLevel: debugLevel,
+ schemaType: dataSchema.type
+ };
+ }
+
+ for (let index = 0; index < dataToBeValidated.length; index++) {
+ oThis.validateObjectBySchema(dataToBeValidated[index], dataSchema.items, `${debugLevel}.[${index}]`);
+ }
+ }
+ }
+}
+
+module.exports = Validate;
diff --git a/lib/util.js b/lib/util.js
new file mode 100644
index 0000000..c299867
--- /dev/null
+++ b/lib/util.js
@@ -0,0 +1,34 @@
+const Crypto = require('crypto');
+
+/**
+ * Class for utility functions.
+ *
+ * @class Util
+ */
+class Util {
+ /**
+ * Get random string.
+ *
+ * @param {number} stringLength
+ *
+ * @returns {string}
+ */
+ getRandomString(stringLength) {
+ const iv = new Buffer.from(crypto.randomBytes(stringLength));
+
+ return iv.toString('hex').slice(0, stringLength);
+ }
+
+ /**
+ * Get random number of given length.
+ *
+ * @param {number} length
+ *
+ * @returns {number}
+ */
+ getRandomNumber(length) {
+ return Math.floor(Math.pow(10, length - 1) + Math.random() * (Math.pow(10, length) - Math.pow(10, length - 1) - 1));
+ }
+}
+
+module.exports = new Util();
diff --git a/lib/validators/Common.js b/lib/validators/Common.js
new file mode 100644
index 0000000..a72ed5d
--- /dev/null
+++ b/lib/validators/Common.js
@@ -0,0 +1,168 @@
+const BigNumber = require('bignumber.js');
+
+/**
+ * Class for common validators.
+ *
+ * @class CommonValidator
+ */
+class CommonValidator {
+ /**
+ * Is var integer?
+ *
+ * @returns {boolean}
+ */
+ static validateInteger(variable) {
+ try {
+ const variableInBn = new BigNumber(String(variable));
+ // Variable is integer and its length is less than 37 digits
+ if (variableInBn.isInteger() && variableInBn.toString(10).length <= 37) {
+ return true;
+ }
+ } catch (e) {}
+
+ return false;
+ }
+
+ /**
+ * Is var float?
+ *
+ * @returns {boolean}
+ */
+ static validateFloat(variable) {
+ try {
+ const variableInBn = new BigNumber(String(variable));
+ // Variable is float and its length is less than 37 digits
+ if (!variableInBn.isNaN() && variableInBn.toString(10).length <= 37) {
+ return true;
+ }
+ } catch (e) {}
+
+ return false;
+ }
+
+ /**
+ * Is string valid ?
+ *
+ * @returns {boolean}
+ */
+ static validateString(variable) {
+ return typeof variable === 'string';
+ }
+
+ /**
+ * Is valid Boolean?
+ *
+ * @returns {boolean}
+ */
+ static validateBoolean(str) {
+ const oThis = this;
+
+ if (oThis.isVarNullOrUndefined(str)) {
+ return false;
+ }
+
+ return str === 'true' || str === 'false' || str === true || str === false;
+ }
+
+ /**
+ * Is var null or undefined?
+ *
+ * @param {object/string/integer/boolean} variable
+ *
+ * @returns {boolean}
+ */
+ static isVarNullOrUndefined(variable) {
+ return typeof variable === 'undefined' || variable == null;
+ }
+
+ /**
+ * Is valid integer array?
+ *
+ * @param {array} array
+ *
+ * @returns {boolean}
+ */
+ static validateIntegerArray(array) {
+ if (Array.isArray(array)) {
+ for (let index = 0; index < array.length; index++) {
+ if (!CommonValidator.validateInteger(array[index])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Is valid array?
+ *
+ * @param {array} array
+ *
+ * @returns {boolean}
+ */
+ static validateArray(array) {
+ return Array.isArray(array);
+ }
+
+ /**
+ * Validate object.
+ *
+ * @param {object} variable
+ *
+ * @returns {boolean}
+ */
+ static validateObject(variable) {
+ return !(CommonValidator.isVarNullOrUndefined(variable) || typeof variable !== 'object');
+ }
+
+ /**
+ * Validate API validateTransactionStatusArray
+ *
+ * @param {array} array
+ *
+ * @returns {boolean}
+ */
+ static validateStringArray(array) {
+ if (Array.isArray(array)) {
+ for (let index = 0; index < array.length; index++) {
+ if (!CommonValidator.validateString(array[index])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if variable is object and non-empty.
+ *
+ * @param {object} variable
+ *
+ * @returns {boolean}
+ */
+ static validateNonEmptyObject(variable) {
+ if (CommonValidator.isVarNullOrUndefined(variable) || typeof variable !== 'object') {
+ return false;
+ }
+
+ for (const prop in variable) {
+ try {
+ if (Object.prototype.hasOwnProperty.call(variable, prop)) {
+ return true;
+ }
+ } catch (error) {
+ return false;
+ }
+ }
+
+ return false;
+ }
+}
+
+module.exports = CommonValidator;
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4dee101
--- /dev/null
+++ b/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@plgworks/openapi-test-suite",
+ "version": "1.0.0",
+ "description": "DRY API test suite using openapi.json",
+ "main": "index.js",
+ "scripts": {
+ "pre-commit": "lint-staged"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/PLG-Works/openapi-test-suite.git"
+ },
+ "keywords": [
+ "openapi",
+ "test suite"
+ ],
+ "author": "PLG Works",
+ "license": "LGPL-3.0",
+ "bugs": {
+ "url": "https://github.com/PLG-Works/openapi-test-suite/issues"
+ },
+ "homepage": "https://github.com/PLG-Works/openapi-test-suite#readme",
+ "dependencies": {
+ "@plgworks/base": "^1.0.0",
+ "bignumber.js": "9.0.2",
+ "html-entities": "2.3.3",
+ "http": "0.0.1-security",
+ "https": "1.0.0",
+ "qs": "6.10.3",
+ "set-cookie-parser": "2.4.8",
+ "url": "0.11.0"
+ },
+ "devDependencies": {
+ "eslint": "7.20.0",
+ "lint-staged": "10.5.4",
+ "pre-commit": "1.2.2",
+ "prettier": "1.13.7"
+ },
+ "pre-commit": [
+ "pre-commit"
+ ],
+ "lint-staged": {
+ "*.js": [
+ "prettier --write --config .prettierrc.json",
+ "git add"
+ ]
+ }
+}