├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.js ├── .nvmrc ├── .nycrc ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── docs └── README.md ├── lib ├── components │ ├── components.js │ ├── index.js │ ├── parameters │ │ └── structs.js │ ├── parser.js │ ├── responses │ │ └── structs.js │ ├── schemas │ │ └── structs.js │ ├── security-schemes │ │ └── structs.js │ └── structs.js ├── errors │ ├── cli-error-codes.js │ ├── openapi-schema-invalid-error.js │ ├── openapi-schema-malformed-error.js │ ├── openapi-schema-not-found-error.js │ └── parser-error.js ├── external-documentation │ ├── external-documentation.js │ ├── index.js │ ├── parser.js │ └── structs.js ├── info │ ├── index.js │ ├── info.js │ ├── parser.js │ └── structs.js ├── mocker │ └── express │ │ ├── request-handler.js │ │ └── server.js ├── open-api-mocker-cli.js ├── open-api-mocker.js ├── openapi │ ├── index.js │ ├── openapi.js │ ├── parser.js │ └── structs.js ├── paths │ ├── index.js │ ├── parser.js │ ├── path.js │ └── structs.js ├── response-generator │ └── index.js ├── schema-loaders │ ├── explicit-loader.js │ └── local-loader.js ├── schema-validator │ └── index.js ├── security-requirement │ ├── index.js │ ├── parser.js │ ├── security-requirement.js │ └── structs.js ├── servers │ ├── index.js │ ├── parser.js │ ├── server.js │ └── structs.js ├── structs │ └── reference.js ├── tags │ ├── index.js │ ├── parser.js │ ├── structs.js │ └── tag.js └── utils │ ├── enhance-struct-validation-error.js │ ├── extract-extensions.js │ ├── get-faker-locale.js │ ├── http-methods.js │ └── options-builder.js ├── nodemon.json ├── package-lock.json ├── package.json └── tests ├── bootstrap.js ├── components ├── components.js ├── parameters-common.js ├── parameters-cookie.js ├── parameters-header.js ├── parameters-path.js ├── parameters-query.js ├── responses.js ├── security-schemes-api-key.js ├── security-schemes-common.js ├── security-schemes-http.js ├── security-schemes-oauth2.js └── security-schemes-open-id.js ├── errors └── errors.js ├── external-documentation └── external-documentation.js ├── info └── info.js ├── mocker └── express │ └── server.js ├── open-api-mocker.js ├── openapi └── openapi.js ├── paths └── path.js ├── resources └── pet-store.yml ├── response-generator └── index.js ├── schema-loaders └── local-loader.js ├── security-requirement └── security-requirement.js ├── servers └── servers.js ├── tags └── tags.js └── utils └── get-faker-locale.js /.dockerignore: -------------------------------------------------------------------------------- 1 | **/* -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'airbnb-base', 5 | 6 | env: { 7 | node: true, 8 | es6: true, 9 | mocha: true 10 | }, 11 | 12 | parserOptions: { 13 | sourceType: 'script' 14 | }, 15 | 16 | rules: { 17 | strict: ['error', 'global'], 18 | 'operator-linebreak': 0, 19 | 'no-continue': 0, 20 | 'no-plusplus': 0, 21 | 'prefer-spread': 0, 22 | 'prefer-rest-params': 0, 23 | 'class-methods-use-this': 0, 24 | 'consistent-return': 0, 25 | 'prefer-template': 0, 26 | 'import/no-unresolved': 0, 27 | 'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/tests/**/*.js'] }], 28 | 29 | 'no-bitwise': 0, 30 | 31 | curly: ['error', 'multi-or-nest'], 32 | 33 | 'no-underscore-dangle': ['warn', { 34 | allowAfterThis: true, 35 | allowAfterSuper: true, 36 | allow: ['_call', '__rootpath', '_where'] 37 | }], 38 | 39 | 'no-tabs': 0, 40 | 41 | 'no-new': 0, 42 | 43 | 'func-names': 0, 44 | 45 | 'space-before-function-paren': ['error', { 46 | 'anonymous': 'never', 47 | 'named': 'never', 48 | 'asyncArrow': 'always' 49 | }], 50 | 51 | 'arrow-parens': ['error', 'as-needed'], 52 | 'arrow-body-style': 0, 53 | 54 | indent: ['error', 'tab', { 55 | SwitchCase: 1 56 | }], 57 | 58 | 'comma-dangle': ['error', 'never'], 59 | 60 | 'padded-blocks': 0, 61 | 62 | 'max-len': ['error', { 63 | code: 150, 64 | tabWidth: 1, 65 | comments: 200 66 | }], 67 | 68 | 'spaced-comment': ['error', 'always', { 69 | exceptions: ['*'] 70 | }], 71 | 72 | 'newline-per-chained-call': ['error', { 73 | ignoreChainWithDepth: 2 74 | }], 75 | 76 | 'no-param-reassign': 0, 77 | 78 | 'no-prototype-builtins': 0, 79 | 80 | 'keyword-spacing': ['error', { 81 | overrides: { 82 | if: { 83 | after: false 84 | }, 85 | for: { 86 | after: false 87 | }, 88 | while: { 89 | after: false 90 | }, 91 | switch: { 92 | after: false 93 | }, 94 | catch: { 95 | after: false 96 | } 97 | } 98 | }], 99 | 100 | 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], 101 | 'function-paren-newline': 0, 102 | 'no-await-in-loop': 0, 103 | 104 | 'object-curly-newline': ['error', { 105 | ObjectExpression: { minProperties: 5, multiline: true, consistent: true }, 106 | ObjectPattern: { minProperties: 5, multiline: true, consistent: true } 107 | }], 108 | 'nonblock-statement-body-position': ['error', 'below', { overrides: { else: 'any' } }] 109 | } 110 | }; -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '!*' # Do not execute on tags 9 | pull_request: 10 | branches: 11 | - '**' 12 | 13 | jobs: 14 | test: 15 | name: Test in node ${{ matrix.node-version }} 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 16.x, 18.x] 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{matrix.node-version}} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{matrix.node-version}} 26 | - name: Install dependencies 27 | run: npm install 28 | - name: Run tests 29 | run: npm run test 30 | 31 | lint: 32 | name: Lint code 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Use Node.js 18 37 | uses: actions/setup-node@v1 38 | with: 39 | node-version: 18 40 | - name: Install dependencies 41 | run: npm install 42 | - name: Lint code 43 | run: npm run lint 44 | 45 | coverage: 46 | needs: 47 | - test 48 | name: Upload coverage to CodeClimate 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@master 52 | - uses: actions/setup-node@master 53 | with: 54 | node-version: '18' 55 | - name: Install dependencies 56 | run: npm install 57 | - uses: paambaati/codeclimate-action@v2.7.5 58 | env: 59 | CC_TEST_REPORTER_ID: ${{secrets.CODE_CLIMATE_TEST_REPORTER_ID}} 60 | with: 61 | coverageCommand: npm run coverage:ci 62 | coverageLocations: | 63 | ${{github.workspace}}/coverage/lcov.info:lcov 64 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish npm package and docker image 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | publish-npm: 10 | 11 | name: Publish to npm 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js 18.x 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 18.x 20 | registry-url: https://registry.npmjs.org/ 21 | - name: Install dependencies 22 | run: npm install 23 | - name: Publish to npm 24 | uses: JS-DevTools/npm-publish@v1 25 | with: 26 | token: ${{ secrets.NPM_TOKEN }} 27 | access: public 28 | - name: Generate Docker tags 29 | id: dockermeta 30 | uses: crazy-max/ghaction-docker-meta@v2 31 | with: 32 | images: jormaechea/open-api-mocker 33 | tags: | 34 | type=semver,pattern={{version}} 35 | - name: Login to Docker Hub 36 | uses: docker/login-action@v1 37 | with: 38 | username: jormaechea 39 | password: ${{ secrets.DOCKER_TOKEN }} 40 | - name: Publish to Docker Hub 41 | uses: docker/build-push-action@v2 42 | with: 43 | build-args: 44 | open_api_mocker_version=${{ steps.dockermeta.outputs.version }} 45 | context: . 46 | push: true 47 | tags: ${{ steps.dockermeta.outputs.tags }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | .npmrc 3 | node_modules 4 | jspm_packages 5 | 6 | # Test and coverage reports 7 | coverage/ 8 | .nyc_output/ 9 | 10 | # Editor configuration files 11 | .idea/ 12 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npx --no-install lint-staged 4 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | '*.js': ['npm run test', 'eslint --fix'] 5 | }; 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "tests/", 4 | "coverage/", 5 | ".eslintrc.js", 6 | ".lintstagedrc.js" 7 | ], 8 | "extension": [ 9 | ".js" 10 | ], 11 | "cache": true, 12 | "all": true, 13 | "check-coverage": true, 14 | "lines": 90, 15 | "statements": 90, 16 | "functions": 90, 17 | "branches": 90 18 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [2.0.0] - 2023-05-30 10 | ### Added 11 | - Added support for node 16 and 18 12 | 13 | ### Changed 14 | - Moved to new `@faker-js/faker@8` instead of old and nuked `faker` **BREAKING CHANGE** (See [docs](https://fakerjs.dev/) for migration) (#63) 15 | - The `x-faker` property now has priority over `example` when generating responses **BREAKING CHANGE** (#59) 16 | 17 | ### Removed 18 | - Dropped support for node 10 and 12 19 | 20 | ## [1.11.1] - 2022-02-20 21 | ### Fixed 22 | - Responses that are just a number are now properly generated (#56) 23 | 24 | ## [1.11.0] - 2021-12-23 25 | ### Added 26 | - Faker locale validation to avoid setting an invalid locale 27 | 28 | ### Fixed 29 | - Locales without country modifier now work properly (solves docker error with faker) (#55) 30 | 31 | ## [1.10.0] - 2021-12-22 32 | ### Added 33 | - `null` examples are now allowed and used for response generation (#49) 34 | 35 | ## [1.9.0] - 2021-12-20 36 | ### Added 37 | - Response handling for different mime types (#48) 38 | 39 | ### Changed 40 | - Now `x-faker` extension takes precedence in response generation (#53) 41 | 42 | ### Fixed 43 | - Faker now locale selection now works as expected (#50, #54) 44 | 45 | ## [1.8.0] - 2021-10-10 46 | ### Changed 47 | - Now responses with enums pick a random element from the `enum` array 48 | - Now JSON request bodies can handle up to 10mb 49 | 50 | ### Fixed 51 | - Dependencies updated to fix vulnerabilities 52 | 53 | ## [1.7.2] - 2021-06-27 54 | ### Fixed 55 | - In some cases `oneOf`, `anyOf` and `allOf` schemas reported an error that wasn't real. This doesn't happen any more 56 | - Response handling had an error since last release, which has now been fixed. 57 | 58 | ## [1.7.1] - 2021-06-22 59 | ### Fixed 60 | - Response header `x-powered-by` has now the correct value 61 | - Schema Object `pattern` property can be passed as a string (#35) 62 | - Refactor to improve code quality and maintainability (#37) 63 | - Schemas with `oneOf`, `anyOf` and `allOf` now work properly and don't get wrong default values injected (#41) 64 | - Dependencies updated to fix vulnerabilities 65 | 66 | ## [1.7.0] - 2021-04-20 67 | ### Added 68 | - `quiet` (alias `q`) option to avoid printing every request and response (#24) 69 | - CLI: force-quit implemented by pressing `ctrl+c` twice 70 | - Added support for custom Server implementation for programmatic usage (#22) 71 | - Added support for custom Schema loader implementation for programmatic usage (#27) 72 | - Added advanced usage docs 73 | 74 | ### Changed 75 | - Schema loading and watch feature moved out of CLI layer 76 | 77 | ### Fixed 78 | - Updated dependencies to fix vulnerabilities 79 | - Huge refactor to improve code quality and separation of concerns (#26, #27) 80 | 81 | ## [1.6.0] - 2021-03-23 82 | ### Added 83 | - `x-faker` extension is now supported for response generation 84 | - `x-count` extension is now supported for responses with array types 85 | - Added support for more formats defined in the OpenAPI specs 86 | 87 | ### Changed 88 | - Unknown formats don't throw errors any more 89 | 90 | ### Fixed 91 | - `Prefer` header with statusCode now works properly again 92 | - Git hooks are now run again with `husky@4` 93 | - Tests now run properly when local timezone is not UTC 94 | 95 | ## [1.5.1] - 2021-02-12 96 | ### Fixed 97 | - Removed CI/CD for node 8 98 | 99 | ## [1.5.0] - 2021-02-12 100 | ### Added 101 | - Support for response section when `examples` property is present using `Prefer` header 102 | - Support for more content-types: `application/x-www-form-urlencoded`, `text/plain` and `application/octet-stream` 103 | 104 | ### Fixed 105 | - Dependencies update 106 | 107 | ## [1.4.2] - 2021-01-26 108 | ### Fixed 109 | - Fixed exclusiveMinimum and exclusiveMaximum validation 110 | 111 | ## [1.4.1] - 2020-10-01 112 | ### Fixed 113 | - Changed YAML parsing library for better support (#10) 114 | 115 | ## [1.4.0] - 2020-07-03 116 | ### Added 117 | - Added support for path level parameters (#8) 118 | 119 | ### Fixed 120 | - Path objects are now correctly as they can have extra standard and extended properties (#8) 121 | 122 | ## [1.3.1] - 2020-04-25 123 | ### Fixed 124 | - Support for every 3.x.x specification version (#6) 125 | 126 | ## [1.3.0] - 2020-04-05 127 | ### Added 128 | - Support for strings in schemas with format `"password"` 129 | 130 | ### Fixed 131 | - Empty response bodies are now handled properly 132 | 133 | ## [1.2.5] - 2020-03-26 134 | ### Fixed 135 | - Path parameters with underscores are now parsed correctly 136 | - Paths with multiple parameters are now handled properly 137 | 138 | ## [1.2.4] - 2020-02-10 139 | ### Fixed 140 | - Added credentials in CORS configuration 141 | 142 | ## [1.2.3] - 2020-02-09 143 | ### Fixed 144 | - Travis deploy config fixed 145 | 146 | ## [1.2.2] - 2020-02-09 147 | ### Fixed 148 | - OpenAPI naming standarized 149 | - Usage documentation improved 150 | 151 | ## [1.2.1] - 2019-12-30 152 | ### Fixed 153 | - Fix in header validation for case insensitive 154 | 155 | ## [1.2.0] - 2019-12-30 156 | ### Added 157 | - Added option to watch schema changes 158 | 159 | ## [1.1.4] - 2019-12-30 160 | ### Fixed 161 | - CORS configuration improved 162 | 163 | ## [1.1.3] - 2019-09-07 164 | ### Added 165 | - Package json repository links 166 | 167 | ### Fixed 168 | - Security issues with dependencies 169 | 170 | ## [1.1.2] - 2019-08-25 171 | ### Added 172 | - `latest` docker image tag is now generated 173 | 174 | ### Fixed 175 | - SIGINT handle improved 176 | - NotFound response uris fixed 177 | 178 | ## [1.1.1] - 2019-08-25 179 | ### Fixed 180 | - Deployment dependencies fixed 181 | 182 | ## [1.1.0] - 2019-08-25 183 | ### Added 184 | - Installation using npm in README 185 | - Npm and docker releases 186 | - SIGINT handling 187 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | ARG open_api_mocker_version 4 | 5 | LABEL version="$open_api_mocker_version" 6 | 7 | RUN npm i -g open-api-mocker@${open_api_mocker_version} 8 | 9 | WORKDIR /app 10 | 11 | EXPOSE 5000 12 | 13 | ENTRYPOINT ["open-api-mocker"] 14 | 15 | CMD ["-s", "/app/schema.json"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Joaquín Ormaechea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAPI Mocker 2 | 3 | ![Build Status](https://github.com/jormaechea/open-api-mocker/workflows/build/badge.svg) 4 | [![npm version](https://badge.fury.io/js/open-api-mocker.svg)](https://www.npmjs.com/package/open-api-mocker) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/79f6eca7ea3f8fe554c2/maintainability)](https://codeclimate.com/github/jormaechea/open-api-mocker/maintainability) 6 | [![Test Coverage](https://api.codeclimate.com/v1/badges/79f6eca7ea3f8fe554c2/test_coverage)](https://codeclimate.com/github/jormaechea/open-api-mocker/test_coverage) 7 | [![Docker compatible](https://img.shields.io/badge/docker-compatible-green)](https://hub.docker.com/repository/docker/jormaechea/open-api-mocker) 8 | 9 | An API mocker based in the OpenAPI 3.0 specification. 10 | 11 | ## Installation and usage 12 | 13 | ### Using npm 14 | 15 | ``` 16 | npm i -g open-api-mocker 17 | 18 | open-api-mocker -s my-schema.json -w 19 | 20 | open-api-mocker --help # To prompt every available setting. 21 | ``` 22 | 23 | ### Using docker 24 | 25 | ``` 26 | docker run -v "$PWD/myschema.json:/app/schema.json" -p "5000:5000" jormaechea/open-api-mocker 27 | ``` 28 | 29 | Or to run an specific version 30 | 31 | ``` 32 | docker run -v "$PWD/myschema.json:/app/schema.json" -p "5000:5000" jormaechea/open-api-mocker:X.Y.Z` 33 | ``` 34 | 35 | You can set any parameter when running inside a docker container 36 | 37 | ``` 38 | docker run -v "$PWD/myschema.json:/app/schema.json" -p "3000:3000" jormaechea/open-api-mocker:X.Y.Z -s /app/schema.json -p 3000` 39 | ``` 40 | 41 | ## Capabilities 42 | 43 | - [x] Read yaml and json OpenAPI v3 schemas. 44 | - [x] Port binding selection 45 | - [x] Request parameters validation 46 | - [x] Request body validation 47 | - [x] Response body and headers generation based on examples or schemas 48 | - [x] Response selection using request header: `Prefer: statusCode=XXX` or `Prefer: example=name` 49 | - [x] Request and response logging 50 | - [x] Servers basepath support 51 | - [x] Support x-faker and x-count extension methods to customise generated responses 52 | - [ ] API Authentication 53 | 54 | ## Customizing Generated Responses 55 | The OpenAPI specification allows custom properties to be added to an API definition in the form of _x-*_. 56 | OpenAPI Mocker supports the use of two custom extensions to allow data to be randomised which should allow for more 57 | realistic looking data when developing a UI against a mock API for instance. 58 | 59 | ### x-faker 60 | The _x-faker_ extension is valid for use on properties that have a primitive type (e.g. `string`/`integer`, etc.) 61 | and can be used within an API definition to use one or more methods from the community mantained 62 | [Faker](https://fakerjs.dev/) library for generating random data. 63 | 64 | Given the following API definition: 65 | ```yaml 66 | openapi: '3.0.2' 67 | info: 68 | title: Cats 69 | version: '1.0' 70 | servers: 71 | - url: https://api.cats.test/v1 72 | paths: 73 | /cat: 74 | get: 75 | responses: 76 | '200': 77 | description: OK 78 | content: 79 | application/json: 80 | schema: 81 | type: object 82 | properties: 83 | firstName: 84 | type: string 85 | x-faker: person.firstName 86 | lastName: 87 | type: string 88 | x-faker: person.lastName 89 | fullName: 90 | type: string 91 | x-faker: '{{person.firstName}} {{person.lastName}}' 92 | age: 93 | type: string 94 | x-faker: 'number.int({ "min": 1, "max": 20 })' 95 | 96 | ``` 97 | 98 | A JSON response similar to the following would be produced: 99 | ```JSON 100 | { 101 | "firstName": "Ted", 102 | "lastName": "Kozey", 103 | "fullName": "Herbert Lowe", 104 | "age": 12 105 | } 106 | ``` 107 | 108 | The _x-faker_ extension accepts values in 3 forms: 109 | 1. _fakerNamespace.method_. e.g. `string.uuid` 110 | 2. _fakerNamespace.method({ "methodArgs": "in", "json": "format" })_. e.g. `number.int({ "max": 100 })` 111 | 3. A mustache template string making use of the 2 forms above. e.g. `My name is {{person.firstName}} {{person.lastName}}` 112 | 113 | *NOTE*: To avoid new fake data from being generated on every call, up to 10 responses per endpoint are cached 114 | based on the incoming query string, request body and headers. 115 | 116 | ### x-count 117 | The _x-count_ extension has effect only when used on an `array` type property. 118 | If encountered, OpenAPI Mocker will return an array with the given number of elements instead of the default of an 119 | array with a single item. 120 | 121 | For example, the following API definition: 122 | ```yaml 123 | openapi: '3.0.2' 124 | info: 125 | title: Cats 126 | version: '1.0' 127 | servers: 128 | - url: https://api.cats.test/v1 129 | paths: 130 | /cat: 131 | get: 132 | responses: 133 | '200': 134 | description: OK 135 | content: 136 | application/json: 137 | schema: 138 | type: array 139 | x-count: 5 140 | items: 141 | type: string 142 | ``` 143 | 144 | Will produce the following response: 145 | ```JSON 146 | [ 147 | "string", 148 | "string", 149 | "string", 150 | "string", 151 | "string" 152 | ] 153 | ``` 154 | 155 | ## Advanced usage 156 | 157 | See the [advanced usage docs](docs/README.md) to extend or build your own app upon OpenAPI Mocker. 158 | 159 | ## Tests 160 | 161 | Simply run `npm t` 162 | 163 | ## Contributing 164 | 165 | Issues and PRs are welcome. 166 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # OpenAPI Mocker advance usage 2 | 3 | ## Programmatic usage 4 | 5 | Normally, you will use OpenAPI Mocker from the CLI or via Docker. But in case you want to use it from within another APP or to extend it and build your own tooling, you can do it like this: 6 | 7 | ```js 8 | const OpenApiMocker = require('open-api-mocker'); 9 | const mocker = new OpenApiMocker(options); 10 | await mocker.validate(); 11 | await mocker.mock(); 12 | ``` 13 | 14 | #### Available options 15 | 16 | The following table shows the options that can be passed to the constructor: 17 | 18 | | Option | Description | Default value | 19 | | ------ | ----------- | ------------- | 20 | | port | The port the default server will listen to. | `5000` | 21 | | schema | The OpenAPI schema to be mocked. It can be a `string` with the path to the schema or an `object` containing the schema. It can also be set using the `setSchema()` method. | `null` | 22 | | watch | Whether or not the schema should be watched for changes. | `false` | 23 | | server | An instance of a Server implementation. See details [below](#custom-server). | `null`, which means that the built-in server will be used | 24 | | schemaLoader | A SchemaLoader class. See details [below](#custom-schema-loader). | `null`, which means that one of the built-in loaders will be used | 25 | 26 | ## Extending OpenAPI Mocker features 27 | 28 | ### Custom server 29 | 30 | If you don't want to use the default [express](https://www.npmjs.com/package/express) server, you can pass your own implementation through the `server` option. 31 | 32 | Every custom server must implement the following interface: 33 | 34 | ```ts 35 | interface OpenApiServer { 36 | setServers(serverBlockFromSchema: OpenApiServer): OpenApiServer; 37 | setPort(port: number): OpenApiServer; 38 | setPaths(pathsFromSchema: any): OpenApiServer; 39 | init(): Promise; 40 | shutdown(): Promise; 41 | } 42 | ``` 43 | 44 | Then you have to pass an instance of that Server class to the constructor like this: 45 | 46 | ```js 47 | const mocker = new OpenApiMocker({ 48 | server: new MyCustomServer() 49 | }); 50 | ``` 51 | 52 | ### Custom schema loader 53 | 54 | Open API Mocker has two built-in schema loaders: `LocalSchemaLoader` to load a schema from the local filesystem and `ExplicitSchemaLoader` to handle schemas passed as a literal object. 55 | 56 | In case you want to use a custom schema loader, you can pass your implementation through the `schemaLoader` option. 57 | 58 | Custom schema loaders must extend the [`EventEmitter`](https://nodejs.org/api/events.html) class implement the following interface: 59 | 60 | ```ts 61 | interface OpenApiSchemaLoader extends EventEmitter { 62 | // SchemaOption is the value of the `schema` option or the value passed to the `setSchema` method. It's value depends on what your loader needs. 63 | load(schema: SchemaOption): OpenApiSchema|Promise; 64 | } 65 | ``` 66 | 67 | If you want your schema loader to support the watch feature, you have to implement the `watch(): void` method, which will be called once and **must** emit the `schema-changed` event each time you detect a change in the watched schema like this: `this.emit('schema-changed');`. 68 | 69 | Each time you trigger the `schema-changed` event, OpenAPI Mocker will invoke your `load()` method to get the new schema. 70 | 71 | Once you have your schema loader implemented, you have to pass an instance of the SchemaLoader class in the constructor: 72 | 73 | ```js 74 | const mocker = new OpenApiMocker({ 75 | schemaLoader: new MyCustomSchemaLoader() 76 | }); 77 | ``` 78 | -------------------------------------------------------------------------------- /lib/components/components.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Components { 4 | 5 | constructor({ 6 | schemas, 7 | responses, 8 | parameters, 9 | examples, 10 | requestBodies, 11 | headers, 12 | securitySchemes, 13 | links, 14 | callbacks 15 | }, extensionProps = []) { 16 | 17 | this.schemas = schemas; 18 | this.responses = responses; 19 | this.parameters = parameters; 20 | this.examples = examples; 21 | this.requestBodies = requestBodies; 22 | this.headers = headers; 23 | this.securitySchemes = securitySchemes; 24 | this.links = links; 25 | this.callbacks = callbacks; 26 | 27 | this.extensions = {}; 28 | for(const [extensionName, extensionValue] of extensionProps) 29 | this.extensions[extensionName] = extensionValue; 30 | } 31 | 32 | } 33 | 34 | module.exports = Components; 35 | -------------------------------------------------------------------------------- /lib/components/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Parser = require('./parser'); 4 | const Components = require('./components'); 5 | 6 | module.exports = { 7 | Parser, 8 | Components 9 | }; 10 | -------------------------------------------------------------------------------- /lib/components/parameters/structs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { struct } = require('superstruct'); 4 | 5 | const SchemaStruct = require('../schemas/structs'); 6 | const ReferenceStruct = require('../../structs/reference'); 7 | 8 | // const defaultStyles = { 9 | // query: 'form', 10 | // path: 'simple', 11 | // header: 'simple', 12 | // cookie: 'form' 13 | // }; 14 | 15 | const ParameterStruct = struct.intersection([ 16 | 'object', 17 | struct.interface({ 18 | name: 'string', 19 | in: struct.enum(['query', 'header', 'path', 'cookie']), 20 | description: 'string?', 21 | required: struct.dynamic((value, parameter) => (parameter.in === 'path' ? struct.literal(true) : struct('boolean?'))), 22 | deprecated: 'boolean?', 23 | allowEmptyValue: struct.dynamic((value, parameter) => (parameter.in === 'query' ? struct('boolean?') : struct('undefined'))), 24 | style: struct.optional(struct.enum(['form', 'simple', 'matrix', 'label', 'spaceDelimited', 'pipeDelimited', 'deepObject'])), 25 | explode: 'boolean?', 26 | allowReserved: struct.dynamic((value, parameter) => (parameter.in === 'query' ? struct('boolean?') : struct('undefined'))), 27 | schema: struct.optional(struct.union([ 28 | SchemaStruct, 29 | ReferenceStruct 30 | ])), 31 | example: struct.optional('string|number|object|array') 32 | }, { 33 | required: false, 34 | deprecated: false 35 | // @todo Uncomment when superstruct issue #131 is resolved 36 | // allowEmptyValue: parameter => (parameter.in === 'query' ? false : undefined), 37 | // style: parameter => defaultStyles[parameter.in], 38 | // explode: parameter => parameter.style === 'form', 39 | // allowReserved: parameter => (parameter.in === 'query' ? false : undefined) 40 | }) 41 | ]); 42 | 43 | module.exports = ParameterStruct; 44 | -------------------------------------------------------------------------------- /lib/components/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ParserError = require('../errors/parser-error'); 4 | const Components = require('./components'); 5 | const ComponentsStruct = require('./structs'); 6 | const extractExtensions = require('../utils/extract-extensions'); 7 | 8 | class Parser { 9 | 10 | parse(schema) { 11 | 12 | const { components } = schema; 13 | 14 | this.validateComponents(components); 15 | 16 | return this.parseComponents(components || {}); 17 | } 18 | 19 | validateComponents(components) { 20 | 21 | try { 22 | return ComponentsStruct(components); 23 | } catch(e) { 24 | throw new ParserError(e.message, 'components'); 25 | } 26 | } 27 | 28 | parseComponents({ 29 | schemas, 30 | responses, 31 | parameters, 32 | examples, 33 | requestBodies, 34 | headers, 35 | securitySchemes, 36 | links, 37 | callbacks, 38 | ...otherProps 39 | }) { 40 | 41 | const extensionProps = extractExtensions(otherProps); 42 | 43 | return new Components({ 44 | schemas, 45 | responses, 46 | parameters, 47 | examples, 48 | requestBodies, 49 | headers, 50 | securitySchemes, 51 | links, 52 | callbacks 53 | }, extensionProps); 54 | } 55 | 56 | } 57 | 58 | module.exports = Parser; 59 | -------------------------------------------------------------------------------- /lib/components/responses/structs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { struct } = require('superstruct'); 4 | 5 | const SchemaStruct = require('../schemas/structs'); 6 | const ReferenceStruct = require('../../structs/reference'); 7 | 8 | const HeaderStruct = struct.intersection([ 9 | 'object', 10 | struct.interface({ 11 | description: 'string?', 12 | required: struct.optional('boolean'), 13 | deprecated: 'boolean?', 14 | style: struct.optional(struct.enum(['form', 'simple'])), 15 | explode: 'boolean?', 16 | schema: struct.optional(struct.union([ 17 | SchemaStruct, 18 | ReferenceStruct 19 | ])), 20 | example: struct.optional('string|number|object|array') 21 | }, { 22 | required: false, 23 | deprecated: false, 24 | style: 'simple', 25 | explode: false 26 | }) 27 | ]); 28 | 29 | const MediaTypeStruct = struct.interface({ 30 | schema: struct.optional(struct.union([ 31 | SchemaStruct, 32 | ReferenceStruct 33 | ])), 34 | example: struct.optional(struct.union(['string', 'number', 'boolean', 'object', 'array'])), 35 | examples: struct.optional(struct.dict(['string', struct.intersection([ 36 | 'object', 37 | struct.interface({ 38 | summary: 'string?', 39 | description: 'string?', 40 | value: struct.union(['string', 'number', 'boolean', 'object', 'array']), 41 | externalValue: 'string?' 42 | }) 43 | ])])) 44 | }); 45 | 46 | const LinkStruct = struct.intersection([ 47 | 'object', 48 | struct.interface({}) 49 | ]); 50 | 51 | const ResponseStruct = struct.intersection([ 52 | 'object', 53 | struct.interface({ 54 | description: 'string', 55 | headers: struct.optional(struct.dict([ 56 | 'string', 57 | struct.union([ 58 | HeaderStruct, 59 | ReferenceStruct 60 | ]) 61 | ])), 62 | content: struct.optional(struct.dict([ 63 | 'string', 64 | MediaTypeStruct 65 | ])), 66 | links: struct.optional(struct.dict([ 67 | 'string', 68 | LinkStruct 69 | ])) 70 | }) 71 | ]); 72 | 73 | module.exports = ResponseStruct; 74 | -------------------------------------------------------------------------------- /lib/components/schemas/structs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { struct } = require('superstruct'); 4 | 5 | const ExternalDocsStruct = require('../../external-documentation/structs'); 6 | const ReferenceStruct = require('../../structs/reference'); 7 | 8 | const optionalListOf = elementsStruct => struct.optional(struct.list([elementsStruct])); 9 | const optionalUnionOf = validStructs => struct.optional(struct.union(validStructs)); 10 | 11 | const referenceUnion = otherStruct => struct.union([otherStruct, ReferenceStruct]); 12 | 13 | const getDefaultType = ({ allOf, oneOf, anyOf }) => (allOf || oneOf || anyOf ? undefined : 'object'); 14 | 15 | const getDefaultTypeFlags = ({ type, allOf, oneOf, anyOf }) => { 16 | 17 | const finalType = type || getDefaultType({ allOf, oneOf, anyOf }); 18 | 19 | return finalType ? false : undefined; 20 | }; 21 | 22 | const SchemaStruct = struct.intersection([ 23 | 'object', 24 | struct.interface({ 25 | title: 'string?', 26 | multipleOf: 'number?', 27 | maximum: 'number?', 28 | exclusiveMaximum: 'boolean?', 29 | minimum: 'number?', 30 | exclusiveMinimum: 'boolean?', 31 | maxLength: 'number?', 32 | minLength: 'number?', 33 | pattern: 'string|regexp?', 34 | maxItems: 'number?', 35 | minItems: 'number?', 36 | uniqueItems: 'boolean?', 37 | maxProperties: 'number?', 38 | minProperties: 'number?', 39 | required: struct.union(['boolean?', struct.optional(['string'])]), 40 | enum: optionalListOf(struct.union(['string', 'number', 'boolean'])), 41 | type: 'string?', 42 | allOf: struct.lazy(() => optionalListOf(referenceUnion(SchemaStruct))), 43 | oneOf: struct.lazy(() => optionalListOf(referenceUnion(SchemaStruct))), 44 | anyOf: struct.lazy(() => optionalListOf(referenceUnion(SchemaStruct))), 45 | not: struct.lazy(() => struct.optional(referenceUnion(SchemaStruct))), 46 | items: struct.lazy(() => struct.optional(referenceUnion(SchemaStruct))), 47 | properties: struct.lazy(() => struct.optional(struct.dict(['string', referenceUnion(SchemaStruct)]))), 48 | additionalProperties: optionalUnionOf([ 49 | 'boolean', 50 | struct.lazy(() => referenceUnion(SchemaStruct)) 51 | ]), 52 | description: 'string?', 53 | format: 'string?', 54 | default: optionalUnionOf([struct.literal(null), 'string', 'number', 'boolean', 'object', 'array']), 55 | nullable: 'boolean?', 56 | readOnly: 'boolean?', 57 | writeOnly: 'boolean?', 58 | externalDocs: ExternalDocsStruct, 59 | example: optionalUnionOf([struct.literal(null), 'string', 'number', 'boolean', 'object', 'array']), 60 | deprecated: 'boolean?' 61 | }, { 62 | type: getDefaultType, 63 | nullable: getDefaultTypeFlags, 64 | readOnly: getDefaultTypeFlags, 65 | writeOnly: getDefaultTypeFlags, 66 | deprecated: getDefaultTypeFlags 67 | }) 68 | ]); 69 | 70 | module.exports = SchemaStruct; 71 | -------------------------------------------------------------------------------- /lib/components/security-schemes/structs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { struct } = require('superstruct'); 4 | 5 | const baseSchema = { 6 | type: 'string', 7 | description: 'string?' 8 | }; 9 | 10 | const ApiKeyStruct = struct.interface({ 11 | type: struct.literal('apiKey'), 12 | name: 'string', 13 | in: struct.enum(['query', 'header', 'cookie']) 14 | }); 15 | 16 | const HttpStruct = struct.interface({ 17 | type: struct.literal('http'), 18 | scheme: 'string', 19 | bearerFormat: struct.dynamic((value, parent) => (parent.scheme && parent.scheme.toLowerCase() === 'bearer' ? struct('string') : struct('undefined'))) 20 | }); 21 | 22 | const Oauth2BaseStruct = { 23 | refreshUrl: 'string?', 24 | scopes: struct.dict(['string', 'string']) 25 | }; 26 | const Oauth2ImplicitFlowStruct = struct.interface({ 27 | ...Oauth2BaseStruct, 28 | authorizationUrl: 'string' 29 | }); 30 | const Oauth2PasswordFlowStruct = struct.interface({ 31 | ...Oauth2BaseStruct, 32 | tokenUrl: 'string' 33 | }); 34 | const Oauth2ClientCredentialsFlowStruct = struct.interface({ 35 | ...Oauth2BaseStruct, 36 | tokenUrl: 'string' 37 | }); 38 | const Oauth2AuthorizationCodeFlowStruct = struct.interface({ 39 | ...Oauth2BaseStruct, 40 | authorizationUrl: 'string', 41 | tokenUrl: 'string' 42 | }); 43 | 44 | const Oauth2FlowsStruct = { 45 | // This should all be struct.optional(Oauth2{...}FlowStruct) but it breaks in superstruct 0.6.1 46 | implicit: struct.union(['undefined', Oauth2ImplicitFlowStruct]), 47 | password: struct.union(['undefined', Oauth2PasswordFlowStruct]), 48 | clientCredentials: struct.union(['undefined', Oauth2ClientCredentialsFlowStruct]), 49 | authorizationCode: struct.union(['undefined', Oauth2AuthorizationCodeFlowStruct]) 50 | }; 51 | 52 | const Oauth2Struct = struct.interface({ 53 | type: struct.literal('oauth2'), 54 | flows: struct.intersection([ 55 | 'object', 56 | struct.interface(Oauth2FlowsStruct) 57 | ]) 58 | }); 59 | 60 | const OpenIdConnectStruct = struct.interface({ 61 | type: struct.literal('openIdConnect'), 62 | openIdConnectUrl: 'string' 63 | }); 64 | 65 | const SecuritySchemeStruct = struct.intersection([ 66 | 'object', 67 | struct.interface(baseSchema), 68 | struct.union([ 69 | ApiKeyStruct, 70 | HttpStruct, 71 | Oauth2Struct, 72 | OpenIdConnectStruct 73 | ]) 74 | ]); 75 | 76 | module.exports = SecuritySchemeStruct; 77 | -------------------------------------------------------------------------------- /lib/components/structs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { struct } = require('superstruct'); 4 | 5 | const ReferenceStruct = require('../structs/reference'); 6 | 7 | const componentTypes = [ 8 | 'schemas', 9 | 'responses', 10 | 'parameters', 11 | 'examples', 12 | 'requestBodies', 13 | 'headers', 14 | 'securitySchemes', 15 | 'links', 16 | 'callbacks' 17 | ]; 18 | 19 | const componentTypesStruct = {}; 20 | 21 | for(const componentType of componentTypes) { 22 | 23 | let ComponentTypeStruct; 24 | try { 25 | // eslint-disable-next-line global-require, import/no-dynamic-require 26 | ComponentTypeStruct = require(`./${componentType.replace(/([A-Z])/g, l => `-${l.toLowerCase()}`)}/structs`); 27 | } catch(e) { 28 | continue; 29 | } 30 | 31 | componentTypesStruct[componentType] = struct.optional(struct.dict([ 32 | 'string', 33 | struct.union([ 34 | ComponentTypeStruct, 35 | ReferenceStruct 36 | ]) 37 | ])); 38 | } 39 | 40 | const ComponentsStruct = struct.intersection([ 41 | 'object', 42 | struct.interface(componentTypesStruct) 43 | ]); 44 | 45 | module.exports = struct.optional(ComponentsStruct); 46 | -------------------------------------------------------------------------------- /lib/errors/cli-error-codes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | SCHEMA_NOT_FOUND: 1, 5 | SCHEMA_MALFORMED: 2, 6 | SCHEMA_INVALID: 3, 7 | RUNTIME_ERROR: 90 8 | }; 9 | -------------------------------------------------------------------------------- /lib/errors/openapi-schema-invalid-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { SCHEMA_INVALID } = require('./cli-error-codes'); 4 | 5 | module.exports = class OpenAPISchemaInvalid extends Error { 6 | 7 | constructor(message) { 8 | super(message); 9 | this.name = 'OpenAPISchemaInvalid'; 10 | } 11 | 12 | get cliError() { 13 | return SCHEMA_INVALID; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/errors/openapi-schema-malformed-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { SCHEMA_MALFORMED } = require('./cli-error-codes'); 4 | 5 | module.exports = class OpenAPISchemaMalformed extends Error { 6 | 7 | constructor(message) { 8 | super(message); 9 | this.name = 'OpenAPISchemaMalformed'; 10 | } 11 | 12 | get cliError() { 13 | return SCHEMA_MALFORMED; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/errors/openapi-schema-not-found-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { SCHEMA_NOT_FOUND } = require('./cli-error-codes'); 4 | 5 | module.exports = class OpenAPISchemaNotFound extends Error { 6 | 7 | constructor(message) { 8 | super(message); 9 | this.name = 'OpenAPISchemaNotFound'; 10 | } 11 | 12 | get cliError() { 13 | return SCHEMA_NOT_FOUND; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/errors/parser-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class ParserError extends Error { 4 | 5 | constructor(message, errorPath) { 6 | super(`${message} in ${errorPath}`); 7 | } 8 | } 9 | 10 | module.exports = ParserError; 11 | -------------------------------------------------------------------------------- /lib/external-documentation/external-documentation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class ExternalDocumentation { 4 | 5 | constructor({ url, description }, extensionProps = []) { 6 | this.url = url; 7 | this.description = description; 8 | 9 | this.extensions = {}; 10 | for(const [extensionName, extensionValue] of extensionProps) 11 | this.extensions[extensionName] = extensionValue; 12 | } 13 | 14 | } 15 | 16 | module.exports = ExternalDocumentation; 17 | -------------------------------------------------------------------------------- /lib/external-documentation/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Parser = require('./parser'); 4 | const ExternalDocumentation = require('./external-documentation'); 5 | 6 | module.exports = { 7 | Parser, 8 | ExternalDocumentation 9 | }; 10 | -------------------------------------------------------------------------------- /lib/external-documentation/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ExternalDocumentation = require('./external-documentation'); 4 | const ExternalDocumentationsStruct = require('./structs'); 5 | const extractExtensions = require('../utils/extract-extensions'); 6 | const enhanceStructValidationError = require('../utils/enhance-struct-validation-error'); 7 | 8 | class Parser { 9 | 10 | parse(schema) { 11 | 12 | const { externalDocs } = schema; 13 | 14 | this.validateExternalDocumentations(externalDocs); 15 | 16 | return this.parseExternalDocumentation(externalDocs || {}); 17 | } 18 | 19 | validateExternalDocumentations(externalDocs) { 20 | 21 | try { 22 | return ExternalDocumentationsStruct(externalDocs); 23 | } catch(e) { 24 | throw enhanceStructValidationError(e, 'externalDocs'); 25 | } 26 | } 27 | 28 | parseExternalDocumentation({ url, description, ...otherProps }) { 29 | 30 | const extensionProps = extractExtensions(otherProps); 31 | 32 | return new ExternalDocumentation({ 33 | url, 34 | description 35 | }, extensionProps); 36 | } 37 | 38 | } 39 | 40 | module.exports = Parser; 41 | -------------------------------------------------------------------------------- /lib/external-documentation/structs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { struct } = require('superstruct'); 4 | 5 | const ExternalDocsStruct = struct.intersection([ 6 | 'object', 7 | struct.interface({ 8 | url: 'string', 9 | description: 'string?' 10 | }) 11 | ]); 12 | 13 | module.exports = struct.optional(ExternalDocsStruct); 14 | -------------------------------------------------------------------------------- /lib/info/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Parser = require('./parser'); 4 | const Info = require('./info'); 5 | 6 | module.exports = { 7 | Parser, 8 | Info 9 | }; 10 | -------------------------------------------------------------------------------- /lib/info/info.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Info { 4 | 5 | constructor({ title, contact, license, version }, extensionProps = []) { 6 | this.title = title; 7 | this.contact = contact; 8 | this.license = license; 9 | this.version = version; 10 | 11 | this.extensions = {}; 12 | for(const [extensionName, extensionValue] of extensionProps) 13 | this.extensions[extensionName] = extensionValue; 14 | } 15 | 16 | } 17 | 18 | module.exports = Info; 19 | -------------------------------------------------------------------------------- /lib/info/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ParserError = require('../errors/parser-error'); 4 | const Info = require('./info'); 5 | const InfoStruct = require('./structs'); 6 | const extractExtensions = require('../utils/extract-extensions'); 7 | 8 | class Parser { 9 | 10 | parse(schema) { 11 | 12 | const { info } = schema; 13 | 14 | this.validateInfo(info); 15 | 16 | return this.parseInfo(info); 17 | } 18 | 19 | validateInfo(info) { 20 | 21 | try { 22 | return InfoStruct(info); 23 | } catch(e) { 24 | throw new ParserError(e.message, 'info'); 25 | } 26 | } 27 | 28 | parseInfo({ 29 | title, 30 | contact, 31 | license, 32 | version, 33 | ...otherProps 34 | }) { 35 | 36 | const extensionProps = extractExtensions(otherProps); 37 | 38 | return new Info({ 39 | title, 40 | contact, 41 | license, 42 | version 43 | }, extensionProps); 44 | } 45 | 46 | } 47 | 48 | module.exports = Parser; 49 | -------------------------------------------------------------------------------- /lib/info/structs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { struct } = require('superstruct'); 4 | 5 | const ContactStruct = struct.intersection([ 6 | 'object', 7 | struct.interface({ 8 | name: 'string?', 9 | url: 'string?', 10 | email: 'string?' 11 | }) 12 | ]); 13 | 14 | const LicenseStruct = struct.intersection([ 15 | 'object', 16 | struct.interface({ 17 | name: 'string', 18 | url: 'string?' 19 | }) 20 | ]); 21 | 22 | const InfoStruct = struct.intersection([ 23 | 'object', 24 | struct.interface({ 25 | title: 'string', 26 | description: 'string?', 27 | termsOfService: 'string?', 28 | contact: struct.optional(ContactStruct), 29 | license: struct.optional(LicenseStruct), 30 | version: 'string' 31 | }) 32 | ]); 33 | 34 | module.exports = InfoStruct; 35 | -------------------------------------------------------------------------------- /lib/mocker/express/request-handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* istanbul ignore file */ 4 | 5 | const parsePreferHeader = require('parse-prefer-header'); 6 | const memoize = require('micro-memoize'); 7 | const logger = require('lllog')(); 8 | const colors = require('colors'); 9 | 10 | // Create a function that is memoized using the URL, query, the Prefer header and the body. 11 | // eslint-disable-next-line no-unused-vars 12 | const getResponse = (path, url, query, preferHeader, body) => { 13 | const { example: preferredExampleName, statuscode: preferredStatusCode } = parsePreferHeader(preferHeader) || {}; 14 | 15 | if(preferredStatusCode) 16 | logger.debug(`Searching requested response with status code ${preferredStatusCode}`); 17 | else 18 | logger.debug('Searching first response'); 19 | return path.getResponse(preferredStatusCode, preferredExampleName); 20 | }; 21 | 22 | const getResponseMemo = memoize(getResponse, { 23 | maxSize: 10 24 | }); 25 | 26 | const checkContentType = req => { 27 | const contentType = req.header('content-type'); 28 | if(!contentType) 29 | logger.warn(`${colors.yellow('*')} Missing content-type header`); 30 | }; 31 | 32 | const handleRequest = (path, responseHandler) => (req, res) => { 33 | 34 | checkContentType(req); 35 | 36 | const { 37 | query, 38 | params, 39 | headers, 40 | cookies, 41 | body: requestBody 42 | } = req; 43 | 44 | const failedValidations = path.validateRequestParameters({ 45 | query, 46 | path: params, 47 | headers, 48 | cookies, 49 | requestBody 50 | }); 51 | 52 | if(failedValidations.length) 53 | return responseHandler(req, res, { errors: failedValidations }, 400); 54 | 55 | const preferHeader = req.header('prefer') || ''; 56 | 57 | const { statusCode, headers: responseHeaders, body, responseMimeType } = 58 | getResponseMemo(path, req.path, JSON.stringify(req.query), preferHeader, JSON.stringify(requestBody)); 59 | 60 | return responseHandler(req, res, body, statusCode, responseHeaders, responseMimeType); 61 | }; 62 | 63 | module.exports = handleRequest; 64 | -------------------------------------------------------------------------------- /lib/mocker/express/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* istanbul ignore file */ 4 | 5 | const bodyParser = require('body-parser'); 6 | const express = require('express'); 7 | const cors = require('cors'); 8 | const cookieParser = require('cookie-parser'); 9 | const logger = require('lllog')(); 10 | const colors = require('colors'); 11 | const handleRequest = require('./request-handler'); 12 | 13 | const openApiMockSymbol = Symbol('openApiMock'); 14 | 15 | class Server { 16 | 17 | constructor() { 18 | this.servers = []; 19 | this.paths = []; 20 | } 21 | 22 | setServers(servers) { 23 | this.servers = servers; 24 | return this; 25 | } 26 | 27 | setPort(port) { 28 | this.port = port; 29 | return this; 30 | } 31 | 32 | setPaths(paths) { 33 | this.paths = paths; 34 | return this; 35 | } 36 | 37 | async init() { 38 | 39 | if(this.server) 40 | await this.shutdown(); 41 | 42 | const app = express(); 43 | app.use('*', (req, res, next) => { 44 | 45 | res[openApiMockSymbol] = { 46 | initTime: Date.now() 47 | }; 48 | 49 | logger.info(`${colors.yellow('>')} [${req.method}] ${req.originalUrl}`); 50 | 51 | next(); 52 | }); 53 | 54 | app.use( 55 | cookieParser(), 56 | cors({ 57 | origin: true, 58 | credentials: true 59 | }), 60 | bodyParser.json({ limit: '10mb' }), 61 | bodyParser.urlencoded({ limit: '50mb', extended: false, parameterLimit: 50000 }), 62 | bodyParser.text(), 63 | bodyParser.raw() 64 | ); 65 | 66 | this._loadBasePaths(); 67 | 68 | this.setRoutes(app); 69 | 70 | app.all('*', this._notFoundHandler.bind(this)); 71 | 72 | return this.startServer(app); 73 | } 74 | 75 | setRoutes(app) { 76 | this.paths.map(path => this.setRoute(app, path)); 77 | } 78 | 79 | setRoute(app, path) { 80 | 81 | logger.debug(`Processing schema path ${path.httpMethod.toUpperCase()} ${path.uri}`); 82 | 83 | const expressHttpMethod = path.httpMethod.toLowerCase(); 84 | 85 | const uris = this._normalizeExpressPath(path.uri); 86 | 87 | app[expressHttpMethod](uris, handleRequest(path, this.sendResponse)); 88 | 89 | return uris.map(uri => { 90 | return logger.info(`Handling route ${path.httpMethod.toUpperCase()} ${uri}`); 91 | }); 92 | } 93 | 94 | startServer(app) { 95 | return new Promise((resolve, reject) => { 96 | this.server = app.listen(this.port); 97 | 98 | this.server.on('listening', err => { 99 | 100 | if(err) 101 | reject(err); 102 | 103 | const realPort = this.server.address().port; 104 | 105 | logger.info(`Mocking API at http://localhost:${realPort}/`); 106 | 107 | resolve(); 108 | }); 109 | }); 110 | } 111 | 112 | shutdown() { 113 | return new Promise((resolve, reject) => { 114 | logger.debug('Closing express server...'); 115 | this.server.close(err => { 116 | if(err) 117 | return reject(err); 118 | 119 | resolve(); 120 | }); 121 | }); 122 | } 123 | 124 | _loadBasePaths() { 125 | const basePaths = [...new Set(this.servers.map(({ url }) => url.pathname.replace(/\/+$/, '')))]; 126 | 127 | if(basePaths.length) 128 | logger.debug(`Found the following base paths: ${basePaths.join(', ')}`); 129 | 130 | this.basePaths = basePaths.length ? basePaths : ['']; 131 | } 132 | 133 | _notFoundHandler(req, res) { 134 | 135 | const validPaths = []; 136 | for(const { httpMethod, uri: schemaUri } of this.paths) { 137 | 138 | const uris = this._normalizeExpressPath(schemaUri); 139 | 140 | for(const uri of uris) 141 | validPaths.push(`${httpMethod.toUpperCase()} ${uri}`); 142 | } 143 | 144 | return this.sendResponse(req, res, { 145 | message: `Path not found: ${req.originalUrl}`, 146 | paths: validPaths 147 | }, 400); 148 | } 149 | 150 | _normalizeExpressPath(schemaUri) { 151 | const normalizedPath = schemaUri.replace(/\{([a-z0-9_]+)\}/gi, ':$1').replace(/^\/*/, '/'); 152 | 153 | return this.basePaths.map(basePath => `${basePath}${normalizedPath}`); 154 | } 155 | 156 | sendResponse(req, res, body, statusCode, headers, responseMimeType = '') { 157 | 158 | statusCode = statusCode || 200; 159 | headers = headers || {}; 160 | 161 | // HTTP/2 prohibits Connection header 162 | // If this is not set, Chrome keeps the connection open and this prevents watch feature to work properly 163 | // We'll see what we can do when Chrome starts using HTTP/2 164 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection 165 | if(req.httpVersion.startsWith('0') || req.httpVersion.startsWith('1')) 166 | res.set('Connection', 'close'); 167 | 168 | const responseTime = Date.now() - res[openApiMockSymbol].initTime; 169 | 170 | const color = statusCode < 400 ? colors.green : colors.red; 171 | 172 | logger.info(`${color('<')} [${statusCode}] ${JSON.stringify(body)} (${responseTime} ms)`); 173 | 174 | res 175 | .status(statusCode) 176 | .set(headers) 177 | .set('x-powered-by', 'jormaechea/open-api-mocker'); 178 | 179 | const mimeType = responseMimeType ? responseMimeType.toLowerCase() : ''; 180 | 181 | if(mimeType) 182 | res.type(mimeType); 183 | 184 | return res.send(typeof body === 'number' ? body.toString() : body); 185 | } 186 | 187 | } 188 | 189 | module.exports = Server; 190 | -------------------------------------------------------------------------------- /lib/open-api-mocker-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | /* istanbul ignore file */ 6 | 7 | const { argv } = require('yargs') 8 | .option('port', { 9 | alias: 'p', 10 | description: 'The port to bind on', 11 | type: 'number', 12 | default: 5000 13 | }) 14 | .option('schema', { 15 | alias: 's', 16 | description: 'The path of the schema to mock', 17 | type: 'string' 18 | }) 19 | .option('watch', { 20 | type: 'boolean', 21 | alias: 'w', 22 | description: 'Indicates if schema should be watched for changes or not', 23 | default: false 24 | }) 25 | .option('verbose', { 26 | type: 'boolean', 27 | alias: 'v', 28 | conflicts: 'quiet' 29 | }) 30 | .option('quiet', { 31 | type: 'boolean', 32 | alias: 'q', 33 | description: 'Suppress request output, only show errors', 34 | conflicts: 'verbose' 35 | }) 36 | .demandOption('schema') 37 | .help(); 38 | 39 | const logger = require('lllog')(); 40 | 41 | const { RUNTIME_ERROR } = require('./errors/cli-error-codes'); 42 | 43 | if(argv.verbose) 44 | logger.setMinLevel('debug'); 45 | else if(argv.quiet) 46 | logger.setMinLevel('error'); 47 | 48 | const OpenApiMocker = require('./open-api-mocker'); 49 | 50 | (async () => { 51 | 52 | let sigintReceived = false; 53 | 54 | try { 55 | const openApiMocker = new OpenApiMocker({ 56 | port: argv.port, 57 | schema: argv.schema, 58 | watch: !!argv.watch 59 | }); 60 | 61 | await openApiMocker.validate(); 62 | 63 | process.on('SIGINT', async () => { 64 | 65 | if(sigintReceived) 66 | process.exit(0); 67 | 68 | sigintReceived = true; 69 | 70 | logger.info('SIGINT received. Shutting down...'); 71 | logger.info('Press ^C again to force stop.'); 72 | 73 | await openApiMocker.shutdown(); 74 | process.exit(0); 75 | }); 76 | 77 | await openApiMocker.mock(); 78 | 79 | } catch(e) { 80 | logger.fatal(`Error while mocking schema: ${e.message}`); 81 | process.exit(e.cliError || RUNTIME_ERROR); 82 | } 83 | 84 | })(); 85 | -------------------------------------------------------------------------------- /lib/open-api-mocker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jsonRefs = require('json-refs'); 4 | 5 | const { Parser: OpenApiParser } = require('./openapi'); 6 | const { Parser: ServersParser } = require('./servers'); 7 | const { Parser: PathsParser } = require('./paths'); 8 | const LocalSchemaLoader = require('./schema-loaders/local-loader'); 9 | const OpenAPISchemaInvalid = require('./errors/openapi-schema-invalid-error'); 10 | 11 | const optionsBuilder = require('./utils/options-builder'); 12 | const ExplicitSchemaLoader = require('./schema-loaders/explicit-loader'); 13 | 14 | class OpenApiMocker { 15 | 16 | constructor(options) { 17 | this.options = optionsBuilder(options); 18 | 19 | if(this.options.schema) 20 | this.setSchema(this.options.schema); 21 | } 22 | 23 | setSchema(schema) { 24 | 25 | if(!this.schemaLoader) { 26 | if(this.options.schemaLoader) 27 | this.schemaLoader = this.options.schemaLoader; 28 | else if(typeof schema === 'string') 29 | this.schemaLoader = new LocalSchemaLoader(); 30 | else 31 | this.schemaLoader = new ExplicitSchemaLoader(); 32 | } 33 | 34 | this.schema = this.schemaLoader.load(schema); 35 | } 36 | 37 | async validate() { 38 | 39 | // In case schema loader is async and returns a Promise 40 | if(this.schema instanceof Promise) 41 | this.schema = await this.schema; 42 | 43 | try { 44 | const parsedSchemas = await jsonRefs.resolveRefs(this.schema); 45 | 46 | this.schema = parsedSchemas.resolved; 47 | 48 | const openApiParser = new OpenApiParser(); 49 | const openapi = openApiParser.parse(this.schema); 50 | 51 | const serversParser = new ServersParser(); 52 | const servers = serversParser.parse(this.schema); 53 | 54 | const pathsParser = new PathsParser(); 55 | const paths = pathsParser.parse(this.schema); 56 | 57 | this.api = { 58 | openapi, 59 | servers, 60 | paths 61 | }; 62 | } catch(e) { 63 | throw new OpenAPISchemaInvalid(e.message); 64 | } 65 | } 66 | 67 | async mock() { 68 | await this.options.server 69 | .setServers(this.api.servers) 70 | .setPort(this.options.port) 71 | .setPaths(this.api.paths) 72 | .init(); 73 | 74 | if(this.options.watch && this.schemaLoader.watch && !this.options.alreadyWatching) { 75 | this.options.alreadyWatching = true; 76 | this.schemaLoader.on('schema-changed', () => { 77 | this.setSchema(this.options.schema); 78 | this.validate().then(() => this.mock()); 79 | }); 80 | this.schemaLoader.watch(); 81 | } 82 | } 83 | 84 | shutdown() { 85 | return this.options.server.shutdown(); 86 | } 87 | 88 | } 89 | 90 | module.exports = OpenApiMocker; 91 | -------------------------------------------------------------------------------- /lib/openapi/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Parser = require('./parser'); 4 | const Openapi = require('./openapi'); 5 | 6 | module.exports = { 7 | Parser, 8 | Openapi 9 | }; 10 | -------------------------------------------------------------------------------- /lib/openapi/openapi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Openapi { 4 | 5 | constructor(version) { 6 | this.version = version; 7 | } 8 | 9 | } 10 | 11 | module.exports = Openapi; 12 | -------------------------------------------------------------------------------- /lib/openapi/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ParserError = require('../errors/parser-error'); 4 | const Openapi = require('./openapi'); 5 | const OpenapiStruct = require('./structs'); 6 | 7 | class Parser { 8 | 9 | parse(schema) { 10 | 11 | const { openapi } = schema; 12 | 13 | this.validateOpenapi(openapi); 14 | 15 | return this.parseOpenapi(openapi); 16 | } 17 | 18 | validateOpenapi(openapi) { 19 | 20 | try { 21 | return OpenapiStruct(openapi); 22 | } catch(e) { 23 | throw new ParserError(e.message, 'openapi'); 24 | } 25 | } 26 | 27 | parseOpenapi(openapi) { 28 | return new Openapi(openapi); 29 | } 30 | 31 | } 32 | 33 | module.exports = Parser; 34 | -------------------------------------------------------------------------------- /lib/openapi/structs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { superstruct } = require('superstruct'); 4 | 5 | const struct = superstruct({ 6 | types: { 7 | openApi3Version: value => { 8 | if(typeof value !== 'string') 9 | return 'not_a_string'; 10 | 11 | if(!value.match(/^3\.\d+\.\d+$/)) 12 | return 'not_a_valid_version'; 13 | 14 | return true; 15 | } 16 | } 17 | }); 18 | 19 | const OpenapiStruct = struct('openApi3Version'); 20 | 21 | module.exports = OpenapiStruct; 22 | -------------------------------------------------------------------------------- /lib/paths/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Parser = require('./parser'); 4 | const Path = require('./path'); 5 | 6 | module.exports = { 7 | Parser, 8 | Path 9 | }; 10 | -------------------------------------------------------------------------------- /lib/paths/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Path = require('./path'); 4 | const PathsStruct = require('./structs'); 5 | 6 | const { knownHttpMethods } = require('../utils/http-methods'); 7 | const enhanceStructValidationError = require('../utils/enhance-struct-validation-error'); 8 | 9 | class Parser { 10 | 11 | parse(schema) { 12 | 13 | const { paths } = schema; 14 | 15 | if(!paths) 16 | return []; 17 | 18 | this.validatePaths(paths); 19 | 20 | return Object.entries(paths) 21 | .map(([uri, operations]) => this.parsePath(uri, operations)) 22 | .reduce((acum, operations) => (acum.length ? [...acum, ...operations] : operations), []); 23 | } 24 | 25 | validatePaths(paths) { 26 | 27 | try { 28 | return PathsStruct(paths); 29 | } catch(e) { 30 | throw enhanceStructValidationError(e, 'paths'); 31 | } 32 | } 33 | 34 | parsePath(uri, operations) { 35 | 36 | const pathLevelParameters = operations.parameters || []; 37 | 38 | return Object.entries(operations) 39 | .map(([httpMethod, operationData]) => this.parseOperation(uri, httpMethod, operationData, pathLevelParameters)) 40 | .filter(Boolean); 41 | } 42 | 43 | parseOperation(uri, httpMethod, { parameters, requestBody, responses }, pathLevelParameters) { 44 | 45 | if(!knownHttpMethods.includes(httpMethod.toLowerCase())) 46 | return; 47 | 48 | return new Path({ 49 | uri, 50 | httpMethod, 51 | parameters: [ 52 | ...(parameters || []), 53 | ...pathLevelParameters 54 | ], 55 | requestBody, 56 | responses 57 | }); 58 | } 59 | 60 | } 61 | 62 | module.exports = Parser; 63 | -------------------------------------------------------------------------------- /lib/paths/path.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const logger = require('lllog')(); 4 | const util = require('util'); 5 | 6 | const ResponseGenerator = require('../response-generator'); 7 | const SchemaValidator = require('../schema-validator'); 8 | 9 | class Path { 10 | 11 | constructor({ 12 | uri, 13 | httpMethod, 14 | parameters, 15 | requestBody, 16 | responses 17 | }) { 18 | 19 | this.uri = uri; 20 | this.httpMethod = httpMethod; 21 | this.parameters = parameters || []; 22 | this.requestBody = requestBody; 23 | this.responses = responses; 24 | } 25 | 26 | validateRequestParameters({ 27 | headers, 28 | query, 29 | path, 30 | cookies, 31 | requestBody 32 | }) { 33 | 34 | const request = { headers, query, path, cookies }; 35 | 36 | return [ 37 | ...this.validateRequestBody(requestBody), 38 | ...this.parameters 39 | .map(parameter => this.validateParameter(parameter, request)) 40 | .filter(validation => !!validation) 41 | ]; 42 | } 43 | 44 | validateRequestBody(requestBody) { 45 | 46 | if(!this.requestBody) 47 | return []; 48 | 49 | if(!requestBody) { 50 | if(this.requestBody.required) 51 | return ['Missing required request body']; 52 | 53 | // If body wasn't required, then there's no problem 54 | return []; 55 | } 56 | 57 | const { content } = this.requestBody; 58 | 59 | if(!content || !content['application/json'] || !content['application/json'].schema) { 60 | // Cannot validate the body if it's application/json content is not defined 61 | logger.warn('Missing application/json content for request body'); 62 | return []; 63 | } 64 | 65 | // Validate the body 66 | const { schema } = content['application/json']; 67 | try { 68 | const validationErrors = SchemaValidator.validate(requestBody, schema); 69 | 70 | return validationErrors.map(error => { 71 | 72 | const cleanDataPath = error.dataPath.replace(/^\./, ''); 73 | return `Invalid request body:${cleanDataPath && ` '${cleanDataPath}'`} ${error.message}`; 74 | }); 75 | } catch(e) { 76 | logger.debug(e.stack); 77 | return [e.message]; 78 | } 79 | } 80 | 81 | validateParameter(parameter, { headers, query, path, cookies }) { 82 | switch(parameter.in) { 83 | 84 | case 'header': 85 | return this._validateParameter({ 86 | ...parameter, 87 | name: parameter.name.toLowerCase() 88 | }, headers); 89 | 90 | case 'query': 91 | return this._validateParameter(parameter, query); 92 | 93 | case 'path': 94 | return this._validateParameter(parameter, path); 95 | 96 | case 'cookie': 97 | return this._validateParameter(parameter, cookies); 98 | 99 | default: 100 | return `Invalid declaration for ${parameter.in} param ${parameter.name}`; 101 | } 102 | } 103 | 104 | _validateParameter(parameter, requestParameters) { 105 | 106 | const { 107 | in: paramIn, 108 | name, 109 | required, 110 | deprecated 111 | } = parameter; 112 | 113 | if(required && typeof requestParameters[name] === 'undefined') 114 | return `Missing required ${paramIn} param ${name}`; 115 | 116 | // Optional parameters not sent are always valid 117 | if(typeof requestParameters[name] === 'undefined') 118 | return; 119 | 120 | // If a deprecated parameter is received, leave a warning 121 | if(deprecated) 122 | logger.warn(`Using deprecated ${paramIn} param ${name}`); 123 | 124 | return this.validateParameterSchema(parameter, requestParameters[name]); 125 | } 126 | 127 | validateParameterSchema(parameter, value) { 128 | 129 | const { in: paramIn, name, schema } = parameter; 130 | 131 | if(!schema) { 132 | // Cannot validate a parameter if it's schema is not defined 133 | logger.warn(`Missing schema for ${paramIn} param ${name}`); 134 | return; 135 | } 136 | 137 | if(!schema.type) { 138 | logger.warn(`Missing schema type for ${paramIn} param ${name}`); 139 | return; 140 | } 141 | 142 | return this.validateParameterType(parameter, value) 143 | || this.validateParameterEnum(parameter, value); 144 | } 145 | 146 | validateParameterType({ in: paramIn, name, schema }, value) { 147 | 148 | try { 149 | const error = this.isValidType(schema.type, value); 150 | 151 | if(error) 152 | return `Invalid ${paramIn} param ${name}. Expected value of type ${schema.type} but received ${util.inspect(value)}`; 153 | 154 | } catch(e) { 155 | return `${e.message} for ${paramIn} param ${name}`; 156 | } 157 | } 158 | 159 | isValidType(type, value) { 160 | switch(type) { 161 | case 'array': 162 | return !Array.isArray(value); 163 | 164 | case 'object': 165 | return typeof value !== 'object' || Array.isArray(value); 166 | 167 | case 'string': 168 | return typeof value !== 'string'; 169 | 170 | case 'number': 171 | return Number.isNaN(Number(value)); 172 | 173 | case 'integer': 174 | return Number.isNaN(Number(value)) || (parseInt(Number(value), 10)) !== Number(value); 175 | 176 | case 'boolean': 177 | return value !== (!!value) && value !== 'true' && value !== 'false'; 178 | 179 | default: 180 | throw new Error(`Invalid type declaration ${type}`); 181 | } 182 | } 183 | 184 | validateParameterEnum({ in: paramIn, name, schema }, value) { 185 | 186 | if(!this.isValidEnumValue(schema.enum, value)) { 187 | 188 | const enumAsString = schema.enum 189 | .map(util.inspect) 190 | .join(', '); 191 | 192 | return `Invalid ${paramIn} param ${name}. Expected enum of [${enumAsString}] but received ${util.inspect(value)}`; 193 | } 194 | } 195 | 196 | isValidEnumValue(possibleValues, value) { 197 | return !possibleValues || !possibleValues.length || possibleValues.includes(value); 198 | } 199 | 200 | getResponse(preferredStatusCode, preferredExampleName) { 201 | 202 | const { 203 | statusCode, 204 | headers, 205 | schema, 206 | responseMimeType 207 | } = preferredStatusCode ? this.getResponseByStatusCode(preferredStatusCode) : this.getFirstResponse(); 208 | 209 | return { 210 | statusCode: Number(statusCode), 211 | headers: headers && this.generateResponseHeaders(headers), 212 | body: schema ? ResponseGenerator.generate(schema, preferredExampleName) : null, 213 | responseMimeType 214 | }; 215 | } 216 | 217 | getResponseByStatusCode(statusCode) { 218 | 219 | if(!this.responses[statusCode]) { 220 | logger.warn(`Could not find a response for status code ${statusCode}. Responding with first response`); 221 | return this.getFirstResponse(); 222 | } 223 | 224 | const preferredResponse = this.responses[statusCode]; 225 | 226 | const [[responseMimeType, responseContent] = []] = Object.entries(preferredResponse.content || {}); 227 | 228 | return { statusCode, schema: responseContent, responseMimeType, headers: preferredResponse.headers }; 229 | } 230 | 231 | getFirstResponse() { 232 | 233 | const [[statusCode, firstResponse]] = Object.entries(this.responses); 234 | const [[responseMimeType, firstResponseContent] = []] = Object.entries(firstResponse.content || {}); 235 | 236 | return { statusCode, schema: firstResponseContent, responseMimeType, headers: firstResponse.headers }; 237 | } 238 | 239 | generateResponseHeaders(headersSchema) { 240 | 241 | const responseHeaders = {}; 242 | 243 | for(const [headerName, headerData] of Object.entries(headersSchema)) 244 | responseHeaders[headerName] = ResponseGenerator.generate(headerData); 245 | 246 | return responseHeaders; 247 | } 248 | 249 | } 250 | 251 | module.exports = Path; 252 | -------------------------------------------------------------------------------- /lib/paths/structs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { struct } = require('superstruct'); 4 | 5 | const ParameterStruct = require('../components/parameters/structs'); 6 | const ResponseStruct = require('../components/responses/structs'); 7 | const ReferenceStruct = require('../structs/reference'); 8 | const SchemaStruct = require('../components/schemas/structs'); 9 | 10 | const { knownHttpMethods } = require('../utils/http-methods'); 11 | 12 | const MediaTypeStruct = struct.interface({ 13 | schema: struct.optional(struct.union([ 14 | SchemaStruct, 15 | ReferenceStruct 16 | ])), 17 | example: struct.optional(struct.union(['string', 'number', 'boolean', 'object', 'array'])) 18 | }); 19 | 20 | const OperationStruct = struct.interface({ 21 | parameters: struct.optional([ParameterStruct]), 22 | responses: struct.dict(['string', ResponseStruct]), 23 | requestBody: struct.optional(struct.union([ 24 | ReferenceStruct, 25 | struct.object({ 26 | description: 'string?', 27 | content: struct.dict(['string', MediaTypeStruct]), 28 | required: 'boolean?' 29 | }, { 30 | required: false 31 | }) 32 | ])) 33 | }); 34 | 35 | const PathStruct = struct.intersection([ 36 | 'object', 37 | struct.interface(knownHttpMethods.reduce((acum, httpMethod) => { 38 | acum[httpMethod] = struct.union(['undefined', OperationStruct]); 39 | return acum; 40 | }, { 41 | parameters: struct.optional([ParameterStruct]) 42 | })) 43 | ]); 44 | 45 | const Paths = struct.dict(['string', PathStruct]); 46 | 47 | module.exports = Paths; 48 | -------------------------------------------------------------------------------- /lib/response-generator/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const logger = require('lllog')(); 4 | const getFakerLocale = require('../utils/get-faker-locale'); 5 | 6 | const faker = getFakerLocale(); 7 | 8 | class ResponseGenerator { 9 | 10 | static generate(schemaResponse, preferredExampleName) { 11 | 12 | if((typeof schemaResponse.example !== 'undefined' && !schemaResponse['x-faker']) || 13 | (schemaResponse.examples && Object.values(schemaResponse.examples).length)) { 14 | const bestExample = this.getBestExample(schemaResponse, preferredExampleName); 15 | if(bestExample !== undefined) 16 | return bestExample; 17 | } 18 | 19 | if(schemaResponse.enum && schemaResponse.enum.length) 20 | return this.generateByEnum(schemaResponse.enum); 21 | 22 | return this.generateBySchema(schemaResponse.schema || schemaResponse); 23 | } 24 | 25 | static getBestExample(schemaResponse, preferredExampleName) { 26 | 27 | if(typeof schemaResponse.example !== 'undefined') 28 | return schemaResponse.example; 29 | if(preferredExampleName && schemaResponse.examples[preferredExampleName] && schemaResponse.examples[preferredExampleName].value) 30 | return schemaResponse.examples[preferredExampleName].value; 31 | if(Object.values(schemaResponse.examples)[0].value) 32 | return Object.values(schemaResponse.examples)[0].value; 33 | } 34 | 35 | static generateByEnum(enumOptions) { 36 | return faker.helpers.arrayElement(enumOptions); 37 | } 38 | 39 | static generateBySchema(schemaResponse) { 40 | 41 | const fakerExtension = schemaResponse['x-faker']; 42 | if(fakerExtension) { 43 | try { 44 | return this.generateByFaker(fakerExtension); 45 | } catch(e) { 46 | logger.warn( 47 | `Failed to generate fake result using ${fakerExtension} schema. Falling back to primitive type.` 48 | ); 49 | } 50 | } 51 | 52 | if(schemaResponse.example) 53 | return schemaResponse.example; 54 | 55 | if(schemaResponse.examples && schemaResponse.examples.length) 56 | return schemaResponse.examples[0]; 57 | 58 | if(schemaResponse.allOf) { 59 | return schemaResponse.allOf.map(oneSchema => this.generate(oneSchema)) 60 | .reduce((acum, oneSchemaValues) => ({ ...acum, ...oneSchemaValues }), {}); 61 | } 62 | 63 | if(schemaResponse.oneOf || schemaResponse.anyOf) 64 | return this.generate((schemaResponse.oneOf || schemaResponse.anyOf)[0]); 65 | 66 | return this.generateByType(schemaResponse); 67 | } 68 | 69 | static generateByType(schemaResponse) { 70 | switch(schemaResponse.type) { 71 | case 'array': 72 | return this.generateArray(schemaResponse); 73 | 74 | case 'object': 75 | return this.generateObject(schemaResponse); 76 | 77 | case 'string': 78 | return this.generateString(schemaResponse); 79 | 80 | case 'number': 81 | return this.generateNumber(schemaResponse); 82 | 83 | case 'integer': 84 | return this.generateInteger(schemaResponse); 85 | 86 | case 'boolean': 87 | return this.generateBoolean(schemaResponse); 88 | 89 | default: 90 | throw new Error('Could not generate response: unknown type'); 91 | } 92 | } 93 | 94 | static generateByFaker(fakerString) { 95 | 96 | // Check if faker string is a template string 97 | if(fakerString.match(/\{\{.+\}\}/)) 98 | return faker.helpers.fake(fakerString); 99 | 100 | const fakerRegex = /^(?\w+)\.(?\w+)(?:\((?.*)\))?$/.exec( 101 | fakerString 102 | ); 103 | 104 | if(!fakerRegex) 105 | throw new Error('Faker extension method is not in the right format. Expecting . or .() format.'); 106 | 107 | const { namespace, method, argsString } = fakerRegex.groups; 108 | 109 | if(!faker[namespace] || !faker[namespace][method]) 110 | throw new Error(`Faker method '${namespace}.${method}' not found`); 111 | 112 | const args = argsString ? JSON.parse(`[${argsString}]`) : []; 113 | 114 | return faker[namespace][method](...args); 115 | } 116 | 117 | static generateArray(schema) { 118 | let count = Number(schema['x-count']); 119 | if(Number.isNaN(count) || count < 1) 120 | count = 1; 121 | 122 | return [...new Array(count)].map(() => this.generate(schema.items)); 123 | } 124 | 125 | static generateObject(schema) { 126 | 127 | const properties = schema.properties || {}; 128 | 129 | return Object.entries(properties) 130 | .map(([property, propertySchema]) => ([property, this.generate(propertySchema)])) 131 | .reduce((acum, [property, value]) => ({ 132 | ...acum, 133 | [property]: value 134 | }), {}); 135 | } 136 | 137 | static generateString() { 138 | return 'string'; 139 | } 140 | 141 | static generateNumber() { 142 | return 1; 143 | } 144 | 145 | static generateInteger() { 146 | return 1; 147 | } 148 | 149 | static generateBoolean() { 150 | return true; 151 | } 152 | 153 | } 154 | 155 | module.exports = ResponseGenerator; 156 | -------------------------------------------------------------------------------- /lib/schema-loaders/explicit-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'); 4 | 5 | module.exports = class ExplicitSchemaLoader extends EventEmitter { 6 | 7 | load(schema) { 8 | return schema; 9 | } 10 | 11 | }; 12 | -------------------------------------------------------------------------------- /lib/schema-loaders/local-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const YAML = require('js-yaml'); 6 | const chokidar = require('chokidar'); 7 | const EventEmitter = require('events'); 8 | const logger = require('lllog')(); 9 | 10 | const OpenAPISchemaNotFound = require('../errors/openapi-schema-not-found-error'); 11 | const OpenAPISchemaMalformed = require('../errors/openapi-schema-malformed-error'); 12 | 13 | module.exports = class LocalSchemaLoader extends EventEmitter { 14 | 15 | load(schemaPath) { 16 | 17 | this.schemaPath = path.isAbsolute(schemaPath) ? schemaPath : path.join(process.cwd(), schemaPath); 18 | 19 | try { 20 | fs.accessSync(this.schemaPath, fs.constants.R_OK); 21 | } catch(e) { 22 | throw new OpenAPISchemaNotFound(`Schema not found in ${this.schemaPath}`); 23 | } 24 | 25 | if(this.schemaPath.match(/\.ya?ml$/)) { 26 | try { 27 | return YAML.load(fs.readFileSync(this.schemaPath)); 28 | } catch(e) { 29 | throw new OpenAPISchemaMalformed(e.message); 30 | } 31 | } 32 | 33 | try { 34 | return JSON.parse(fs.readFileSync(this.schemaPath)); 35 | } catch(e) { 36 | throw new OpenAPISchemaMalformed(e.message); 37 | } 38 | } 39 | 40 | watch() { 41 | logger.info('Watching changes...'); 42 | chokidar.watch(this.schemaPath) 43 | .on('change', () => { 44 | setTimeout(async () => this.emit('schema-changed'), 100); 45 | }); 46 | } 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /lib/schema-validator/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Ajv = require('ajv'); 4 | const openApi = require('ajv-openapi'); 5 | 6 | // Configuration as defined in ajv-openapi example: https://www.npmjs.com/package/ajv-openapi#configuration-for-full-openapi-compatibility 7 | const ajvOptions = { 8 | schemaId: 'auto', 9 | format: 'full', 10 | coerceTypes: true, 11 | unknownFormats: 'ignore', 12 | useDefaults: true 13 | }; 14 | 15 | const openApiOptions = { 16 | useDraft04: true 17 | }; 18 | 19 | const ajv = openApi( 20 | new Ajv(ajvOptions), 21 | openApiOptions 22 | ); 23 | 24 | class SchemaValidator { 25 | static validate(data, schema) { 26 | return !ajv.validate(schema, data) ? ajv.errors : []; 27 | } 28 | } 29 | 30 | module.exports = SchemaValidator; 31 | -------------------------------------------------------------------------------- /lib/security-requirement/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Parser = require('./parser'); 4 | const SecurityRequirement = require('./security-requirement'); 5 | 6 | module.exports = { 7 | Parser, 8 | SecurityRequirement 9 | }; 10 | -------------------------------------------------------------------------------- /lib/security-requirement/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const SecurityRequirement = require('./security-requirement'); 4 | const SecurityRequirementsStruct = require('./structs'); 5 | const enhanceStructValidationError = require('../utils/enhance-struct-validation-error'); 6 | 7 | class Parser { 8 | 9 | parse(schema) { 10 | 11 | const { security } = schema; 12 | 13 | this.validateSecurityRequirements(security); 14 | 15 | if(!security || !security.length) 16 | return []; 17 | 18 | return security 19 | .map(this.parseSecurityRequirement.bind(this)); 20 | } 21 | 22 | validateSecurityRequirements(security) { 23 | 24 | try { 25 | return SecurityRequirementsStruct(security); 26 | } catch(e) { 27 | throw enhanceStructValidationError(e, 'security'); 28 | } 29 | } 30 | 31 | parseSecurityRequirement(requirements) { 32 | return new SecurityRequirement(requirements); 33 | } 34 | 35 | } 36 | 37 | module.exports = Parser; 38 | -------------------------------------------------------------------------------- /lib/security-requirement/security-requirement.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class SecurityRequirement { 4 | 5 | constructor(requirements) { 6 | this.requirements = requirements; 7 | } 8 | 9 | } 10 | 11 | module.exports = SecurityRequirement; 12 | -------------------------------------------------------------------------------- /lib/security-requirement/structs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { struct } = require('superstruct'); 4 | 5 | const SecurityRequirementStruct = struct.dict(['string', ['string']]); 6 | 7 | const SecurityRequirementsStruct = struct.list([SecurityRequirementStruct]); 8 | 9 | module.exports = struct.optional(SecurityRequirementsStruct); 10 | -------------------------------------------------------------------------------- /lib/servers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Parser = require('./parser'); 4 | const Server = require('./server'); 5 | 6 | module.exports = { 7 | Parser, 8 | Server 9 | }; 10 | -------------------------------------------------------------------------------- /lib/servers/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Server = require('./server'); 4 | const ServersStruct = require('./structs'); 5 | const extractExtensions = require('../utils/extract-extensions'); 6 | const enhanceStructValidationError = require('../utils/enhance-struct-validation-error'); 7 | 8 | class Parser { 9 | 10 | parse(schema) { 11 | 12 | const { servers } = schema; 13 | 14 | this.validateServers(servers); 15 | 16 | if(!servers || !servers.length) 17 | return [this.defaultServer()]; 18 | 19 | return servers 20 | .map(this.parseServer.bind(this)); 21 | } 22 | 23 | defaultServer() { 24 | return Server.getDefault(); 25 | } 26 | 27 | validateServers(servers) { 28 | 29 | try { 30 | return ServersStruct(servers); 31 | } catch(e) { 32 | throw enhanceStructValidationError(e, 'servers'); 33 | } 34 | } 35 | 36 | parseServer({ url, description, variables, ...otherProps }) { 37 | 38 | const extensionProps = extractExtensions(otherProps); 39 | 40 | return new Server({ 41 | url, 42 | description, 43 | variables 44 | }, extensionProps); 45 | } 46 | 47 | } 48 | 49 | module.exports = Parser; 50 | -------------------------------------------------------------------------------- /lib/servers/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { URL } = require('url'); 4 | 5 | class Server { 6 | 7 | static getDefault() { 8 | return new Server({ 9 | url: '/' 10 | }); 11 | } 12 | 13 | static get defaultUrlBase() { 14 | return 'http://0.0.0.0'; 15 | } 16 | 17 | constructor({ url, description, variables = {} }, extensionProps = []) { 18 | 19 | let finalUrl = url; 20 | 21 | for(const [variableName, variableData] of Object.entries(variables)) 22 | finalUrl = finalUrl.replace(new RegExp(`{${variableName}}`, 'g'), variableData.default); 23 | 24 | const urlBase = finalUrl.match(/^https?:\/\//) ? undefined : this.constructor.defaultUrlBase; 25 | 26 | this.url = new URL(finalUrl, urlBase); 27 | this.description = description; 28 | 29 | this.extensions = {}; 30 | for(const [extensionName, extensionValue] of extensionProps) 31 | this.extensions[extensionName] = extensionValue; 32 | } 33 | 34 | } 35 | 36 | module.exports = Server; 37 | -------------------------------------------------------------------------------- /lib/servers/structs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { struct } = require('superstruct'); 4 | 5 | const ServerVariableStruct = struct.intersection([ 6 | 'object', 7 | struct.interface({ 8 | default: 'string', 9 | enum: struct.optional(['string']), 10 | description: 'string?' 11 | }) 12 | ]); 13 | 14 | const ServerStruct = struct.interface({ 15 | url: 'string', 16 | description: 'string?', 17 | variables: struct.optional(struct.dict(['string', ServerVariableStruct])) 18 | }); 19 | 20 | const ServersStruct = struct.optional(struct.list([ServerStruct])); 21 | 22 | module.exports = ServersStruct; 23 | -------------------------------------------------------------------------------- /lib/structs/reference.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { struct } = require('superstruct'); 4 | 5 | const ReferenceStruct = struct({ 6 | $ref: 'string' 7 | }); 8 | 9 | module.exports = ReferenceStruct; 10 | -------------------------------------------------------------------------------- /lib/tags/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Parser = require('./parser'); 4 | const Tag = require('./tag'); 5 | 6 | module.exports = { 7 | Parser, 8 | Tag 9 | }; 10 | -------------------------------------------------------------------------------- /lib/tags/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Tag = require('./tag'); 4 | const TagsStruct = require('./structs'); 5 | const extractExtensions = require('../utils/extract-extensions'); 6 | const enhanceStructValidationError = require('../utils/enhance-struct-validation-error'); 7 | 8 | class Parser { 9 | 10 | parse(schema) { 11 | 12 | const { tags } = schema; 13 | 14 | this.validateTags(tags); 15 | 16 | if(!tags || !tags.length) 17 | return []; 18 | 19 | return tags 20 | .map(this.parseTag.bind(this)); 21 | } 22 | 23 | validateTags(tags) { 24 | 25 | try { 26 | return TagsStruct(tags); 27 | } catch(e) { 28 | throw enhanceStructValidationError(e, 'tags'); 29 | } 30 | } 31 | 32 | parseTag({ name, description, externalDocs, ...otherProps }) { 33 | 34 | const extensionProps = extractExtensions(otherProps); 35 | 36 | return new Tag({ 37 | name, 38 | description, 39 | externalDocs 40 | }, extensionProps); 41 | } 42 | 43 | } 44 | 45 | module.exports = Parser; 46 | -------------------------------------------------------------------------------- /lib/tags/structs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { struct } = require('superstruct'); 4 | 5 | const ExternalDocsStruct = require('../external-documentation/structs'); 6 | 7 | const TagStruct = struct.interface({ 8 | name: 'string', 9 | description: 'string?', 10 | externalDocs: ExternalDocsStruct 11 | }); 12 | 13 | const TagsStruct = struct.optional(struct.list([TagStruct])); 14 | 15 | module.exports = TagsStruct; 16 | -------------------------------------------------------------------------------- /lib/tags/tag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Parser: ExternalDocumentationParser } = require('../external-documentation'); 4 | 5 | class Tag { 6 | 7 | constructor({ name, description, externalDocs }, extensionProps = []) { 8 | 9 | const externalDocumentationParser = new ExternalDocumentationParser(); 10 | 11 | this.name = name; 12 | this.description = description; 13 | this.externalDocs = externalDocumentationParser.parse({ externalDocs }); 14 | 15 | this.extensions = {}; 16 | for(const [extensionName, extensionValue] of extensionProps) 17 | this.extensions[extensionName] = extensionValue; 18 | } 19 | 20 | } 21 | 22 | module.exports = Tag; 23 | -------------------------------------------------------------------------------- /lib/utils/enhance-struct-validation-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ParserError = require('../errors/parser-error'); 4 | 5 | const enhanceStructValidationError = (error, initialPath) => { 6 | 7 | const path = error.path 8 | .reduce((acum, pathPart) => `${acum}.${pathPart}`, initialPath); 9 | 10 | return new ParserError(error.message, path); 11 | 12 | }; 13 | 14 | module.exports = enhanceStructValidationError; 15 | -------------------------------------------------------------------------------- /lib/utils/extract-extensions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const extractExtensions = object => Object.entries(object) 4 | .filter(([propName]) => propName.substr(0, 2) === 'x-'); 5 | 6 | module.exports = extractExtensions; 7 | -------------------------------------------------------------------------------- /lib/utils/get-faker-locale.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { allFakers } = require('@faker-js/faker'); 4 | 5 | const DEFAULT_LOCALE = 'en'; 6 | 7 | const parseUserLocale = () => { 8 | 9 | const { locale } = Intl.DateTimeFormat().resolvedOptions(); 10 | 11 | const [lang, country = ''] = locale.split('-'); 12 | 13 | if(!country) 14 | return lang; 15 | 16 | return lang.toLowerCase() === country.toLowerCase() && 17 | lang.toLowerCase() !== 'pt' 18 | ? lang 19 | : `${lang}_${country}`; 20 | }; 21 | 22 | /** 23 | * @returns {import('@faker-js/faker').Faker} 24 | */ 25 | const getFakerLocale = (userLocaleParser = parseUserLocale) => { 26 | 27 | const userLocale = userLocaleParser(); 28 | 29 | if(allFakers[userLocale]) 30 | return allFakers[userLocale]; 31 | 32 | if(userLocale.includes('_')) { 33 | const [baseUserLocale] = userLocale.split('_'); 34 | if(allFakers[baseUserLocale]) 35 | return allFakers[baseUserLocale]; 36 | } 37 | 38 | return allFakers[DEFAULT_LOCALE]; 39 | }; 40 | 41 | module.exports = getFakerLocale; 42 | -------------------------------------------------------------------------------- /lib/utils/http-methods.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.knownHttpMethods = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace']; 4 | -------------------------------------------------------------------------------- /lib/utils/options-builder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const DefaultServer = require('../mocker/express/server'); 4 | 5 | const defaultOptions = { 6 | port: 5000, 7 | server: null, 8 | watch: false, 9 | schemaLoader: null 10 | }; 11 | 12 | const forcedOptions = { 13 | alreadyWatching: false 14 | }; 15 | 16 | module.exports = customOptions => { 17 | 18 | const finalOptions = { 19 | ...defaultOptions, 20 | ...customOptions, 21 | ...forcedOptions 22 | }; 23 | 24 | if(!finalOptions.server) 25 | finalOptions.server = new DefaultServer(); 26 | 27 | return finalOptions; 28 | }; 29 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "js,json,yml,yaml", 3 | "exec": "node lib/open-api-mocker-cli.js -s tests/resources/pet-store.yml" 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-api-mocker", 3 | "version": "2.0.0", 4 | "description": "A mock server based in Open API Specification", 5 | "main": "lib/open-api-mocker.js", 6 | "bin": { 7 | "open-api-mocker": "lib/open-api-mocker-cli.js" 8 | }, 9 | "scripts": { 10 | "lint": "eslint ./lib ./tests", 11 | "test": "mocha -R nyan --recursive --require tests/bootstrap.js tests/", 12 | "coverage:ci": "nyc --reporter=lcov mocha --recursive tests/", 13 | "coverage": "nyc npm test", 14 | "prepare": "husky install" 15 | }, 16 | "files": [ 17 | "lib/" 18 | ], 19 | "keywords": [ 20 | "api", 21 | "openapi", 22 | "schema", 23 | "mock", 24 | "mocking", 25 | "mock-server" 26 | ], 27 | "author": "Joaquin Ormaechea", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "eslint": "^8.41.0", 31 | "eslint-config-airbnb-base": "^15.0.0", 32 | "eslint-plugin-import": "^2.27.5", 33 | "husky": "^8.0.3", 34 | "lint-staged": "^13.2.2", 35 | "mocha": "^10.2.0", 36 | "mock-require": "^3.0.3", 37 | "nyc": "^15.1.0", 38 | "sinon": "^15.1.0" 39 | }, 40 | "dependencies": { 41 | "@faker-js/faker": "^8.0.2", 42 | "ajv": "^6.12.6", 43 | "ajv-openapi": "^2.0.0", 44 | "body-parser": "^1.20.2", 45 | "chokidar": "^3.5.3", 46 | "colors": "^1.4.0", 47 | "cookie-parser": "^1.4.6", 48 | "cors": "^2.8.5", 49 | "express": "^4.18.2", 50 | "js-yaml": "^4.1.0", 51 | "json-refs": "^3.0.15", 52 | "lllog": "^1.1.2", 53 | "micro-memoize": "^4.1.2", 54 | "parse-prefer-header": "^1.0.0", 55 | "superstruct": "^0.6.2", 56 | "yargs": "^17.7.2" 57 | }, 58 | "directories": { 59 | "lib": "lib", 60 | "test": "tests" 61 | }, 62 | "repository": { 63 | "type": "git", 64 | "url": "git+https://jormaechea@github.com/jormaechea/open-api-mocker.git" 65 | }, 66 | "bugs": { 67 | "url": "https://github.com/jormaechea/open-api-mocker/issues" 68 | }, 69 | "homepage": "https://github.com/jormaechea/open-api-mocker#readme" 70 | } 71 | -------------------------------------------------------------------------------- /tests/bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.TZ = 'UTC'; 4 | 5 | require('lllog')() 6 | .setMinLevel('none'); 7 | -------------------------------------------------------------------------------- /tests/components/components.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ParserError = require('../../lib/errors/parser-error'); 6 | const { Parser, Components } = require('../../lib/components'); 7 | 8 | describe('Components', () => { 9 | 10 | describe('Schema parse', () => { 11 | 12 | const parser = new Parser(); 13 | 14 | it('Should return the Components object if components is not defined', () => { 15 | 16 | const schema = {}; 17 | 18 | const components = parser.parse(schema); 19 | 20 | const expectedComponents = new Components({}); 21 | 22 | assert.deepStrictEqual(components, expectedComponents); 23 | }); 24 | 25 | it('Should return the Components object if components is an empty object', () => { 26 | 27 | const schema = { components: {} }; 28 | 29 | const components = parser.parse(schema); 30 | 31 | const expectedComponents = new Components({}); 32 | 33 | assert.deepStrictEqual(components, expectedComponents); 34 | }); 35 | 36 | it('Should throw a ParserError if components is an not an object', () => { 37 | 38 | const schema = { components: [] }; 39 | 40 | assert.throws(() => parser.parse(schema), ParserError); 41 | }); 42 | 43 | it('Should not set an schema type if it has the oneOf property set', () => { 44 | 45 | const schema = { 46 | components: { 47 | schemas: { 48 | FooSchema: { 49 | type: 'object', 50 | properties: { 51 | id: { 52 | oneOf: [ 53 | { type: 'string' }, 54 | { type: 'number' } 55 | ] 56 | } 57 | } 58 | } 59 | } 60 | } 61 | }; 62 | 63 | const components = parser.parse(schema); 64 | assert.deepStrictEqual(components.schemas.FooSchema.properties.id.type, undefined); 65 | }); 66 | 67 | it('Should mantain the specification extension properties', () => { 68 | 69 | const schema = { 70 | components: { 71 | 'x-foo': 'bar', 72 | 'x-baz': { 73 | test: [1, 2, 3] 74 | } 75 | } 76 | }; 77 | 78 | const components = parser.parse(schema); 79 | 80 | const expectedComponents = new Components(schema.components, [ 81 | ['x-foo', 'bar'], 82 | ['x-baz', { test: [1, 2, 3] }] 83 | ]); 84 | 85 | assert.deepStrictEqual(components, expectedComponents); 86 | }); 87 | 88 | }); 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /tests/components/parameters-common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ParameterStruct = require('../../lib/components/parameters/structs'); 6 | 7 | describe('Components', () => { 8 | 9 | describe('Parameters', () => { 10 | 11 | describe('Common structure', () => { 12 | 13 | it('Should throw if the parameter is an not an object', () => { 14 | 15 | const parameter = []; 16 | 17 | assert.throws(() => ParameterStruct(parameter)); 18 | }); 19 | 20 | it('Should throw if the parameter doesn\'t have the required properties', () => { 21 | 22 | const parameter = {}; 23 | 24 | assert.throws(() => ParameterStruct(parameter)); 25 | }); 26 | 27 | it('Should throw if the parameter has an invalid name property', () => { 28 | 29 | const parameter = { 30 | name: 1, 31 | in: 'query' 32 | }; 33 | 34 | assert.throws(() => ParameterStruct(parameter), { 35 | path: ['name'] 36 | }); 37 | }); 38 | 39 | it('Should throw if the parameter has an invalid in property', () => { 40 | 41 | const parameter = { 42 | name: 'someName', 43 | in: 'invalidIn' 44 | }; 45 | 46 | assert.throws(() => ParameterStruct(parameter), { 47 | path: ['in'] 48 | }); 49 | }); 50 | 51 | it('Should pass if the parameter has the required properties', () => { 52 | 53 | const parameter = { 54 | name: 'someName', 55 | in: 'query' 56 | }; 57 | 58 | ParameterStruct(parameter); 59 | }); 60 | 61 | it('Should throw if any optional property is invalid', () => { 62 | 63 | const parameter = { 64 | name: 'someName', 65 | in: 'query' 66 | }; 67 | 68 | assert.throws(() => ParameterStruct({ 69 | ...parameter, 70 | description: ['Invalid'] 71 | }), { 72 | path: ['description'] 73 | }); 74 | 75 | assert.throws(() => ParameterStruct({ 76 | ...parameter, 77 | required: ['Invalid'] 78 | }), { 79 | path: ['required'] 80 | }); 81 | 82 | assert.throws(() => ParameterStruct({ 83 | ...parameter, 84 | deprecated: ['Invalid'] 85 | }), { 86 | path: ['deprecated'] 87 | }); 88 | 89 | assert.throws(() => ParameterStruct({ 90 | ...parameter, 91 | style: ['Invalid'] 92 | }), { 93 | path: ['style'] 94 | }); 95 | 96 | assert.throws(() => ParameterStruct({ 97 | ...parameter, 98 | explode: ['Invalid'] 99 | }), { 100 | path: ['explode'] 101 | }); 102 | 103 | assert.throws(() => ParameterStruct({ 104 | ...parameter, 105 | schema: ['Invalid'] 106 | }), { 107 | path: ['schema'] 108 | }); 109 | }); 110 | 111 | it('Should pass if the parameter has the required properties and valid optional properties', () => { 112 | 113 | const parameter = { 114 | name: 'someName', 115 | in: 'query', 116 | description: 'Some description', 117 | required: true, 118 | deprecated: false, 119 | style: 'form', 120 | explode: true, 121 | example: { foo: 'bar' } 122 | }; 123 | 124 | ParameterStruct(parameter); 125 | }); 126 | 127 | it('Should set the default values', () => { 128 | 129 | const parameter = { 130 | name: 'someName', 131 | in: 'query' 132 | }; 133 | 134 | const result = ParameterStruct(parameter); 135 | 136 | assert.deepStrictEqual(result, { 137 | ...parameter, 138 | required: false, 139 | deprecated: false 140 | // @todo Uncomment when superstruct issue #131 is resolved 141 | // allowEmptyValue: false, 142 | // allowReserved: false 143 | }); 144 | }); 145 | 146 | it('Should allow and maintain the specification extension properties', () => { 147 | 148 | const parameter = { 149 | name: 'someName', 150 | in: 'query', 151 | 'x-foo': 'bar', 152 | 'x-baz': { 153 | test: [1, 2, 3] 154 | } 155 | }; 156 | 157 | const result = ParameterStruct(parameter); 158 | 159 | assert.strictEqual(result['x-foo'], 'bar'); 160 | assert.deepStrictEqual(result['x-baz'], { test: [1, 2, 3] }); 161 | }); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /tests/components/parameters-cookie.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ParameterStruct = require('../../lib/components/parameters/structs'); 6 | 7 | describe('Components', () => { 8 | 9 | describe('Parameters', () => { 10 | 11 | describe('Cookie parameters', () => { 12 | 13 | it('Should pass if the parameter has the required properties', () => { 14 | 15 | const parameter = { 16 | name: 'someName', 17 | in: 'cookie' 18 | }; 19 | 20 | ParameterStruct(parameter); 21 | }); 22 | 23 | it('Should throw if any optional property is invalid', () => { 24 | 25 | const parameter = { 26 | name: 'someName', 27 | in: 'cookie' 28 | }; 29 | 30 | assert.throws(() => ParameterStruct({ 31 | ...parameter, 32 | allowEmptyValue: true 33 | }), { 34 | path: ['allowEmptyValue'] 35 | }); 36 | 37 | assert.throws(() => ParameterStruct({ 38 | ...parameter, 39 | allowReserved: true 40 | }), { 41 | path: ['allowReserved'] 42 | }); 43 | }); 44 | 45 | it('Should pass if the parameter has the required properties and valid optional properties', () => { 46 | 47 | const parameter = { 48 | name: 'someName', 49 | in: 'cookie', 50 | required: false 51 | }; 52 | 53 | ParameterStruct(parameter); 54 | }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/components/parameters-header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ParameterStruct = require('../../lib/components/parameters/structs'); 6 | 7 | describe('Components', () => { 8 | 9 | describe('Parameters', () => { 10 | 11 | describe('Header parameters', () => { 12 | 13 | it('Should pass if the parameter has the required properties', () => { 14 | 15 | const parameter = { 16 | name: 'someName', 17 | in: 'header' 18 | }; 19 | 20 | ParameterStruct(parameter); 21 | }); 22 | 23 | it('Should throw if any optional property is invalid', () => { 24 | 25 | const parameter = { 26 | name: 'someName', 27 | in: 'header' 28 | }; 29 | 30 | assert.throws(() => ParameterStruct({ 31 | ...parameter, 32 | allowEmptyValue: true 33 | }), { 34 | path: ['allowEmptyValue'] 35 | }); 36 | 37 | assert.throws(() => ParameterStruct({ 38 | ...parameter, 39 | allowReserved: true 40 | }), { 41 | path: ['allowReserved'] 42 | }); 43 | }); 44 | 45 | it('Should pass if the parameter has the required properties and valid optional properties', () => { 46 | 47 | const parameter = { 48 | name: 'someName', 49 | in: 'header', 50 | required: false 51 | }; 52 | 53 | ParameterStruct(parameter); 54 | }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/components/parameters-path.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ParameterStruct = require('../../lib/components/parameters/structs'); 6 | 7 | describe('Components', () => { 8 | 9 | describe('Parameters', () => { 10 | 11 | describe('Path parameters', () => { 12 | 13 | it('Should pass if the parameter doesn\'t have the `required` property set as true', () => { 14 | 15 | const parameter = { 16 | name: 'someName', 17 | in: 'path' 18 | }; 19 | 20 | assert.throws(() => ParameterStruct(parameter), { 21 | path: ['required'] 22 | }); 23 | 24 | assert.throws(() => ParameterStruct({ 25 | ...parameter, 26 | required: false 27 | }), { 28 | path: ['required'] 29 | }); 30 | }); 31 | 32 | it('Should pass if the parameter has the required properties', () => { 33 | 34 | const parameter = { 35 | name: 'someName', 36 | in: 'path', 37 | required: true 38 | }; 39 | 40 | ParameterStruct(parameter); 41 | }); 42 | 43 | it('Should throw if any optional property is invalid', () => { 44 | 45 | const parameter = { 46 | name: 'someName', 47 | in: 'path', 48 | required: true 49 | }; 50 | 51 | assert.throws(() => ParameterStruct({ 52 | ...parameter, 53 | allowEmptyValue: true 54 | }), { 55 | path: ['allowEmptyValue'] 56 | }); 57 | 58 | assert.throws(() => ParameterStruct({ 59 | ...parameter, 60 | allowReserved: true 61 | }), { 62 | path: ['allowReserved'] 63 | }); 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /tests/components/parameters-query.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ParameterStruct = require('../../lib/components/parameters/structs'); 6 | 7 | describe('Components', () => { 8 | 9 | describe('Parameters', () => { 10 | 11 | describe('Query parameters', () => { 12 | 13 | it('Should pass if the parameter has the required properties', () => { 14 | 15 | const parameter = { 16 | name: 'someName', 17 | in: 'query' 18 | }; 19 | 20 | ParameterStruct(parameter); 21 | }); 22 | 23 | it('Should throw if any optional property is invalid', () => { 24 | 25 | const parameter = { 26 | name: 'someName', 27 | in: 'query' 28 | }; 29 | 30 | assert.throws(() => ParameterStruct({ 31 | ...parameter, 32 | allowEmptyValue: ['Invalid'] 33 | }), { 34 | path: ['allowEmptyValue'] 35 | }); 36 | 37 | assert.throws(() => ParameterStruct({ 38 | ...parameter, 39 | allowReserved: ['Invalid'] 40 | }), { 41 | path: ['allowReserved'] 42 | }); 43 | }); 44 | 45 | it('Should pass if the parameter has the required properties and valid optional properties', () => { 46 | 47 | const parameter = { 48 | name: 'someName', 49 | in: 'query', 50 | required: false, 51 | allowEmptyValue: true, 52 | allowReserved: true 53 | }; 54 | 55 | ParameterStruct(parameter); 56 | }); 57 | 58 | it('Should set the default values', () => { 59 | 60 | const parameter = { 61 | name: 'someName', 62 | in: 'query' 63 | }; 64 | 65 | const result = ParameterStruct(parameter); 66 | 67 | assert.strictEqual(result.required, false); 68 | // @todo Uncomment when superstruct issue #131 is resolved 69 | // assert.strictEqual(result.allowEmptyValue, false); 70 | // assert.strictEqual(result.allowReserved, false); 71 | }); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/components/responses.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ResponseStruct = require('../../lib/components/responses/structs'); 6 | 7 | describe('Components', () => { 8 | 9 | describe('Responses', () => { 10 | 11 | describe('Base structure', () => { 12 | 13 | it('Should throw if the response is an not an object', () => { 14 | 15 | const response = []; 16 | 17 | assert.throws(() => ResponseStruct(response)); 18 | }); 19 | 20 | it('Should throw if the response doesn\'t have the required properties', () => { 21 | 22 | const response = {}; 23 | 24 | assert.throws(() => ResponseStruct(response)); 25 | }); 26 | 27 | it('Should throw if the response has an invalid description property', () => { 28 | 29 | const response = { 30 | description: ['Invalid'] 31 | }; 32 | 33 | assert.throws(() => ResponseStruct(response), { 34 | path: ['description'] 35 | }); 36 | }); 37 | 38 | it('Should pass if the response has the required properties', () => { 39 | 40 | const response = { 41 | description: 'The description' 42 | }; 43 | 44 | ResponseStruct(response); 45 | }); 46 | 47 | it('Should allow and maintain the specification extension properties', () => { 48 | 49 | const response = { 50 | description: 'The description', 51 | 'x-foo': 'bar', 52 | 'x-baz': { 53 | test: [1, 2, 3] 54 | } 55 | }; 56 | 57 | const result = ResponseStruct(response); 58 | 59 | assert.strictEqual(result['x-foo'], 'bar'); 60 | assert.deepStrictEqual(result['x-baz'], { test: [1, 2, 3] }); 61 | }); 62 | }); 63 | 64 | describe('Headers', () => { 65 | 66 | it('Should throw if the response has an invalid headers property', () => { 67 | 68 | const response = { 69 | description: 'The description', 70 | headers: 'Invalid' 71 | }; 72 | 73 | assert.throws(() => ResponseStruct(response), { 74 | path: ['headers'] 75 | }); 76 | 77 | assert.throws(() => ResponseStruct({ 78 | ...response, 79 | headers: ['Invalid'] 80 | }), { 81 | path: ['headers'] 82 | }); 83 | 84 | assert.throws(() => ResponseStruct({ 85 | ...response, 86 | headers: { 87 | headerName: { 88 | description: ['An invalid value'] 89 | } 90 | } 91 | }), { 92 | path: ['headers', 'headerName', 'description'] 93 | }); 94 | }); 95 | 96 | it('Should pass if the response has and empty object header', () => { 97 | 98 | const response = { 99 | description: 'The description', 100 | headers: { 101 | headerName: {} 102 | } 103 | }; 104 | 105 | ResponseStruct(response); 106 | }); 107 | 108 | it('Should pass if the response has a referenced header', () => { 109 | 110 | const response = { 111 | description: 'The description', 112 | headers: { 113 | headerName: { 114 | $ref: '#/some/ref' 115 | } 116 | } 117 | }; 118 | 119 | ResponseStruct(response); 120 | }); 121 | 122 | it('Should pass if the response has a complete object header', () => { 123 | 124 | const response = { 125 | description: 'The description', 126 | headers: { 127 | headerName: { 128 | description: 'Some header description', 129 | required: true, 130 | deprecated: false, 131 | style: 'simple', 132 | explode: false, 133 | schema: { 134 | $ref: '#some/ref' 135 | } 136 | } 137 | } 138 | }; 139 | 140 | ResponseStruct(response); 141 | }); 142 | 143 | it('Should set the response headers defaults', () => { 144 | 145 | const response = { 146 | description: 'The description', 147 | headers: { 148 | headerName: {} 149 | } 150 | }; 151 | 152 | const result = ResponseStruct(response); 153 | 154 | assert.deepStrictEqual(result.headers.headerName, { 155 | required: false, 156 | deprecated: false, 157 | style: 'simple', 158 | explode: false 159 | }); 160 | }); 161 | 162 | it('Should allow and maintain the specification extension properties', () => { 163 | 164 | const response = { 165 | description: 'The description', 166 | headers: { 167 | headerName: { 168 | 'x-foo': 'bar', 169 | 'x-baz': { 170 | test: [1, 2, 3] 171 | } 172 | } 173 | } 174 | }; 175 | 176 | const result = ResponseStruct(response); 177 | 178 | assert.strictEqual(result.headers.headerName['x-foo'], 'bar'); 179 | assert.deepStrictEqual(result.headers.headerName['x-baz'], { test: [1, 2, 3] }); 180 | }); 181 | }); 182 | 183 | describe('Content', () => { 184 | 185 | it('Should throw if the response has an invalid content property', () => { 186 | 187 | const response = { 188 | description: 'The description', 189 | content: 'Invalid' 190 | }; 191 | 192 | assert.throws(() => ResponseStruct(response), { 193 | path: ['content'] 194 | }); 195 | 196 | assert.throws(() => ResponseStruct({ 197 | ...response, 198 | content: ['Invalid'] 199 | }), { 200 | path: ['content'] 201 | }); 202 | 203 | assert.throws(() => ResponseStruct({ 204 | ...response, 205 | content: { 206 | 'application/json': { 207 | schema: ['Invalid schema'] 208 | } 209 | } 210 | }), { 211 | path: ['content', 'application/json', 'schema'] 212 | }); 213 | }); 214 | 215 | it('Should pass if the response has and empty object content', () => { 216 | 217 | const response = { 218 | description: 'The description', 219 | content: { 220 | 'application/json': {} 221 | } 222 | }; 223 | 224 | ResponseStruct(response); 225 | }); 226 | 227 | it('Should pass if the response has a referenced content', () => { 228 | 229 | const response = { 230 | description: 'The description', 231 | content: { 232 | 'application/json': { 233 | schema: { 234 | $ref: '#/some/ref' 235 | } 236 | } 237 | } 238 | }; 239 | 240 | ResponseStruct(response); 241 | }); 242 | 243 | it('Should pass if the response has a complete object content', () => { 244 | 245 | const response = { 246 | description: 'The description', 247 | content: { 248 | 'application/json': { 249 | schema: { 250 | type: 'string' 251 | } 252 | } 253 | } 254 | }; 255 | 256 | ResponseStruct(response); 257 | }); 258 | 259 | it('Should allow and maintain the specification extension properties', () => { 260 | 261 | const response = { 262 | description: 'The description', 263 | content: { 264 | 'application/json': { 265 | 'x-foo': 'bar', 266 | 'x-baz': { 267 | test: [1, 2, 3] 268 | } 269 | } 270 | } 271 | }; 272 | 273 | const result = ResponseStruct(response); 274 | 275 | assert.strictEqual(result.content['application/json']['x-foo'], 'bar'); 276 | assert.deepStrictEqual(result.content['application/json']['x-baz'], { test: [1, 2, 3] }); 277 | }); 278 | }); 279 | }); 280 | }); 281 | -------------------------------------------------------------------------------- /tests/components/security-schemes-api-key.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const SecuritySchemesStruct = require('../../lib/components/security-schemes/structs'); 6 | 7 | describe('Components', () => { 8 | 9 | describe('Security schemes', () => { 10 | 11 | describe('API key', () => { 12 | 13 | it('Should pass if the security scheme has the required properties', () => { 14 | 15 | const securityScheme = { 16 | type: 'apiKey', 17 | name: 'x-api-key', 18 | in: 'header' 19 | }; 20 | 21 | SecuritySchemesStruct(securityScheme); 22 | }); 23 | 24 | it('Should pass if the security scheme has the required properties and valid optional properties', () => { 25 | 26 | SecuritySchemesStruct({ 27 | type: 'apiKey', 28 | name: 'x-api-key', 29 | in: 'header', 30 | description: 'Some description' 31 | }); 32 | 33 | SecuritySchemesStruct({ 34 | type: 'apiKey', 35 | name: 'token', 36 | in: 'query', 37 | description: 'Some description' 38 | }); 39 | 40 | SecuritySchemesStruct({ 41 | type: 'apiKey', 42 | name: 'Authorization', 43 | in: 'cookie', 44 | description: 'Some description' 45 | }); 46 | }); 47 | 48 | it('Should allow and maintain the specification extension properties', () => { 49 | 50 | const securityScheme = { 51 | type: 'apiKey', 52 | name: 'x-api-key', 53 | in: 'header', 54 | 'x-foo': 'bar', 55 | 'x-baz': { 56 | test: [1, 2, 3] 57 | } 58 | }; 59 | 60 | const result = SecuritySchemesStruct(securityScheme); 61 | 62 | assert.strictEqual(result['x-foo'], 'bar'); 63 | assert.deepStrictEqual(result['x-baz'], { test: [1, 2, 3] }); 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /tests/components/security-schemes-common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const SecuritySchemesStruct = require('../../lib/components/security-schemes/structs'); 6 | 7 | describe('Components', () => { 8 | 9 | describe('Security schemes', () => { 10 | 11 | describe('Common structure', () => { 12 | 13 | it('Should throw if the security scheme is an not an object', () => { 14 | 15 | const securityScheme = []; 16 | 17 | assert.throws(() => SecuritySchemesStruct(securityScheme)); 18 | }); 19 | 20 | it('Should throw if the security scheme doesn\'t have the required properties', () => { 21 | 22 | const securityScheme = {}; 23 | 24 | assert.throws(() => SecuritySchemesStruct(securityScheme)); 25 | }); 26 | 27 | it('Should throw if the security scheme has an invalid type property', () => { 28 | 29 | const securityScheme = { 30 | type: 'invalid' 31 | }; 32 | 33 | assert.throws(() => SecuritySchemesStruct(securityScheme), { 34 | path: ['type'] 35 | }); 36 | }); 37 | 38 | it('Should throw if any optional property is invalid', () => { 39 | 40 | const securityScheme = { 41 | type: 'apiKey' 42 | }; 43 | 44 | assert.throws(() => SecuritySchemesStruct({ 45 | ...securityScheme, 46 | description: ['Invalid'] 47 | }), { 48 | path: ['description'] 49 | }); 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/components/security-schemes-http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const SecuritySchemesStruct = require('../../lib/components/security-schemes/structs'); 6 | 7 | describe('Components', () => { 8 | 9 | describe('Security schemes', () => { 10 | 11 | describe('Http', () => { 12 | 13 | it('Should pass if the security scheme has the required properties', () => { 14 | 15 | SecuritySchemesStruct({ 16 | type: 'http', 17 | scheme: 'bearer', 18 | bearerFormat: 'jwt' 19 | }); 20 | 21 | SecuritySchemesStruct({ 22 | type: 'http', 23 | scheme: 'otherScheme' 24 | }); 25 | }); 26 | 27 | it('Should fail if the security scheme has forbidden properties', () => { 28 | 29 | assert.throws(() => SecuritySchemesStruct({ 30 | type: 'http', 31 | scheme: 'otherScheme', 32 | bearerFormat: 'jwt' 33 | }), { 34 | path: ['type'] // This should be a bearerFormat but it breaks in superstruct 0.6.1 35 | }); 36 | }); 37 | 38 | it('Should pass if the security scheme has the required properties and valid optional properties', () => { 39 | 40 | SecuritySchemesStruct({ 41 | type: 'http', 42 | scheme: 'bearer', 43 | bearerFormat: 'jwt', 44 | description: 'Some description' 45 | }); 46 | 47 | SecuritySchemesStruct({ 48 | type: 'http', 49 | scheme: 'otherScheme', 50 | description: 'Some description' 51 | }); 52 | }); 53 | 54 | it('Should allow and maintain the specification extension properties', () => { 55 | 56 | const securityScheme = { 57 | type: 'http', 58 | scheme: 'otherScheme', 59 | 'x-foo': 'bar', 60 | 'x-baz': { 61 | test: [1, 2, 3] 62 | } 63 | }; 64 | 65 | const result = SecuritySchemesStruct(securityScheme); 66 | 67 | assert.strictEqual(result['x-foo'], 'bar'); 68 | assert.deepStrictEqual(result['x-baz'], { test: [1, 2, 3] }); 69 | }); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/components/security-schemes-oauth2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const SecuritySchemesStruct = require('../../lib/components/security-schemes/structs'); 6 | 7 | describe('Components', () => { 8 | 9 | describe('Security schemes', () => { 10 | 11 | describe('OAuth2', () => { 12 | 13 | it('Should fail if the security scheme has invalid required properties', () => { 14 | 15 | assert.throws(() => SecuritySchemesStruct({ 16 | type: 'oauth2', 17 | flows: [] 18 | }), { 19 | path: ['type'] // This should be a flows but it breaks in superstruct 0.6.1 20 | }); 21 | }); 22 | 23 | it('Should pass if the security scheme has the required properties', () => { 24 | 25 | SecuritySchemesStruct({ 26 | type: 'oauth2', 27 | flows: {} 28 | }); 29 | }); 30 | 31 | it('Should pass if the security scheme has the required properties and valid optional properties', () => { 32 | 33 | SecuritySchemesStruct({ 34 | type: 'oauth2', 35 | flows: { 36 | implicit: { 37 | authorizationUrl: 'https://oauth2.example.com/authorization', 38 | refreshUrl: 'https://oauth2.example.com/refresh', 39 | scopes: { 40 | scope1: 'Some scope description' 41 | } 42 | }, 43 | password: { 44 | refreshUrl: 'https://oauth2.example.com/refresh', 45 | scopes: { 46 | scope1: 'Some scope description' 47 | }, 48 | tokenUrl: 'https://oauth2.example.com/token' 49 | }, 50 | clientCredentials: { 51 | refreshUrl: 'https://oauth2.example.com/refresh', 52 | scopes: { 53 | scope1: 'Some scope description' 54 | }, 55 | tokenUrl: 'https://oauth2.example.com/token' 56 | }, 57 | authorizationCode: { 58 | authorizationUrl: 'https://oauth2.example.com/authorization', 59 | refreshUrl: 'https://oauth2.example.com/refresh', 60 | scopes: { 61 | scope1: 'Some scope description' 62 | }, 63 | tokenUrl: 'https://oauth2.example.com/token' 64 | } 65 | }, 66 | description: 'Some description' 67 | }); 68 | }); 69 | 70 | it('Should throw if any optional property is invalid', () => { 71 | 72 | assert.throws(() => SecuritySchemesStruct({ 73 | type: 'oauth2', 74 | flows: { 75 | implicit: { 76 | authorizationUrl: 'https://oauth2.example.com/authorization', 77 | refreshUrl: 'https://oauth2.example.com/refresh' 78 | } 79 | }, 80 | description: 'Some description' 81 | }), { 82 | path: ['type'] // This should be a [flows, implicit] or [flows, implicit, scopes] but it breaks in superstruct 0.6.1 83 | }); 84 | }); 85 | 86 | it('Should allow and maintain the specification extension properties', () => { 87 | 88 | const securityScheme = { 89 | type: 'oauth2', 90 | flows: {}, 91 | 'x-foo': 'bar', 92 | 'x-baz': { 93 | test: [1, 2, 3] 94 | } 95 | }; 96 | 97 | const result = SecuritySchemesStruct(securityScheme); 98 | 99 | assert.strictEqual(result['x-foo'], 'bar'); 100 | assert.deepStrictEqual(result['x-baz'], { test: [1, 2, 3] }); 101 | }); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /tests/components/security-schemes-open-id.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const SecuritySchemesStruct = require('../../lib/components/security-schemes/structs'); 6 | 7 | describe('Components', () => { 8 | 9 | describe('Security schemes', () => { 10 | 11 | describe('Open ID', () => { 12 | 13 | it('Should pass if the security scheme has the required properties', () => { 14 | 15 | SecuritySchemesStruct({ 16 | type: 'openIdConnect', 17 | openIdConnectUrl: 'https://openid.example.com' 18 | }); 19 | }); 20 | 21 | it('Should pass if the security scheme has the required properties and valid optional properties', () => { 22 | 23 | SecuritySchemesStruct({ 24 | type: 'openIdConnect', 25 | openIdConnectUrl: 'https://openid.example.com', 26 | description: 'Some description' 27 | }); 28 | }); 29 | 30 | it('Should allow and maintain the specification extension properties', () => { 31 | 32 | const securityScheme = { 33 | type: 'openIdConnect', 34 | openIdConnectUrl: 'https://openid.example.com', 35 | 'x-foo': 'bar', 36 | 'x-baz': { 37 | test: [1, 2, 3] 38 | } 39 | }; 40 | 41 | const result = SecuritySchemesStruct(securityScheme); 42 | 43 | assert.strictEqual(result['x-foo'], 'bar'); 44 | assert.deepStrictEqual(result['x-baz'], { test: [1, 2, 3] }); 45 | }); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/errors/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const CliErrorCodes = require('../../lib/errors/cli-error-codes'); 6 | const OpenAPISchemaInvalid = require('../../lib/errors/openapi-schema-invalid-error'); 7 | const OpenAPISchemaMalformed = require('../../lib/errors/openapi-schema-malformed-error'); 8 | const OpenAPISchemaNotFound = require('../../lib/errors/openapi-schema-not-found-error'); 9 | 10 | describe('Errors', () => { 11 | 12 | describe('OpenAPISchemaInvalid error', () => { 13 | it('Should return the correct CLI error code', () => { 14 | const error = new OpenAPISchemaInvalid(); 15 | assert.strictEqual(error.cliError, CliErrorCodes.SCHEMA_INVALID); 16 | }); 17 | }); 18 | 19 | describe('OpenAPISchemaMalformed error', () => { 20 | it('Should return the correct CLI error code', () => { 21 | const error = new OpenAPISchemaMalformed(); 22 | assert.strictEqual(error.cliError, CliErrorCodes.SCHEMA_MALFORMED); 23 | }); 24 | }); 25 | 26 | describe('OpenAPISchemaNotFound error', () => { 27 | it('Should return the correct CLI error code', () => { 28 | const error = new OpenAPISchemaNotFound(); 29 | assert.strictEqual(error.cliError, CliErrorCodes.SCHEMA_NOT_FOUND); 30 | }); 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /tests/external-documentation/external-documentation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ParserError = require('../../lib/errors/parser-error'); 6 | const { Parser, ExternalDocumentation } = require('../../lib/external-documentation'); 7 | 8 | describe('ExternalDocumentation', () => { 9 | 10 | describe('Schema parse', () => { 11 | 12 | const parser = new Parser(); 13 | 14 | it('Should return an empty ExternalDocumentation instance if externalDocs is not defined', () => { 15 | 16 | const schema = {}; 17 | 18 | const externalDocs = parser.parse(schema); 19 | 20 | const expectedExternalDocumentation = new ExternalDocumentation({}); 21 | 22 | assert.deepStrictEqual(externalDocs, expectedExternalDocumentation); 23 | }); 24 | 25 | it('Should throw a ParserError if externalDocs is not an object', () => { 26 | 27 | const schema = { 28 | externalDocs: 'I\'m a string' 29 | }; 30 | 31 | assert.throws(() => parser.parse(schema), ParserError); 32 | }); 33 | 34 | it('Should throw a ParserError if externalDocs.url is not defined', () => { 35 | 36 | const schema = { 37 | externalDocs: { 38 | description: 'The documentation description' 39 | } 40 | }; 41 | 42 | assert.throws(() => parser.parse(schema), ParserError); 43 | }); 44 | 45 | it('Should throw a ParserError if externalDocs.url is defined but not as a string', () => { 46 | 47 | const schema = { 48 | externalDocs: { 49 | url: ['https://api.example.com/docs'] 50 | } 51 | }; 52 | 53 | assert.throws(() => parser.parse(schema), ParserError); 54 | }); 55 | 56 | it('Should return the ExternalDocumentation object if externalDocs is defined with minimal fields', () => { 57 | 58 | const schema = { 59 | externalDocs: { 60 | url: 'https://api.example.com/docs' 61 | } 62 | }; 63 | 64 | const externalDocs = parser.parse(schema); 65 | 66 | const expectedExternalDocumentation = new ExternalDocumentation(schema.externalDocs); 67 | 68 | assert.deepStrictEqual(externalDocs, expectedExternalDocumentation); 69 | }); 70 | 71 | it('Should return the ExternalDocumentation object if externalDocs is defined with every field', () => { 72 | 73 | const schema = { 74 | externalDocs: { 75 | url: 'https://api.example.com/docs', 76 | description: 'The documentation description' 77 | } 78 | }; 79 | 80 | const externalDocs = parser.parse(schema); 81 | 82 | const expectedExternalDocumentation = new ExternalDocumentation(schema.externalDocs); 83 | 84 | assert.deepStrictEqual(externalDocs, expectedExternalDocumentation); 85 | }); 86 | 87 | it('Should mantain the specification extension properties', () => { 88 | 89 | const schema = { 90 | externalDocs: { 91 | url: 'https://api.example.com/docs', 92 | description: 'The documentation description', 93 | 'x-foo': 'bar', 94 | 'x-baz': { 95 | test: [1, 2, 3] 96 | } 97 | } 98 | }; 99 | 100 | const externalDocs = parser.parse(schema); 101 | 102 | const expectedExternalDocumentation = new ExternalDocumentation(schema.externalDocs, [ 103 | ['x-foo', 'bar'], 104 | ['x-baz', { test: [1, 2, 3] }] 105 | ]); 106 | 107 | assert.deepStrictEqual(externalDocs, expectedExternalDocumentation); 108 | }); 109 | 110 | }); 111 | 112 | }); 113 | -------------------------------------------------------------------------------- /tests/info/info.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ParserError = require('../../lib/errors/parser-error'); 6 | const { Parser, Info } = require('../../lib/info'); 7 | 8 | describe('Info', () => { 9 | 10 | describe('Schema parse', () => { 11 | 12 | const parser = new Parser(); 13 | 14 | it('Should throw a ParserError if info is not defined', () => { 15 | 16 | const schema = {}; 17 | 18 | assert.throws(() => parser.parse(schema), ParserError); 19 | }); 20 | 21 | it('Should throw a ParserError if info is not an object', () => { 22 | 23 | const schema = { 24 | info: 'I\'m a string' 25 | }; 26 | 27 | assert.throws(() => parser.parse(schema), ParserError); 28 | }); 29 | 30 | it('Should throw a ParserError if info.title is not defined', () => { 31 | 32 | const schema = { 33 | info: {} 34 | }; 35 | 36 | assert.throws(() => parser.parse(schema), ParserError); 37 | }); 38 | 39 | it('Should throw a ParserError if info.version is not defined', () => { 40 | 41 | const schema = { 42 | info: { 43 | title: 'My API' 44 | } 45 | }; 46 | 47 | assert.throws(() => parser.parse(schema), ParserError); 48 | }); 49 | 50 | it('Should throw a ParserError if info.contact is not defined but not as object', () => { 51 | 52 | const schema = { 53 | info: { 54 | title: 'My API', 55 | version: '1.0.0', 56 | contact: 'john.doe@example.com' 57 | } 58 | }; 59 | 60 | assert.throws(() => parser.parse(schema), ParserError); 61 | }); 62 | 63 | it('Should throw a ParserError if info.license is defined but not as object', () => { 64 | 65 | const schema = { 66 | info: { 67 | title: 'My API', 68 | version: '1.0.0', 69 | license: 'MIT' 70 | } 71 | }; 72 | 73 | assert.throws(() => parser.parse(schema), ParserError); 74 | }); 75 | 76 | it('Should throw a ParserError if info.license is defined without a name', () => { 77 | 78 | const schema = { 79 | info: { 80 | title: 'My API', 81 | version: '1.0.0', 82 | license: { 83 | url: 'https://example.com/license' 84 | } 85 | } 86 | }; 87 | 88 | assert.throws(() => parser.parse(schema), ParserError); 89 | }); 90 | 91 | it('Should return the Info object if info is defined with minimal fields', () => { 92 | 93 | const schema = { 94 | info: { 95 | title: 'My API', 96 | version: '1.0.0' 97 | } 98 | }; 99 | 100 | const info = parser.parse(schema); 101 | 102 | const expectedInfo = new Info(schema.info); 103 | 104 | assert.deepStrictEqual(info, expectedInfo); 105 | }); 106 | 107 | it('Should return the Info object if info is defined with every field', () => { 108 | 109 | const schema = { 110 | info: { 111 | title: 'My API', 112 | version: '1.0.0', 113 | contact: { 114 | name: 'John Doe', 115 | url: 'https://example.com/contact', 116 | email: 'john.doe@example.com' 117 | }, 118 | license: { 119 | name: 'MIT', 120 | url: 'https://example.com/license' 121 | } 122 | } 123 | }; 124 | 125 | const info = parser.parse(schema); 126 | 127 | const expectedInfo = new Info(schema.info); 128 | 129 | assert.deepStrictEqual(info, expectedInfo); 130 | }); 131 | 132 | it('Should mantain the specification extension properties', () => { 133 | 134 | const schema = { 135 | info: { 136 | title: 'My API', 137 | version: '1.0.0', 138 | 'x-foo': 'bar', 139 | 'x-baz': { 140 | test: [1, 2, 3] 141 | } 142 | } 143 | }; 144 | 145 | const info = parser.parse(schema); 146 | 147 | const expectedInfo = new Info(schema.info, [ 148 | ['x-foo', 'bar'], 149 | ['x-baz', { test: [1, 2, 3] }] 150 | ]); 151 | 152 | assert.deepStrictEqual(info, expectedInfo); 153 | }); 154 | 155 | }); 156 | 157 | }); 158 | -------------------------------------------------------------------------------- /tests/mocker/express/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sandbox = require('sinon').createSandbox(); 4 | const expressApp = require('express')(); 5 | const mockRequire = require('mock-require'); 6 | 7 | const expressAppMock = sandbox.stub(expressApp); 8 | mockRequire('express', () => expressAppMock); 9 | 10 | const Server = require('../../../lib/mocker/express/server'); 11 | 12 | describe('Mocker', () => { 13 | 14 | describe('Express server', () => { 15 | 16 | after(() => { 17 | mockRequire.stopAll(); 18 | sandbox.restore(); 19 | }); 20 | 21 | afterEach(() => { 22 | sandbox.reset(); 23 | sandbox.resetBehavior(); 24 | sandbox.resetHistory(); 25 | }); 26 | 27 | it('Should listen at a random port by default', () => { 28 | 29 | const fakeServerOn = sandbox.fake(); 30 | 31 | expressAppMock.listen.returns({ 32 | on: fakeServerOn 33 | }); 34 | 35 | const server = new Server(); 36 | server.init(); 37 | 38 | sandbox.assert.calledOnce(expressAppMock.listen); 39 | sandbox.assert.calledWithExactly(expressAppMock.listen.getCall(0), undefined); 40 | }); 41 | 42 | it('Should listen at the correct port if it\'s set', () => { 43 | 44 | const fakeServerOn = sandbox.fake(); 45 | 46 | expressAppMock.listen.returns({ 47 | on: fakeServerOn 48 | }); 49 | 50 | const server = new Server(); 51 | server.setPort(8000); 52 | server.init(); 53 | 54 | sandbox.assert.calledTwice(expressAppMock.listen); 55 | sandbox.assert.calledWithExactly(expressAppMock.listen.getCall(1), 8000); 56 | }); 57 | 58 | it('Should set the catch all request for not found resources', () => { 59 | 60 | const fakeServerOn = sandbox.fake(); 61 | 62 | expressAppMock.listen.returns({ 63 | on: fakeServerOn 64 | }); 65 | 66 | const server = new Server(); 67 | server.init(); 68 | 69 | sandbox.assert.calledThrice(expressAppMock.all); 70 | sandbox.assert.calledWithExactly(expressAppMock.all.getCall(2), '*', sandbox.match.func); 71 | }); 72 | 73 | it('Should set the request handlers for the passed paths', () => { 74 | 75 | const simplePath = { 76 | httpMethod: 'get', 77 | uri: '/hello-world', 78 | getResponse: () => ({ message: 'Hi there!' }) 79 | }; 80 | 81 | const pathWithOneVariable = { 82 | httpMethod: 'post', 83 | uri: '/hello-world/{world}', 84 | getResponse: () => ({ message: 'Hi there again!' }) 85 | }; 86 | 87 | const pathWithVariables = { 88 | httpMethod: 'post', 89 | uri: '/hello-world/{world}/planet/{myPlanet}', 90 | getResponse: () => ({ message: 'Hi there again!' }) 91 | }; 92 | 93 | const pathWithUnderscoredVariable = { 94 | httpMethod: 'post', 95 | uri: '/hello-world/{my_world}', 96 | getResponse: () => ({ message: 'Hi there again!' }) 97 | }; 98 | 99 | const fakeServerOn = sandbox.fake(); 100 | 101 | expressAppMock.listen.returns({ 102 | on: fakeServerOn 103 | }); 104 | 105 | const server = new Server(); 106 | server.setPaths([simplePath, pathWithOneVariable, pathWithVariables, pathWithUnderscoredVariable]); 107 | server.init(); 108 | 109 | sandbox.assert.calledOnce(expressAppMock.get); 110 | sandbox.assert.calledWithExactly(expressAppMock.get.getCall(0), ['/hello-world'], sandbox.match.func); 111 | sandbox.assert.calledThrice(expressAppMock.post); 112 | sandbox.assert.calledWithExactly(expressAppMock.post.getCall(0), ['/hello-world/:world'], sandbox.match.func); 113 | sandbox.assert.calledWithExactly(expressAppMock.post.getCall(1), ['/hello-world/:world/planet/:myPlanet'], sandbox.match.func); 114 | sandbox.assert.calledWithExactly(expressAppMock.post.getCall(2), ['/hello-world/:my_world'], sandbox.match.func); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /tests/open-api-mocker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | 3 | 'use strict'; 4 | 5 | const assert = require('assert'); 6 | const fs = require('fs'); 7 | const EventEmitter = require('events'); 8 | 9 | const sinon = require('sinon'); 10 | const YAML = require('js-yaml'); 11 | 12 | const schema = YAML.load(fs.readFileSync('./tests/resources/pet-store.yml')); 13 | 14 | const OpenApiMocker = require('../lib/open-api-mocker'); 15 | const Server = require('../lib/mocker/express/server'); 16 | const ExplicitSchemaLoader = require('../lib/schema-loaders/explicit-loader'); 17 | const LocalSchemaLoader = require('../lib/schema-loaders/local-loader'); 18 | 19 | class CustomServer { 20 | 21 | setServers() { return this; } 22 | 23 | setPort() { return this; } 24 | 25 | setPaths() { return this; } 26 | 27 | init() {} 28 | } 29 | 30 | class CustomSchemaLoader extends EventEmitter { 31 | load() { return schema; } 32 | } 33 | 34 | class CustomSchemaLoaderWithWatch extends EventEmitter { 35 | 36 | load() { return schema; } 37 | 38 | watch() {} 39 | } 40 | 41 | describe('OpenAPI Mocker', () => { 42 | 43 | beforeEach(() => { 44 | sinon.spy(Server.prototype, 'setServers'); 45 | sinon.spy(Server.prototype, 'setPort'); 46 | sinon.spy(Server.prototype, 'setPaths'); 47 | sinon.stub(Server.prototype, 'init'); 48 | sinon.stub(Server.prototype, 'shutdown'); 49 | }); 50 | 51 | afterEach(() => { 52 | sinon.restore(); 53 | }); 54 | 55 | describe('Servers', () => { 56 | 57 | context('Default server', () => { 58 | it('Should set the parameters to the built-in express server by default', async () => { 59 | 60 | const openApiMocker = new OpenApiMocker({}); 61 | openApiMocker.setSchema(schema); 62 | 63 | await openApiMocker.validate(); 64 | await openApiMocker.mock(); 65 | 66 | sinon.assert.calledOnce(Server.prototype.setServers); 67 | sinon.assert.calledOnce(Server.prototype.setPort); 68 | sinon.assert.calledOnce(Server.prototype.setPaths); 69 | sinon.assert.calledOnce(Server.prototype.init); 70 | }); 71 | }); 72 | 73 | context('Custom server', () => { 74 | it('Should use the server instance passed as server option', async () => { 75 | 76 | sinon.spy(CustomServer.prototype, 'setServers'); 77 | sinon.spy(CustomServer.prototype, 'setPort'); 78 | sinon.spy(CustomServer.prototype, 'setPaths'); 79 | sinon.stub(CustomServer.prototype, 'init'); 80 | 81 | const openApiMocker = new OpenApiMocker({ server: new CustomServer() }); 82 | openApiMocker.setSchema(schema); 83 | 84 | await openApiMocker.validate(); 85 | await openApiMocker.mock(); 86 | 87 | sinon.assert.calledOnce(CustomServer.prototype.setServers); 88 | sinon.assert.calledOnce(CustomServer.prototype.setPort); 89 | sinon.assert.calledOnce(CustomServer.prototype.setPaths); 90 | sinon.assert.calledOnce(CustomServer.prototype.init); 91 | 92 | // Should not use the default server 93 | sinon.assert.notCalled(Server.prototype.setServers); 94 | sinon.assert.notCalled(Server.prototype.setPort); 95 | sinon.assert.notCalled(Server.prototype.setPaths); 96 | sinon.assert.notCalled(Server.prototype.init); 97 | }); 98 | }); 99 | 100 | }); 101 | 102 | describe('Schema loaders', () => { 103 | 104 | context('Common behaviour', () => { 105 | 106 | beforeEach(() => { 107 | sinon.stub(ExplicitSchemaLoader.prototype, 'load').returns(schema); 108 | }); 109 | 110 | it('Should throw an error while validating if passed schema is invalid', async () => { 111 | 112 | const invalidSchema = ['invalid']; 113 | 114 | ExplicitSchemaLoader.prototype.load.returns(invalidSchema); 115 | 116 | const openApiMocker = new OpenApiMocker({ 117 | schema: invalidSchema 118 | }); 119 | 120 | await assert.rejects(openApiMocker.validate()); 121 | }); 122 | }); 123 | 124 | context('Default loaders', () => { 125 | 126 | beforeEach(() => { 127 | sinon.stub(ExplicitSchemaLoader.prototype, 'load').returns(schema); 128 | sinon.stub(LocalSchemaLoader.prototype, 'load').resolves(schema); 129 | sinon.stub(LocalSchemaLoader.prototype, 'watch'); 130 | }); 131 | 132 | it('Should use the built-in explicit schema loader if a literal schema object is passed', async () => { 133 | 134 | sinon.spy(CustomSchemaLoader.prototype, 'load'); 135 | 136 | const openApiMocker = new OpenApiMocker({ 137 | schema 138 | }); 139 | 140 | await openApiMocker.validate(); 141 | await openApiMocker.mock(); 142 | 143 | sinon.assert.calledOnce(ExplicitSchemaLoader.prototype.load); 144 | sinon.assert.notCalled(LocalSchemaLoader.prototype.load); 145 | }); 146 | 147 | it('Should use the built-in explicit schema loader if a string schema (path) is passed', async () => { 148 | 149 | sinon.spy(CustomSchemaLoader.prototype, 'load'); 150 | 151 | const openApiMocker = new OpenApiMocker({ 152 | schema: 'path/to/schema' 153 | }); 154 | 155 | await openApiMocker.validate(); 156 | await openApiMocker.mock(); 157 | 158 | sinon.assert.calledOnce(LocalSchemaLoader.prototype.load); 159 | sinon.assert.notCalled(ExplicitSchemaLoader.prototype.load); 160 | }); 161 | }); 162 | 163 | context('Custom loaders', () => { 164 | 165 | it('Should use a custom schema loader if it is provided', async () => { 166 | 167 | sinon.spy(CustomSchemaLoader.prototype, 'load'); 168 | 169 | const openApiMocker = new OpenApiMocker({ 170 | schemaLoader: new CustomSchemaLoader(), 171 | schema: 'someSchemaHint' 172 | }); 173 | 174 | await openApiMocker.validate(); 175 | await openApiMocker.mock(); 176 | 177 | sinon.assert.calledOnce(CustomSchemaLoader.prototype.load); 178 | }); 179 | 180 | it('Should call the watch method of the schema loader if watch option is passed', async () => { 181 | 182 | sinon.spy(CustomSchemaLoaderWithWatch.prototype, 'load'); 183 | sinon.spy(CustomSchemaLoaderWithWatch.prototype, 'watch'); 184 | 185 | const openApiMocker = new OpenApiMocker({ 186 | schemaLoader: new CustomSchemaLoaderWithWatch(), 187 | schema: 'someSchemaHint', 188 | watch: true 189 | }); 190 | 191 | await openApiMocker.validate(); 192 | await openApiMocker.mock(); 193 | 194 | sinon.assert.calledOnce(CustomSchemaLoaderWithWatch.prototype.load); 195 | sinon.assert.calledOnce(CustomSchemaLoaderWithWatch.prototype.watch); 196 | }); 197 | 198 | it('Should not call the watch method of the schema loader if watch option is passed but loader does not support it', async () => { 199 | 200 | sinon.spy(CustomSchemaLoader.prototype, 'load'); 201 | 202 | const openApiMocker = new OpenApiMocker({ 203 | schemaLoader: new CustomSchemaLoader(), 204 | schema: 'someSchemaHint' 205 | }); 206 | 207 | await openApiMocker.validate(); 208 | await openApiMocker.mock(); 209 | 210 | sinon.assert.calledOnce(CustomSchemaLoader.prototype.load); 211 | }); 212 | 213 | it('Should not call the watch method of the schema loader if watch option is passed as false and loader supports it', async () => { 214 | 215 | sinon.spy(CustomSchemaLoaderWithWatch.prototype, 'load'); 216 | sinon.spy(CustomSchemaLoaderWithWatch.prototype, 'watch'); 217 | 218 | const openApiMocker = new OpenApiMocker({ 219 | schemaLoader: new CustomSchemaLoaderWithWatch(), 220 | schema: 'someSchemaHint', 221 | watch: false 222 | }); 223 | 224 | await openApiMocker.validate(); 225 | await openApiMocker.mock(); 226 | 227 | sinon.assert.calledOnce(CustomSchemaLoaderWithWatch.prototype.load); 228 | sinon.assert.notCalled(CustomSchemaLoaderWithWatch.prototype.watch); 229 | }); 230 | 231 | }); 232 | 233 | context('Watch feature', () => { 234 | 235 | let clock; 236 | 237 | beforeEach(() => { 238 | clock = sinon.useFakeTimers(); 239 | sinon.stub(ExplicitSchemaLoader.prototype, 'load').returns(schema); 240 | sinon.stub(ExplicitSchemaLoader.prototype, 'removeAllListeners'); 241 | sinon.stub(LocalSchemaLoader.prototype, 'load').returns(schema); 242 | sinon.stub(LocalSchemaLoader.prototype, 'removeAllListeners'); 243 | sinon.stub(LocalSchemaLoader.prototype, 'watch'); 244 | }); 245 | 246 | it('Should revalidate and mock the service again if schema loader emits the schema-changed event', async () => { 247 | 248 | const newSchema = { 249 | ...schema, 250 | info: { 251 | ...schema.info, 252 | title: 'New title' 253 | } 254 | }; 255 | 256 | class CustomSchemaLoaderWithRealWatch extends CustomSchemaLoader { 257 | watch() { 258 | setTimeout(() => this.emit('schema-changed'), 1000); 259 | } 260 | } 261 | 262 | sinon.spy(OpenApiMocker.prototype, 'setSchema'); 263 | sinon.spy(OpenApiMocker.prototype, 'validate'); 264 | sinon.spy(OpenApiMocker.prototype, 'mock'); 265 | const loadStub = sinon.stub(CustomSchemaLoaderWithRealWatch.prototype, 'load'); 266 | loadStub.onCall(0).returns(schema); 267 | loadStub.onCall(1).returns(newSchema); 268 | 269 | const openApiMocker = new OpenApiMocker({ 270 | schemaLoader: new CustomSchemaLoaderWithRealWatch(), 271 | schema, 272 | watch: true 273 | }); 274 | 275 | await openApiMocker.validate(); 276 | await openApiMocker.mock(); 277 | 278 | sinon.assert.calledOnceWithExactly(OpenApiMocker.prototype.setSchema, schema); 279 | sinon.assert.calledOnce(CustomSchemaLoaderWithRealWatch.prototype.load); 280 | sinon.assert.calledOnce(OpenApiMocker.prototype.validate); 281 | sinon.assert.calledOnce(OpenApiMocker.prototype.mock); 282 | 283 | // Tick the clock and wait for the event loop to be empty 284 | await clock.tickAsync(1000); 285 | 286 | sinon.assert.calledTwice(OpenApiMocker.prototype.setSchema); 287 | sinon.assert.calledWithExactly(OpenApiMocker.prototype.setSchema.getCall(1), schema); 288 | sinon.assert.calledTwice(CustomSchemaLoaderWithRealWatch.prototype.load); 289 | sinon.assert.calledTwice(OpenApiMocker.prototype.validate); 290 | sinon.assert.calledTwice(OpenApiMocker.prototype.mock); 291 | }); 292 | 293 | }); 294 | 295 | context('Shoutdown', () => { 296 | 297 | it('Should call server shutdown and wait until it finishes', async () => { 298 | 299 | const openApiMocker = new OpenApiMocker({ 300 | schema 301 | }); 302 | 303 | await openApiMocker.validate(); 304 | await openApiMocker.mock(); 305 | await openApiMocker.shutdown(); 306 | 307 | sinon.assert.calledOnce(Server.prototype.shutdown); 308 | }); 309 | 310 | }); 311 | 312 | }); 313 | 314 | }); 315 | -------------------------------------------------------------------------------- /tests/openapi/openapi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ParserError = require('../../lib/errors/parser-error'); 6 | const { Parser, Openapi } = require('../../lib/openapi'); 7 | 8 | describe('Openapi', () => { 9 | 10 | describe('Schema parse', () => { 11 | 12 | const parser = new Parser(); 13 | 14 | it('Should throw a ParserError if openapi is not defined', () => { 15 | 16 | const schema = {}; 17 | 18 | assert.throws(() => parser.parse(schema), ParserError); 19 | }); 20 | 21 | it('Should throw a ParserError if openapi is not a string', () => { 22 | 23 | const schema = { 24 | openapi: { foo: 'bar' } 25 | }; 26 | 27 | assert.throws(() => parser.parse(schema), ParserError); 28 | }); 29 | 30 | it('Should throw a ParserError if openapi is not a valid version', () => { 31 | 32 | const schema = { 33 | openapi: '2.0.0' 34 | }; 35 | 36 | assert.throws(() => parser.parse(schema), ParserError); 37 | }); 38 | 39 | it('Should return an array with the correct server if one server without variables is defined', () => { 40 | 41 | const openapi = parser.parse({ 42 | openapi: '3.0.0' 43 | }); 44 | 45 | const expectedOpenapi = new Openapi('3.0.0'); 46 | 47 | assert.deepStrictEqual(openapi, expectedOpenapi); 48 | }); 49 | 50 | it('Should pass for any 3.x version', () => { 51 | 52 | const openapi = parser.parse({ 53 | openapi: '3.1.3' 54 | }); 55 | 56 | const expectedOpenapi = new Openapi('3.1.3'); 57 | 58 | assert.deepStrictEqual(openapi, expectedOpenapi); 59 | }); 60 | 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /tests/resources/pet-store.yml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | name: Swagger API Team 9 | email: apiteam@swagger.io 10 | url: http://swagger.io 11 | license: 12 | name: Apache 2.0 13 | url: https://www.apache.org/licenses/LICENSE-2.0.html 14 | servers: 15 | - url: http://petstore.swagger.io/api 16 | paths: 17 | /pets: 18 | get: 19 | description: | 20 | Returns all pets from the system that the user has access to 21 | Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. 22 | 23 | Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. 24 | operationId: findPets 25 | parameters: 26 | - name: tags 27 | in: query 28 | description: tags to filter by 29 | required: false 30 | style: form 31 | schema: 32 | type: array 33 | items: 34 | type: string 35 | - name: limit 36 | in: query 37 | description: maximum number of results to return 38 | required: false 39 | schema: 40 | type: integer 41 | format: int32 42 | responses: 43 | '200': 44 | description: pet response 45 | content: 46 | application/json: 47 | schema: 48 | type: array 49 | items: 50 | $ref: '#/components/schemas/Pet' 51 | default: 52 | description: unexpected error 53 | content: 54 | application/json: 55 | schema: 56 | $ref: '#/components/schemas/Error' 57 | post: 58 | description: Creates a new pet in the store. Duplicates are allowed 59 | operationId: addPet 60 | requestBody: 61 | description: Pet to add to the store 62 | required: true 63 | content: 64 | application/json: 65 | schema: 66 | $ref: '#/components/schemas/NewPet' 67 | responses: 68 | '200': 69 | description: pet response 70 | content: 71 | application/json: 72 | schema: 73 | $ref: '#/components/schemas/Pet' 74 | default: 75 | description: unexpected error 76 | content: 77 | application/json: 78 | schema: 79 | $ref: '#/components/schemas/Error' 80 | /pets/{id}: 81 | get: 82 | description: Returns a user based on a single ID, if the user does not have access to the pet 83 | operationId: find pet by id 84 | parameters: 85 | - name: id 86 | in: path 87 | description: ID of pet to fetch 88 | required: true 89 | schema: 90 | type: integer 91 | format: int64 92 | responses: 93 | '200': 94 | description: pet response 95 | content: 96 | application/json: 97 | schema: 98 | $ref: '#/components/schemas/Pet' 99 | default: 100 | description: unexpected error 101 | content: 102 | application/json: 103 | schema: 104 | $ref: '#/components/schemas/Error' 105 | delete: 106 | description: deletes a single pet based on the ID supplied 107 | operationId: deletePet 108 | parameters: 109 | - name: id 110 | in: path 111 | description: ID of pet to delete 112 | required: true 113 | schema: 114 | type: integer 115 | format: int64 116 | responses: 117 | '204': 118 | description: pet deleted 119 | default: 120 | description: unexpected error 121 | content: 122 | application/json: 123 | schema: 124 | $ref: '#/components/schemas/Error' 125 | components: 126 | schemas: 127 | Pet: 128 | allOf: 129 | - $ref: '#/components/schemas/NewPet' 130 | - required: 131 | - id 132 | properties: 133 | id: 134 | type: integer 135 | format: int64 136 | 137 | NewPet: 138 | required: 139 | - name 140 | properties: 141 | name: 142 | type: string 143 | tag: 144 | type: string 145 | 146 | Error: 147 | required: 148 | - code 149 | - message 150 | properties: 151 | code: 152 | type: integer 153 | format: int32 154 | message: 155 | type: string -------------------------------------------------------------------------------- /tests/response-generator/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | 6 | const ResponseGenerator = require('../../lib/response-generator'); 7 | const getFakerLocale = require('../../lib/utils/get-faker-locale'); 8 | 9 | const faker = getFakerLocale(); 10 | 11 | describe('Response Generator', () => { 12 | beforeEach(() => { 13 | sinon.restore(); 14 | }); 15 | 16 | describe('Generate', () => { 17 | 18 | it('Should return the example if it\'s defined', () => { 19 | 20 | const responseSchema = { 21 | example: { 22 | foo: 'bar' 23 | } 24 | }; 25 | 26 | const response = ResponseGenerator.generate(responseSchema); 27 | 28 | assert.deepStrictEqual(response, { 29 | foo: 'bar' 30 | }); 31 | }); 32 | 33 | it('Should return the first example if examples is defined', () => { 34 | 35 | const responseSchema = { 36 | examples: { 37 | first: { 38 | value: { 39 | foo: 'bar' 40 | } 41 | } 42 | } 43 | }; 44 | 45 | const response = ResponseGenerator.generate(responseSchema); 46 | 47 | assert.deepStrictEqual(response, { 48 | foo: 'bar' 49 | }); 50 | }); 51 | 52 | it('Should throw if examples is defined but example has no value', () => { 53 | 54 | const responseSchema = { 55 | examples: { 56 | first: { 57 | foo: 'bar' 58 | } 59 | } 60 | }; 61 | 62 | assert.throws(() => ResponseGenerator.generate(responseSchema)); 63 | }); 64 | 65 | it('Should return the first example if examples is defined & preferred example value undefined', () => { 66 | 67 | const responseSchema = { 68 | examples: { 69 | first: { 70 | value: { 71 | yes: 'no' 72 | } 73 | }, 74 | second: { 75 | hello: 'goodbye' 76 | } 77 | } 78 | }; 79 | 80 | const response = ResponseGenerator.generate(responseSchema, 'second'); 81 | 82 | assert.deepStrictEqual(response, { 83 | yes: 'no' 84 | }); 85 | }); 86 | 87 | it('Should return the preferred example if prefer header is set', () => { 88 | 89 | const responseSchema = { 90 | examples: { 91 | cat: { 92 | summary: 'An example of a cat', 93 | value: { 94 | name: 'Fluffy', 95 | petType: 'Cat', 96 | color: 'White', 97 | gender: 'male', 98 | breed: 'Persian' 99 | } 100 | }, 101 | dog: { 102 | summary: 'An example of a dog with a cat\'s name', 103 | value: { 104 | name: 'Puma', 105 | petType: 'Dog', 106 | color: 'Black', 107 | gender: 'Female', 108 | breed: 'Mixed' 109 | } 110 | } 111 | } 112 | }; 113 | 114 | const response = ResponseGenerator.generate(responseSchema, 'dog'); 115 | 116 | assert.deepStrictEqual(response, { 117 | name: 'Puma', 118 | petType: 'Dog', 119 | color: 'Black', 120 | gender: 'Female', 121 | breed: 'Mixed' 122 | }); 123 | }); 124 | 125 | it('Should return the schema\'s example if it\'s defined', () => { 126 | 127 | const responseSchema = { 128 | schema: { 129 | example: { 130 | foo: 'bar' 131 | } 132 | } 133 | }; 134 | 135 | const response = ResponseGenerator.generate(responseSchema); 136 | 137 | assert.deepStrictEqual(response, { 138 | foo: 'bar' 139 | }); 140 | }); 141 | 142 | it('Should return null if the schema\'s example if is defined as null', () => { 143 | 144 | const responseSchema = { 145 | schema: { 146 | type: 'object', 147 | properties: { 148 | foo: { 149 | type: 'string', 150 | example: null 151 | } 152 | } 153 | } 154 | }; 155 | 156 | const response = ResponseGenerator.generate(responseSchema); 157 | 158 | assert.deepStrictEqual(response, { 159 | foo: null 160 | }); 161 | }); 162 | 163 | it('Should return the schema\'s first example if examples is defined', () => { 164 | 165 | const responseSchema = { 166 | schema: { 167 | examples: [{ 168 | foo: 'bar' 169 | }] 170 | } 171 | }; 172 | 173 | const response = ResponseGenerator.generate(responseSchema); 174 | 175 | assert.deepStrictEqual(response, { 176 | foo: 'bar' 177 | }); 178 | }); 179 | 180 | it('Should return the an element from the enum if enum is defined', () => { 181 | 182 | const responseSchema = { 183 | enum: ['foo', 'bar', 'baz'] 184 | }; 185 | 186 | const response = ResponseGenerator.generate(responseSchema); 187 | 188 | assert(['foo', 'bar', 'baz'].includes(response)); 189 | }); 190 | 191 | it('Should return a number if type is defined as number', () => { 192 | 193 | const responseSchema = { 194 | type: 'number' 195 | }; 196 | 197 | const response = ResponseGenerator.generate(responseSchema); 198 | 199 | assert.strictEqual(typeof response, 'number'); 200 | }); 201 | 202 | it('Should return an integer if type is defined as integer', () => { 203 | 204 | const responseSchema = { 205 | type: 'integer' 206 | }; 207 | 208 | const response = ResponseGenerator.generate(responseSchema); 209 | 210 | assert.strictEqual(typeof response, 'number'); 211 | assert.strictEqual(response, response | 1); 212 | }); 213 | 214 | it('Should return a not-empty array if type is defined as array', () => { 215 | 216 | const responseSchema = { 217 | type: 'array', 218 | items: { 219 | type: 'integer' 220 | } 221 | }; 222 | 223 | const response = ResponseGenerator.generate(responseSchema); 224 | 225 | assert.strictEqual(typeof response, 'object'); 226 | assert.ok(Array.isArray(response)); 227 | assert.strictEqual(response[0], response[0] | 1); 228 | }); 229 | 230 | it('Should return an array with specified number of items if type is defined as array and x-count extension is specified', () => { 231 | 232 | const responseSchema = { 233 | type: 'array', 234 | 'x-count': 2, 235 | items: { 236 | type: 'integer' 237 | } 238 | }; 239 | 240 | const response = ResponseGenerator.generate(responseSchema); 241 | 242 | assert.deepStrictEqual(response, [1, 1]); 243 | }); 244 | 245 | it('Should return an empty object if type is defined as object without any other props', () => { 246 | 247 | const responseSchema = { 248 | type: 'object' 249 | }; 250 | 251 | const response = ResponseGenerator.generate(responseSchema); 252 | 253 | assert.deepStrictEqual(response, {}); 254 | }); 255 | 256 | it('Should return the schema example if type is defined as object with an example property', () => { 257 | 258 | const responseSchema = { 259 | type: 'object', 260 | example: { foo: 'bar' } 261 | }; 262 | 263 | const response = ResponseGenerator.generate(responseSchema); 264 | 265 | assert.deepStrictEqual(response, { foo: 'bar' }); 266 | }); 267 | 268 | it('Should return all schemas merged if the allOf property is defined', () => { 269 | 270 | const responseSchema = { 271 | schema: { 272 | allOf: [ 273 | { 274 | type: 'object', 275 | example: { foo: 'bar' } 276 | }, 277 | { 278 | type: 'object', 279 | example: { baz: 'yeah' } 280 | } 281 | ] 282 | } 283 | }; 284 | 285 | const response = ResponseGenerator.generate(responseSchema); 286 | 287 | assert.deepStrictEqual(response, { 288 | foo: 'bar', 289 | baz: 'yeah' 290 | }); 291 | }); 292 | 293 | it('Should return the first schema if the oneOf property is defined', () => { 294 | 295 | const responseSchema = { 296 | schema: { 297 | oneOf: [ 298 | { 299 | type: 'object', 300 | example: { foo: 'bar' } 301 | }, 302 | { 303 | type: 'object', 304 | example: { baz: 'yeah' } 305 | } 306 | ] 307 | } 308 | }; 309 | 310 | const response = ResponseGenerator.generate(responseSchema); 311 | 312 | assert.deepStrictEqual(response, { 313 | foo: 'bar' 314 | }); 315 | }); 316 | 317 | it('Should return the first schema if the anyOf property is defined', () => { 318 | 319 | const responseSchema = { 320 | schema: { 321 | anyOf: [ 322 | { 323 | type: 'object', 324 | example: { foo: 'bar' } 325 | }, 326 | { 327 | type: 'object', 328 | example: { baz: 'yeah' } 329 | } 330 | ] 331 | } 332 | }; 333 | 334 | const response = ResponseGenerator.generate(responseSchema); 335 | 336 | assert.deepStrictEqual(response, { 337 | foo: 'bar' 338 | }); 339 | }); 340 | 341 | it('Should return the first schema if the anyOf property is defined as array items schema', () => { 342 | 343 | const responseSchema = { 344 | schema: { 345 | type: 'array', 346 | items: { 347 | anyOf: [ 348 | { 349 | type: 'object', 350 | example: { foo: 'bar' } 351 | }, 352 | { 353 | type: 'object', 354 | example: { baz: 'yeah' } 355 | } 356 | ] 357 | } 358 | } 359 | }; 360 | 361 | const response = ResponseGenerator.generate(responseSchema); 362 | 363 | assert.deepStrictEqual(response, [{ 364 | foo: 'bar' 365 | }]); 366 | }); 367 | 368 | it('Should throw if an invalid type is defined', () => { 369 | 370 | const responseSchema = { 371 | type: 'invalidType' 372 | }; 373 | 374 | assert.throws(() => ResponseGenerator.generate(responseSchema)); 375 | }); 376 | 377 | it('Should throw if an invalid schema is passed', () => { 378 | 379 | const responseSchema = {}; 380 | 381 | assert.throws(() => ResponseGenerator.generate(responseSchema)); 382 | }); 383 | 384 | it('Should return a generated response if a complex schema is defined', () => { 385 | 386 | const responseSchema = { 387 | schema: { 388 | type: 'object', 389 | properties: { 390 | foo: { 391 | type: 'string' 392 | }, 393 | bar: { 394 | type: 'array', 395 | items: { 396 | type: 'number' 397 | } 398 | }, 399 | baz: { 400 | type: 'boolean' 401 | }, 402 | yeah: { 403 | type: 'object', 404 | properties: { 405 | test1: { 406 | type: 'string', 407 | example: 'Hi' 408 | }, 409 | test2: { 410 | type: 'array', 411 | items: { 412 | type: 'number', 413 | enum: [10, 20, 30] 414 | } 415 | } 416 | } 417 | }, 418 | employee: { 419 | schema: { 420 | allOf: [ 421 | { 422 | type: 'object', 423 | properties: { 424 | name: { 425 | type: 'string', 426 | example: 'John' 427 | }, 428 | age: { 429 | type: 'integer', 430 | example: 30 431 | } 432 | } 433 | }, 434 | { 435 | type: 'object', 436 | properties: { 437 | employeeId: { 438 | type: 'string', 439 | example: '0001222-B' 440 | } 441 | } 442 | } 443 | ] 444 | } 445 | } 446 | } 447 | } 448 | }; 449 | 450 | const response = ResponseGenerator.generate(responseSchema); 451 | 452 | sinon.assert.match(response, { 453 | foo: 'string', 454 | bar: [1], 455 | baz: true, 456 | yeah: { 457 | test1: 'Hi', 458 | test2: [sinon.match.in([10, 20, 30])] 459 | }, 460 | employee: { 461 | name: 'John', 462 | age: 30, 463 | employeeId: '0001222-B' 464 | } 465 | }); 466 | }); 467 | 468 | it('Should return a generated response with value generated using relevant faker method if x-faker extension is ' + 469 | 'present in and method exists in faker', () => { 470 | sinon.stub(faker.person, 'firstName').returns('bob'); 471 | const responseSchema = { 472 | type: 'string', 473 | 'x-faker': 'person.firstName' 474 | }; 475 | 476 | const response = ResponseGenerator.generate(responseSchema); 477 | 478 | assert.strictEqual(response, 'bob'); 479 | 480 | sinon.assert.calledOnceWithExactly(faker.person.firstName); 481 | }); 482 | 483 | it('Should return a generated response with date in ISO format if type is date and x-faker is used', () => { 484 | sinon.stub(faker.date, 'recent').returns(new Date(Date.UTC(2000, 0, 1))); 485 | const responseSchema = { 486 | type: 'date-time', 487 | 'x-faker': 'date.recent' 488 | }; 489 | 490 | const response = ResponseGenerator.generate(responseSchema); 491 | 492 | assert.strictEqual(response.toISOString(), '2000-01-01T00:00:00.000Z'); 493 | 494 | sinon.assert.calledOnceWithExactly(faker.date.recent); 495 | }); 496 | 497 | it('Should return a generated response with standard primitive value if x-faker field is ' + 498 | 'present but method does not exist in faker', () => { 499 | const responseSchema = { 500 | type: 'string', 501 | 'x-faker': 'idonotexist' 502 | }; 503 | 504 | const response = ResponseGenerator.generate(responseSchema); 505 | 506 | assert.strictEqual(response, 'string'); 507 | }); 508 | 509 | it('Should return a generated response with string value built using composite faker methods if ' + 510 | 'x-faker extension includes mustache template string', 511 | () => { 512 | sinon 513 | .stub(faker.helpers, 'fake') 514 | .returns('1+2'); 515 | 516 | const responseSchema = { 517 | type: 'string', 518 | 'x-faker': '{{number.int}}+{{number.int}}' 519 | }; 520 | 521 | const response = ResponseGenerator.generate(responseSchema); 522 | 523 | assert.strictEqual(response, '1+2'); 524 | sinon.assert.calledOnceWithExactly(faker.helpers.fake, '{{number.int}}+{{number.int}}'); 525 | }); 526 | 527 | it('Should return a generated response with standard primitive value if x-faker field is not in the namespace.method format', () => { 528 | const responseSchema = { 529 | type: 'string', 530 | 'x-faker': 'random' 531 | }; 532 | 533 | const response = ResponseGenerator.generate(responseSchema); 534 | 535 | assert.strictEqual(response, 'string'); 536 | }); 537 | 538 | it('Should return a generated response with standard primitive value if x-faker field contains an invalid faker namespace', () => { 539 | const responseSchema = { 540 | type: 'string', 541 | 'x-faker': 'randum.number' 542 | }; 543 | 544 | const response = ResponseGenerator.generate(responseSchema); 545 | 546 | assert.strictEqual(response, 'string'); 547 | }); 548 | 549 | it('Should return a generated response with standard primitive value if x-faker field contains an invalid faker method', () => { 550 | const responseSchema = { 551 | type: 'string', 552 | 'x-faker': 'random.numbr' 553 | }; 554 | 555 | const response = ResponseGenerator.generate(responseSchema); 556 | 557 | assert.strictEqual(response, 'string'); 558 | }); 559 | 560 | it('Should return a generated response with value from faker when x-faker extension contains valid faker namespace, method and arguments', () => { 561 | sinon.stub(faker.number, 'int').returns(1); 562 | const responseSchema = { 563 | type: 'integer', 564 | 'x-faker': 'number.int({ "max": 5 })' 565 | }; 566 | 567 | const response = ResponseGenerator.generate(responseSchema); 568 | 569 | assert.strictEqual(response, 1); 570 | 571 | sinon.assert.calledOnceWithExactly(faker.number.int, { max: 5 }); 572 | }); 573 | }); 574 | }); 575 | -------------------------------------------------------------------------------- /tests/schema-loaders/local-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const EventEmitter = require('events'); 7 | 8 | const sinon = require('sinon'); 9 | const chokidar = require('chokidar'); 10 | 11 | const LocalSchemaLoader = require('../../lib/schema-loaders/local-loader'); 12 | 13 | const changeYamlToJson = string => string.replace(/\.yaml$/, '.json'); 14 | 15 | describe('Schema Loaders', () => { 16 | describe('Local Schema Loader', () => { 17 | 18 | describe('load()', () => { 19 | 20 | beforeEach(() => { 21 | sinon.stub(fs, 'accessSync'); 22 | sinon.stub(fs, 'readFileSync'); 23 | }); 24 | 25 | afterEach(() => sinon.restore()); 26 | 27 | const fakePath = 'path/to/schema.yaml'; 28 | const fakeFullPath = path.join(process.cwd(), 'path/to/schema.yaml'); 29 | 30 | it('Should throw if the schema does not exist', () => { 31 | fs.accessSync.throws(new Error('File not found')); 32 | 33 | const schemaLoader = new LocalSchemaLoader(); 34 | assert.throws(() => schemaLoader.load(fakePath), { 35 | name: 'OpenAPISchemaNotFound' 36 | }); 37 | 38 | sinon.assert.calledOnceWithExactly(fs.accessSync, fakeFullPath, fs.constants.R_OK); 39 | sinon.assert.notCalled(fs.readFileSync); 40 | }); 41 | 42 | it('Should throw if the schema fails to be read', () => { 43 | fs.readFileSync.throws(new Error('File could not be read')); 44 | 45 | const schemaLoader = new LocalSchemaLoader(); 46 | assert.throws(() => schemaLoader.load(fakePath), { 47 | name: 'OpenAPISchemaMalformed' 48 | }); 49 | 50 | sinon.assert.calledOnceWithExactly(fs.accessSync, fakeFullPath, fs.constants.R_OK); 51 | sinon.assert.calledOnceWithExactly(fs.readFileSync, fakeFullPath); 52 | }); 53 | 54 | it('Should throw if the schema is not a valid YAML', () => { 55 | fs.readFileSync.returns('>>'); 56 | 57 | const schemaLoader = new LocalSchemaLoader(); 58 | assert.throws(() => schemaLoader.load(fakePath), { 59 | name: 'OpenAPISchemaMalformed' 60 | }); 61 | 62 | sinon.assert.calledOnceWithExactly(fs.accessSync, fakeFullPath, fs.constants.R_OK); 63 | sinon.assert.calledOnceWithExactly(fs.readFileSync, fakeFullPath); 64 | }); 65 | 66 | it('Should return the schema if it is a valid YAML', () => { 67 | fs.readFileSync.returns('foo: bar'); 68 | 69 | const schemaLoader = new LocalSchemaLoader(); 70 | assert.deepStrictEqual(schemaLoader.load(fakePath), { 71 | foo: 'bar' 72 | }); 73 | 74 | sinon.assert.calledOnceWithExactly(fs.accessSync, fakeFullPath, fs.constants.R_OK); 75 | sinon.assert.calledOnceWithExactly(fs.readFileSync, fakeFullPath); 76 | }); 77 | 78 | it('Should throw if the schema is not a valid JSON', () => { 79 | fs.readFileSync.returns('>>'); 80 | 81 | const schemaLoader = new LocalSchemaLoader(); 82 | assert.throws(() => schemaLoader.load(changeYamlToJson(fakePath)), { 83 | name: 'OpenAPISchemaMalformed' 84 | }); 85 | 86 | sinon.assert.calledOnceWithExactly(fs.accessSync, changeYamlToJson(fakeFullPath), fs.constants.R_OK); 87 | sinon.assert.calledOnceWithExactly(fs.readFileSync, changeYamlToJson(fakeFullPath)); 88 | }); 89 | 90 | it('Should return the schema if it is a valid JSON', () => { 91 | fs.readFileSync.returns('{"foo": "bar"}'); 92 | 93 | const schemaLoader = new LocalSchemaLoader(); 94 | assert.deepStrictEqual(schemaLoader.load(changeYamlToJson(fakePath)), { 95 | foo: 'bar' 96 | }); 97 | 98 | sinon.assert.calledOnceWithExactly(fs.accessSync, changeYamlToJson(fakeFullPath), fs.constants.R_OK); 99 | sinon.assert.calledOnceWithExactly(fs.readFileSync, changeYamlToJson(fakeFullPath)); 100 | }); 101 | 102 | it('Should handle an absolute schema properly', () => { 103 | fs.readFileSync.returns('foo: bar'); 104 | 105 | const schemaLoader = new LocalSchemaLoader(); 106 | assert.deepStrictEqual(schemaLoader.load(fakeFullPath), { 107 | foo: 'bar' 108 | }); 109 | 110 | sinon.assert.calledOnceWithExactly(fs.accessSync, fakeFullPath, fs.constants.R_OK); 111 | sinon.assert.calledOnceWithExactly(fs.readFileSync, fakeFullPath); 112 | }); 113 | 114 | }); 115 | 116 | describe('watch()', () => { 117 | 118 | let clock; 119 | 120 | beforeEach(() => { 121 | sinon.stub(LocalSchemaLoader.prototype, 'load'); 122 | sinon.stub(chokidar, 'watch'); 123 | clock = sinon.useFakeTimers(); 124 | }); 125 | 126 | afterEach(() => sinon.restore()); 127 | 128 | it('Should emit the schema-changed event each time the schema changes', () => { 129 | 130 | LocalSchemaLoader.prototype.load.onCall(0).returns({ foo: 'bar' }); 131 | LocalSchemaLoader.prototype.load.onCall(1).returns({ foo: 'bar2' }); 132 | LocalSchemaLoader.prototype.load.onCall(2).returns({ foo: 'bar3' }); 133 | 134 | const chokidarEmitter = new EventEmitter(); 135 | chokidar.watch.returns(chokidarEmitter); 136 | 137 | const changeCallback = sinon.fake(); 138 | 139 | const schemaLoader = new LocalSchemaLoader(); 140 | schemaLoader.on('schema-changed', changeCallback); 141 | 142 | schemaLoader.watch(); 143 | 144 | // Should not emit initially 145 | sinon.assert.notCalled(changeCallback); 146 | 147 | chokidarEmitter.emit('change'); 148 | 149 | // Should not emit immediately after change 150 | sinon.assert.notCalled(changeCallback); 151 | 152 | clock.tick(100); 153 | 154 | // Should emit after 100ms 155 | sinon.assert.calledOnce(changeCallback); 156 | 157 | // And now test consecutive calls 158 | chokidarEmitter.emit('change'); 159 | clock.tick(100); 160 | sinon.assert.calledTwice(changeCallback); 161 | 162 | chokidarEmitter.emit('change'); 163 | clock.tick(100); 164 | sinon.assert.calledThrice(changeCallback); 165 | }); 166 | 167 | }); 168 | 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /tests/security-requirement/security-requirement.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ParserError = require('../../lib/errors/parser-error'); 6 | const { Parser, SecurityRequirement } = require('../../lib/security-requirement'); 7 | 8 | describe('SecurityRequirement', () => { 9 | 10 | describe('Schema parse', () => { 11 | 12 | const parser = new Parser(); 13 | 14 | it('Should return an empty array if security is not defined', () => { 15 | 16 | const security = parser.parse({}); 17 | 18 | assert.deepStrictEqual(security, []); 19 | }); 20 | 21 | it('Should return an empty array if security is an empty array', () => { 22 | 23 | const security = parser.parse({ 24 | security: [] 25 | }); 26 | 27 | assert.deepStrictEqual(security, []); 28 | }); 29 | 30 | it('Should throw a ParserError if security is not an array', () => { 31 | 32 | const schema = { 33 | security: 'I\'m not an array' 34 | }; 35 | 36 | assert.throws(() => parser.parse(schema), ParserError); 37 | }); 38 | 39 | it('Should throw a ParserError if at least one security requirement is not valid', () => { 40 | 41 | const schema = { 42 | security: [ 43 | { 44 | 'oauth-token': ['some:scope'] 45 | }, 46 | { 47 | 'x-api-key': 0 48 | } 49 | ] 50 | }; 51 | 52 | assert.throws(() => parser.parse(schema), ParserError); 53 | }); 54 | 55 | it('Should return an array with the correct security requirement if one security requirement is defined only without scopes', () => { 56 | 57 | const schema = { 58 | security: [{ 59 | 'x-api-key': [] 60 | }] 61 | }; 62 | 63 | const security = parser.parse(schema); 64 | 65 | const expectedSecurityRequirements = new SecurityRequirement(schema.security[0]); 66 | 67 | assert.deepStrictEqual(security, [expectedSecurityRequirements]); 68 | }); 69 | 70 | it('Should return an array of both security requirements if two security requirements without variables are defined', () => { 71 | 72 | const schema = { 73 | security: [ 74 | { 75 | 'x-api-key': [], 76 | 'x-api-token': [] 77 | }, 78 | { 79 | 'oauth-token': ['some:scope'] 80 | } 81 | ] 82 | }; 83 | 84 | const security = parser.parse(schema); 85 | 86 | const expectedSecurityRequirement1 = new SecurityRequirement(schema.security[0]); 87 | const expectedSecurityRequirement2 = new SecurityRequirement(schema.security[1]); 88 | 89 | assert.deepStrictEqual(security, [expectedSecurityRequirement1, expectedSecurityRequirement2]); 90 | }); 91 | 92 | }); 93 | 94 | }); 95 | -------------------------------------------------------------------------------- /tests/servers/servers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ParserError = require('../../lib/errors/parser-error'); 6 | const { Parser, Server } = require('../../lib/servers'); 7 | 8 | describe('Servers', () => { 9 | 10 | describe('Schema parse', () => { 11 | 12 | const parser = new Parser(); 13 | 14 | it('Should return an array with default server if servers is not defined', () => { 15 | 16 | const servers = parser.parse({}); 17 | 18 | const defaultServer = new Server({ url: '/' }); 19 | 20 | assert.deepStrictEqual(servers, [defaultServer]); 21 | }); 22 | 23 | it('Should return an array with default server if servers is an empty array', () => { 24 | 25 | const servers = parser.parse({ 26 | servers: [] 27 | }); 28 | 29 | const defaultServer = new Server({ url: '/' }); 30 | 31 | assert.deepStrictEqual(servers, [defaultServer]); 32 | }); 33 | 34 | it('Should throw a ParserError if servers is not an array', () => { 35 | 36 | const schema = { 37 | servers: 'I\'m not an array' 38 | }; 39 | 40 | assert.throws(() => parser.parse(schema), ParserError); 41 | }); 42 | 43 | it('Should throw a ParserError if at least one server is not valid', () => { 44 | 45 | const schema = { 46 | servers: [ 47 | { 48 | url: 'https://api.example.com' 49 | }, 50 | { 51 | url: 0 52 | } 53 | ] 54 | }; 55 | 56 | assert.throws(() => parser.parse(schema), ParserError); 57 | }); 58 | 59 | it('Should return an array with the correct server if one server without variables is defined', () => { 60 | 61 | const servers = parser.parse({ 62 | servers: [{ 63 | url: 'https://api.example.com', 64 | description: 'A sample server' 65 | }] 66 | }); 67 | 68 | const expectedServer = new Server({ url: 'https://api.example.com', description: 'A sample server' }); 69 | 70 | assert.deepStrictEqual(servers, [expectedServer]); 71 | }); 72 | 73 | it('Should return an array of both servers if two servers without variables are defined', () => { 74 | 75 | const servers = parser.parse({ 76 | servers: [ 77 | { 78 | url: 'https://api.example.com', 79 | description: 'A sample server' 80 | }, 81 | { 82 | url: 'https://api2.example.com', 83 | description: 'Another sample server' 84 | } 85 | ] 86 | }); 87 | 88 | const expectedServer1 = new Server({ url: 'https://api.example.com', description: 'A sample server' }); 89 | const expectedServer2 = new Server({ url: 'https://api2.example.com', description: 'Another sample server' }); 90 | 91 | assert.deepStrictEqual(servers, [expectedServer1, expectedServer2]); 92 | }); 93 | 94 | it('Should return an array with the correct server if one server with empty variables is defined', () => { 95 | 96 | const servers = parser.parse({ 97 | servers: [{ 98 | url: 'https://api.example.com', 99 | variables: {} 100 | }] 101 | }); 102 | 103 | const expectedServer = new Server({ url: 'https://api.example.com' }); 104 | 105 | assert.deepStrictEqual(servers, [expectedServer]); 106 | }); 107 | 108 | it('Should throw a ParserError if server variables is not an object', () => { 109 | 110 | const schema = { 111 | servers: [ 112 | { 113 | url: 'https://api.example.com', 114 | variables: [] 115 | } 116 | ] 117 | }; 118 | 119 | assert.throws(() => parser.parse(schema), ParserError); 120 | }); 121 | 122 | it('Should throw a ParserError if server variables have invalid values', () => { 123 | 124 | const schema = { 125 | servers: [ 126 | { 127 | url: 'https://api.example.com', 128 | variables: { 129 | foo: ['I\'m an array'] 130 | } 131 | } 132 | ] 133 | }; 134 | 135 | assert.throws(() => parser.parse(schema), ParserError); 136 | }); 137 | 138 | it('Should throw a ParserError if server variables have no default', () => { 139 | 140 | const schema = { 141 | servers: [ 142 | { 143 | url: 'https://api.example.com', 144 | variables: { 145 | foo: { 146 | enum: ['foo', 'bar'] 147 | } 148 | } 149 | } 150 | ] 151 | }; 152 | 153 | assert.throws(() => parser.parse(schema), ParserError); 154 | }); 155 | 156 | it('Should return an array with the correct server if one server with variables is defined', () => { 157 | 158 | const servers = parser.parse({ 159 | servers: [{ 160 | url: 'https://api.example.com', 161 | description: 'A sample server', 162 | variables: { 163 | foo: { 164 | default: 'foo', 165 | enum: ['foo', 'bar'] 166 | } 167 | } 168 | }] 169 | }); 170 | 171 | const expectedServer = new Server({ url: 'https://api.example.com', description: 'A sample server' }); 172 | 173 | assert.deepStrictEqual(servers, [expectedServer]); 174 | }); 175 | 176 | it('Should replace the server variables in it\'s url', () => { 177 | 178 | const servers = parser.parse({ 179 | servers: [{ 180 | url: 'https://api.example.com/{stage}', 181 | description: 'A sample server', 182 | variables: { 183 | stage: { 184 | default: 'prod', 185 | enum: ['prod', 'test'] 186 | } 187 | } 188 | }] 189 | }); 190 | 191 | const expectedServer = new Server({ url: 'https://api.example.com/prod', description: 'A sample server' }); 192 | 193 | assert.deepStrictEqual(servers, [expectedServer]); 194 | }); 195 | 196 | it('Should mantain the specification extension properties', () => { 197 | 198 | const servers = parser.parse({ 199 | servers: [{ 200 | url: 'https://api.example.com/{stage}', 201 | description: 'A sample server', 202 | variables: { 203 | stage: { 204 | default: 'prod', 205 | enum: ['prod', 'test'] 206 | } 207 | }, 208 | 'x-foo': 'bar', 209 | 'x-baz': { 210 | test: [1, 2, 3] 211 | } 212 | }] 213 | }); 214 | 215 | const expectedServer = new Server({ url: 'https://api.example.com/prod', description: 'A sample server' }, [ 216 | ['x-foo', 'bar'], 217 | ['x-baz', { test: [1, 2, 3] }] 218 | ]); 219 | 220 | assert.deepStrictEqual(servers, [expectedServer]); 221 | }); 222 | 223 | }); 224 | 225 | }); 226 | -------------------------------------------------------------------------------- /tests/tags/tags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const ParserError = require('../../lib/errors/parser-error'); 6 | const { Parser, Tag } = require('../../lib/tags'); 7 | 8 | describe('Tags', () => { 9 | 10 | describe('Schema parse', () => { 11 | 12 | const parser = new Parser(); 13 | 14 | it('Should return an empty array if tags is not defined', () => { 15 | 16 | const tags = parser.parse({}); 17 | 18 | assert.deepStrictEqual(tags, []); 19 | }); 20 | 21 | it('Should return an empty array if tags is an empty array', () => { 22 | 23 | const tags = parser.parse({ 24 | tags: [] 25 | }); 26 | 27 | assert.deepStrictEqual(tags, []); 28 | }); 29 | 30 | it('Should throw a ParserError if tags is not an array', () => { 31 | 32 | const schema = { 33 | tags: 'I\'m not an array' 34 | }; 35 | 36 | assert.throws(() => parser.parse(schema), ParserError); 37 | }); 38 | 39 | it('Should throw a ParserError if at least one tag is not valid', () => { 40 | 41 | const schema = { 42 | tags: [ 43 | { 44 | name: 'Tag 1' 45 | }, 46 | { 47 | name: 0 48 | } 49 | ] 50 | }; 51 | 52 | assert.throws(() => parser.parse(schema), ParserError); 53 | }); 54 | 55 | it('Should return an array with the correct tag if one tag is defined only with a name', () => { 56 | 57 | const tags = parser.parse({ 58 | tags: [{ 59 | name: 'Tag 1' 60 | }] 61 | }); 62 | 63 | const expectedTag = new Tag({ name: 'Tag 1' }); 64 | 65 | assert.deepStrictEqual(tags, [expectedTag]); 66 | }); 67 | 68 | it('Should return an array of both tags if two tags without variables are defined', () => { 69 | 70 | const schema = { 71 | tags: [ 72 | { 73 | name: 'Tag 1' 74 | }, 75 | { 76 | name: 'Tag 2', 77 | description: 'Description of tag 2' 78 | } 79 | ] 80 | }; 81 | 82 | const tags = parser.parse(schema); 83 | 84 | const expectedTag1 = new Tag(schema.tags[0]); 85 | const expectedTag2 = new Tag(schema.tags[1]); 86 | 87 | assert.deepStrictEqual(tags, [expectedTag1, expectedTag2]); 88 | }); 89 | 90 | it('Should return an array with the correct tag if one tag is defined with all it\'s props', () => { 91 | 92 | const schema = { 93 | tags: [{ 94 | name: 'Tag 2', 95 | description: 'Description of tag 2', 96 | externalDocs: { 97 | url: 'https://api.example.com/docs', 98 | description: 'The documentation description' 99 | } 100 | }] 101 | }; 102 | 103 | const tags = parser.parse(schema); 104 | 105 | const expectedTag = new Tag(schema.tags[0]); 106 | 107 | assert.deepStrictEqual(tags, [expectedTag]); 108 | }); 109 | 110 | it('Should mantain the specification extension properties', () => { 111 | 112 | const specificationTagInfo = { 113 | name: 'Tag 2', 114 | description: 'Description of tag 2', 115 | externalDocs: { 116 | url: 'https://api.example.com/docs', 117 | description: 'The documentation description' 118 | } 119 | }; 120 | 121 | const schema = { 122 | tags: [{ 123 | ...specificationTagInfo, 124 | 'x-foo': 'bar', 125 | 'x-baz': { 126 | test: [1, 2, 3] 127 | } 128 | }] 129 | }; 130 | 131 | const tags = parser.parse(schema); 132 | 133 | const expectedTag = new Tag(specificationTagInfo, [ 134 | ['x-foo', 'bar'], 135 | ['x-baz', { test: [1, 2, 3] }] 136 | ]); 137 | 138 | assert.deepStrictEqual(tags, [expectedTag]); 139 | }); 140 | 141 | }); 142 | 143 | }); 144 | -------------------------------------------------------------------------------- /tests/utils/get-faker-locale.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | 6 | const { allFakers } = require('@faker-js/faker'); 7 | 8 | const getFakerLocale = require('../../lib/utils/get-faker-locale'); 9 | 10 | describe('Utils', () => { 11 | 12 | describe('getFakerLocale()', () => { 13 | 14 | let localeFake; 15 | 16 | beforeEach(() => { 17 | localeFake = sinon.stub().returns({ locale: 'en' }); 18 | sinon.stub(Intl, 'DateTimeFormat').returns({ 19 | resolvedOptions: localeFake 20 | }); 21 | }); 22 | 23 | afterEach(() => sinon.restore()); 24 | 25 | context('When locale has no country', () => { 26 | it('Should return the user locale if it is supported by faker', () => { 27 | const locale = getFakerLocale(); 28 | assert.deepStrictEqual(locale, allFakers.en); 29 | }); 30 | 31 | it('Should return the default locale (en) if user locale is not supported by faker', () => { 32 | 33 | localeFake.returns({ locale: 'zz' }); 34 | 35 | const locale = getFakerLocale(); 36 | assert.deepStrictEqual(locale, allFakers.en); 37 | }); 38 | }); 39 | 40 | context('When locale has country', () => { 41 | 42 | it('Should return the user locale if it is supported by faker', () => { 43 | 44 | localeFake.returns({ locale: 'en-US' }); 45 | 46 | const locale = getFakerLocale(); 47 | assert.deepStrictEqual(locale, allFakers.en_US); 48 | }); 49 | 50 | it('Should return the user locale without country if locale is equals to country', () => { 51 | 52 | localeFake.returns({ locale: 'es-ES' }); 53 | 54 | const locale = getFakerLocale(); 55 | assert.deepStrictEqual(locale, allFakers.es); 56 | }); 57 | 58 | it('Should return the user locale without country if it is not supported by faker but the base locale is', () => { 59 | 60 | localeFake.returns({ locale: 'en-ZZ' }); 61 | 62 | const locale = getFakerLocale(); 63 | assert.deepStrictEqual(locale, allFakers.en); 64 | }); 65 | 66 | it('Should return the default locale if user locale nor the base locale are supported by faker', () => { 67 | 68 | localeFake.returns({ locale: 'aa-ZZ' }); 69 | 70 | const locale = getFakerLocale(); 71 | assert.deepStrictEqual(locale, allFakers.en); 72 | }); 73 | }); 74 | 75 | context('When faker locales do not follow the standard', () => { 76 | it('Should return pt_PT for the locale pt-PT', () => { 77 | 78 | localeFake.returns({ locale: 'pt-PT' }); 79 | 80 | const locale = getFakerLocale(); 81 | assert.deepStrictEqual(locale, allFakers.pt_PT); 82 | }); 83 | }); 84 | 85 | }); 86 | 87 | }); 88 | --------------------------------------------------------------------------------