From d62291e50f22c86712d92333f4b81ca4747cd39d Mon Sep 17 00:00:00 2001 From: javierbrea Date: Wed, 17 Aug 2022 08:11:13 +0200 Subject: [PATCH 01/21] feat(#384): Create plugin-openapi package --- .github/workflows/build.yml | 1 + README.md | 21 +++++---- codecov.yml | 7 +++ packages/main/CHANGELOG.md | 5 +++ packages/main/package.json | 1 + packages/main/src/createCore.js | 3 +- packages/main/test/createCore.spec.js | 5 ++- packages/main/test/start.spec.js | 3 +- packages/plugin-openapi/.gitignore | 28 ++++++++++++ packages/plugin-openapi/CHANGELOG.md | 14 ++++++ packages/plugin-openapi/LICENSE | 21 +++++++++ packages/plugin-openapi/README.md | 23 ++++++++++ packages/plugin-openapi/babel.config.js | 4 ++ packages/plugin-openapi/jest.config.js | 33 ++++++++++++++ packages/plugin-openapi/package.json | 45 +++++++++++++++++++ packages/plugin-openapi/project.json | 5 +++ .../plugin-openapi/sonar-project.properties | 13 ++++++ packages/plugin-openapi/src/.eslintrc.json | 13 ++++++ packages/plugin-openapi/src/Plugin.ts | 9 ++++ packages/plugin-openapi/src/index.ts | 3 ++ packages/plugin-openapi/test/.eslintrc.cjs | 8 ++++ packages/plugin-openapi/test/Plugin.spec.js | 9 ++++ packages/plugin-openapi/test/index.spec.js | 9 ++++ packages/plugin-openapi/tsconfig.json | 21 +++++++++ pnpm-lock.yaml | 8 ++++ tsconfig.base.json | 3 ++ workspace.json | 1 + 27 files changed, 304 insertions(+), 12 deletions(-) create mode 100644 packages/plugin-openapi/.gitignore create mode 100644 packages/plugin-openapi/CHANGELOG.md create mode 100644 packages/plugin-openapi/LICENSE create mode 100644 packages/plugin-openapi/README.md create mode 100644 packages/plugin-openapi/babel.config.js create mode 100644 packages/plugin-openapi/jest.config.js create mode 100644 packages/plugin-openapi/package.json create mode 100644 packages/plugin-openapi/project.json create mode 100644 packages/plugin-openapi/sonar-project.properties create mode 100644 packages/plugin-openapi/src/.eslintrc.json create mode 100644 packages/plugin-openapi/src/Plugin.ts create mode 100644 packages/plugin-openapi/src/index.ts create mode 100644 packages/plugin-openapi/test/.eslintrc.cjs create mode 100644 packages/plugin-openapi/test/Plugin.spec.js create mode 100644 packages/plugin-openapi/test/index.spec.js create mode 100644 packages/plugin-openapi/tsconfig.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f2583e32c..b2d2a214d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,7 @@ on: branches: - master - release + - plugin-openapi pull_request: jobs: get-affected: diff --git a/README.md b/README.md index 0145e8b14..78f808e09 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,10 @@ To check out docs, visit [www.mocks-server.org][website-url]. | --- | --- | --- | | [main] | [![main-status]][main-package] | Main distribution. It includes all plugins preinstalled | | [core] | [![core-status]][core-package] | Pluggable core. It can be used programmatically also | -| [plugin-proxy] | [![plugin-proxy-status]][plugin-proxy-package] | Plugin providing Proxy route handler | -| [plugin-inquirer-cli] | [![plugin-inquirer-cli-status]][plugin-inquirer-cli-package] | Plugin providing an administration interactive CLI | | [plugin-admin-api] | [![plugin-admin-api-status]][plugin-admin-api-package] | Plugin providing an administration REST API | +| [plugin-inquirer-cli] | [![plugin-inquirer-cli-status]][plugin-inquirer-cli-package] | Plugin providing an administration interactive CLI | +| [plugin-openapi] | [![plugin-openapi-status]][plugin-openapi-package] | Plugin allowing to create routes and collections from OpenApi definitions | +| [plugin-proxy] | [![plugin-proxy-status]][plugin-proxy-package] | Plugin providing Proxy route handler | | [admin-api-client] | [![admin-api-client-status]][admin-api-client-package] | API client for [plugin-admin-api] | | [admin-api-client-data-provider] | [![admin-api-client-data-provider-status]][admin-api-client-data-provider-package] | API client for [plugin-admin-api] built using [data-provider] | | [admin-api-paths] | [![admin-api-paths-status]][admin-api-paths-package] | Definition of [plugin-admin-api] routes | @@ -67,17 +68,21 @@ Please read the [contributing guidelines](.github/CONTRIBUTING.md) and [code of [core-status]: https://img.shields.io/npm/v/@mocks-server/core.svg [core-package]: https://npmjs.com/package/@mocks-server/core -[plugin-proxy]: https://github.com/mocks-server/main/tree/master/packages/plugin-proxy -[plugin-proxy-status]: https://img.shields.io/npm/v/@mocks-server/plugin-proxy.svg -[plugin-proxy-package]: https://npmjs.com/package/@mocks-server/plugin-proxy +[plugin-admin-api]: https://github.com/mocks-server/main/tree/master/packages/plugin-admin-api +[plugin-admin-api-status]: https://img.shields.io/npm/v/@mocks-server/plugin-admin-api.svg +[plugin-admin-api-package]: https://npmjs.com/package/@mocks-server/plugin-admin-api [plugin-inquirer-cli]: https://github.com/mocks-server/main/tree/master/packages/plugin-inquirer-cli [plugin-inquirer-cli-status]: https://img.shields.io/npm/v/@mocks-server/plugin-inquirer-cli.svg [plugin-inquirer-cli-package]: https://npmjs.com/package/@mocks-server/plugin-inquirer-cli -[plugin-admin-api]: https://github.com/mocks-server/main/tree/master/packages/plugin-admin-api -[plugin-admin-api-status]: https://img.shields.io/npm/v/@mocks-server/plugin-admin-api.svg -[plugin-admin-api-package]: https://npmjs.com/package/@mocks-server/plugin-admin-api +[plugin-openapi]: https://github.com/mocks-server/main/tree/master/packages/plugin-openapi +[plugin-openapi-status]: https://img.shields.io/npm/v/@mocks-server/plugin-openapi.svg +[plugin-openapi-package]: https://npmjs.com/package/@mocks-server/plugin-openapi + +[plugin-proxy]: https://github.com/mocks-server/main/tree/master/packages/plugin-proxy +[plugin-proxy-status]: https://img.shields.io/npm/v/@mocks-server/plugin-proxy.svg +[plugin-proxy-package]: https://npmjs.com/package/@mocks-server/plugin-proxy [admin-api-client]: https://github.com/mocks-server/main/tree/master/packages/admin-api-client [admin-api-client-status]: https://img.shields.io/npm/v/@mocks-server/admin-api-client.svg diff --git a/codecov.yml b/codecov.yml index b8402b77e..817cdcef4 100644 --- a/codecov.yml +++ b/codecov.yml @@ -43,6 +43,9 @@ coverage: plugin-inquirer-cli: flags: - plugin-inquirer-cli + plugin-openapi: + flags: + - plugin-openapi plugin-proxy: flags: - plugin-proxy @@ -95,6 +98,10 @@ flags: paths: - packages/plugin-inquirer-cli/** carryforward: true + plugin-openapi: + paths: + - packages/plugin-openapi/** + carryforward: true plugin-proxy: paths: - packages/plugin-proxy/** diff --git a/packages/main/CHANGELOG.md b/packages/main/CHANGELOG.md index b978c29ea..34fb7c861 100644 --- a/packages/main/CHANGELOG.md +++ b/packages/main/CHANGELOG.md @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Removed ### Breaking change +## [unreleased] + +### Added +- feat(#384): Add plugin-openapi to preinstalled plugins + ## [3.10.0] - 2022-08-11 ### Changed diff --git a/packages/main/package.json b/packages/main/package.json index 327a9daea..9428ceefb 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -44,6 +44,7 @@ "@mocks-server/core": "workspace:*", "@mocks-server/plugin-admin-api": "workspace:*", "@mocks-server/plugin-inquirer-cli": "workspace:*", + "@mocks-server/plugin-openapi": "workspace:*", "@mocks-server/plugin-proxy": "workspace:*", "deepmerge": "4.2.2" }, diff --git a/packages/main/src/createCore.js b/packages/main/src/createCore.js index 7401e635b..ed284dbc7 100644 --- a/packages/main/src/createCore.js +++ b/packages/main/src/createCore.js @@ -15,6 +15,7 @@ const Core = require("@mocks-server/core"); const PluginProxy = require("@mocks-server/plugin-proxy"); const AdminApi = require("@mocks-server/plugin-admin-api"); const InquirerCli = require("@mocks-server/plugin-inquirer-cli"); +const OpenApi = require("@mocks-server/plugin-openapi").default; const deepMerge = require("deepmerge"); const pkg = require("../package.json"); @@ -26,7 +27,7 @@ const DEFAULT_CONFIG = { readFile: false, }, plugins: { - register: [PluginProxy, AdminApi, InquirerCli], + register: [PluginProxy, AdminApi, InquirerCli, OpenApi], inquirerCli: { enabled: false, }, diff --git a/packages/main/test/createCore.spec.js b/packages/main/test/createCore.spec.js index 1e292da0e..7b8a6b086 100644 --- a/packages/main/test/createCore.spec.js +++ b/packages/main/test/createCore.spec.js @@ -13,6 +13,7 @@ const sinon = require("sinon"); const PluginProxy = require("@mocks-server/plugin-proxy"); const AdminApi = require("@mocks-server/plugin-admin-api"); const InquirerCli = require("@mocks-server/plugin-inquirer-cli"); +const OpenApi = require("@mocks-server/plugin-openapi").default; const CoreMocks = require("./Core.mocks.js"); @@ -42,7 +43,7 @@ describe("createCore method", () => { readFile: false, }, plugins: { - register: [PluginProxy, AdminApi, InquirerCli], + register: [PluginProxy, AdminApi, InquirerCli, OpenApi], inquirerCli: { enabled: false, }, @@ -75,7 +76,7 @@ describe("createCore method", () => { readFile: false, }, plugins: { - register: [PluginProxy, AdminApi, InquirerCli, FooPlugin], + register: [PluginProxy, AdminApi, InquirerCli, OpenApi, FooPlugin], inquirerCli: { enabled: false, }, diff --git a/packages/main/test/start.spec.js b/packages/main/test/start.spec.js index 13c51dfb0..e702b6678 100644 --- a/packages/main/test/start.spec.js +++ b/packages/main/test/start.spec.js @@ -13,6 +13,7 @@ const sinon = require("sinon"); const PluginProxy = require("@mocks-server/plugin-proxy"); const AdminApi = require("@mocks-server/plugin-admin-api"); const InquirerCli = require("@mocks-server/plugin-inquirer-cli"); +const OpenApi = require("@mocks-server/plugin-openapi").default; const CoreMocks = require("./Core.mocks.js"); @@ -38,7 +39,7 @@ describe("start method", () => { expect(coreMocks.stubs.Constructor.mock.calls[0][0]).toEqual({ config: { readArguments: true, readEnvironment: true, readFile: true }, plugins: { - register: [PluginProxy, AdminApi, InquirerCli], + register: [PluginProxy, AdminApi, InquirerCli, OpenApi], inquirerCli: { enabled: true }, }, files: { enabled: true }, diff --git a/packages/plugin-openapi/.gitignore b/packages/plugin-openapi/.gitignore new file mode 100644 index 000000000..319414beb --- /dev/null +++ b/packages/plugin-openapi/.gitignore @@ -0,0 +1,28 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +#environment variables +.env + +# dependencies +/node_modules + +# build +/dist + +# tests +/coverage + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# ides +.idea +.vs diff --git a/packages/plugin-openapi/CHANGELOG.md b/packages/plugin-openapi/CHANGELOG.md new file mode 100644 index 000000000..37c14bdac --- /dev/null +++ b/packages/plugin-openapi/CHANGELOG.md @@ -0,0 +1,14 @@ +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [unreleased] +### Added +### Changed +### Fixed +### Removed +### BREAKING CHANGE + +## [unreleased] diff --git a/packages/plugin-openapi/LICENSE b/packages/plugin-openapi/LICENSE new file mode 100644 index 000000000..4a7f8c581 --- /dev/null +++ b/packages/plugin-openapi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-present Javier Brea + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/plugin-openapi/README.md b/packages/plugin-openapi/README.md new file mode 100644 index 000000000..a35320e5c --- /dev/null +++ b/packages/plugin-openapi/README.md @@ -0,0 +1,23 @@ +

Mocks Server logo

+ +

+ Build Status + Coverage + Quality Gate + Downloads + Renovate + License +

+ +--- + +# Mocks Server Plugin OpenApi + +[Mocks Server][website-url] plugin allowing to create routes and collections from OpenApi definitions. + +## Contributing + +Contributors are welcome. +Please read the [contributing guidelines](../../.github/CONTRIBUTING.md) and [code of conduct](../../.github/CODE_OF_CONDUCT.md). + +[website-url]: https://www.mocks-server.org diff --git a/packages/plugin-openapi/babel.config.js b/packages/plugin-openapi/babel.config.js new file mode 100644 index 000000000..cef502c56 --- /dev/null +++ b/packages/plugin-openapi/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"], + plugins: ["babel-plugin-replace-ts-export-assignment"], +}; diff --git a/packages/plugin-openapi/jest.config.js b/packages/plugin-openapi/jest.config.js new file mode 100644 index 000000000..02b01e85a --- /dev/null +++ b/packages/plugin-openapi/jest.config.js @@ -0,0 +1,33 @@ +// For a detailed explanation regarding each configuration property, visit: +// https://jestjs.io/docs/en/configuration.html + +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: ["src/**/*.ts"], + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, + + // The glob patterns Jest uses to detect test files + testMatch: ["/test/**/*.spec.js"], + // testMatch: ["/test/**/Plugin.spec.js"], + + // The test environment that will be used for testing + testEnvironment: "node", +}; diff --git a/packages/plugin-openapi/package.json b/packages/plugin-openapi/package.json new file mode 100644 index 000000000..bf24bd6a2 --- /dev/null +++ b/packages/plugin-openapi/package.json @@ -0,0 +1,45 @@ +{ + "name": "@mocks-server/plugin-openapi", + "version": "1.0.0", + "description": "Mocks server plugin allowing to create routes and collections from OpenApi definitions", + "keywords": [ + "mocks-server-plugin", + "OpenApi", + "mock", + "http", + "rest", + "plugin", + "mocks-server" + ], + "author": "Javier Brea", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/mocks-server/main.git", + "directory": "packages/plugin-openapi" + }, + "homepage": "https://www.mocks-server.org", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test:unit": "jest" + }, + "peerDependencies": { + "@mocks-server/core": ">=3.10.0 <4.x" + }, + "dependencies": { + }, + "devDependencies": { + "@mocks-server/core": "workspace:*" + }, + "engines": { + "node": ">=14.0.0" + } +} diff --git a/packages/plugin-openapi/project.json b/packages/plugin-openapi/project.json new file mode 100644 index 000000000..ec37038f6 --- /dev/null +++ b/packages/plugin-openapi/project.json @@ -0,0 +1,5 @@ +{ + "root": "packages/plugin-openapi/", + "projectType": "library", + "tags": ["type:lib"] +} diff --git a/packages/plugin-openapi/sonar-project.properties b/packages/plugin-openapi/sonar-project.properties new file mode 100644 index 000000000..2a1f36ceb --- /dev/null +++ b/packages/plugin-openapi/sonar-project.properties @@ -0,0 +1,13 @@ +sonar.organization=mocks-server +sonar.projectKey=mocks-server_main_plugin-openapi +sonar.projectName=plugin-openapi +sonar.projectVersion=1.0.0 + +sonar.javascript.file.suffixes=.js +sonar.sourceEncoding=UTF-8 +sonar.exclusions=node_modules/**,*.config.js +sonar.test.exclusions=test/**/* +sonar.coverage.exclusions=test/**/* +sonar.cpd.exclusions=test/** +sonar.javascript.lcov.reportPaths=coverage/lcov.info +sonar.host.url=https://sonarcloud.io diff --git a/packages/plugin-openapi/src/.eslintrc.json b/packages/plugin-openapi/src/.eslintrc.json new file mode 100644 index 000000000..dccaeeb3d --- /dev/null +++ b/packages/plugin-openapi/src/.eslintrc.json @@ -0,0 +1,13 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./packages/plugin-openapi/tsconfig.json" + }, + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ] +} diff --git a/packages/plugin-openapi/src/Plugin.ts b/packages/plugin-openapi/src/Plugin.ts new file mode 100644 index 000000000..ccb207af6 --- /dev/null +++ b/packages/plugin-openapi/src/Plugin.ts @@ -0,0 +1,9 @@ +const PLUGIN_NAME = "openapi"; + +class Plugin { + static get id() { + return PLUGIN_NAME; + } +} + +export default Plugin; diff --git a/packages/plugin-openapi/src/index.ts b/packages/plugin-openapi/src/index.ts new file mode 100644 index 000000000..025bc4077 --- /dev/null +++ b/packages/plugin-openapi/src/index.ts @@ -0,0 +1,3 @@ +import Plugin from "./Plugin"; + +export default Plugin; diff --git a/packages/plugin-openapi/test/.eslintrc.cjs b/packages/plugin-openapi/test/.eslintrc.cjs new file mode 100644 index 000000000..41e1c0653 --- /dev/null +++ b/packages/plugin-openapi/test/.eslintrc.cjs @@ -0,0 +1,8 @@ +module.exports = { + parser: "@babel/eslint-parser", + parserOptions: { + sourceType: "module", + allowImportExportEverywhere: true, + requireConfigFile: false, + }, +}; diff --git a/packages/plugin-openapi/test/Plugin.spec.js b/packages/plugin-openapi/test/Plugin.spec.js new file mode 100644 index 000000000..6ea7798a9 --- /dev/null +++ b/packages/plugin-openapi/test/Plugin.spec.js @@ -0,0 +1,9 @@ +import Plugin from "../src/Plugin"; + +describe("Plugin", () => { + describe("id", () => { + it("should be openapi", () => { + expect(Plugin.id).toEqual("openapi"); + }); + }); +}); diff --git a/packages/plugin-openapi/test/index.spec.js b/packages/plugin-openapi/test/index.spec.js new file mode 100644 index 000000000..13b52c518 --- /dev/null +++ b/packages/plugin-openapi/test/index.spec.js @@ -0,0 +1,9 @@ +import index from "../src/index"; + +import Plugin from "../src/Plugin"; + +describe("index file", () => { + it("should export plugin as default", () => { + expect(index).toBe(Plugin); + }); +}); diff --git a/packages/plugin-openapi/tsconfig.json b/packages/plugin-openapi/tsconfig.json new file mode 100644 index 000000000..4c7e38547 --- /dev/null +++ b/packages/plugin-openapi/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + "outDir": "./dist", + "declaration": true, + "target": "es6", + "strict": true, + "strictNullChecks": true, + "esModuleInterop": true, + "moduleResolution": "node", + "module": "CommonJS", + "useDefineForClassFields": true, + "importsNotUsedAsValues": "error", + "forceConsistentCasingInFileNames": true, + "noUnusedParameters": true, + "isolatedModules": true + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5844b4b5..cd3152e4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -300,12 +300,14 @@ importers: '@mocks-server/core': workspace:* '@mocks-server/plugin-admin-api': workspace:* '@mocks-server/plugin-inquirer-cli': workspace:* + '@mocks-server/plugin-openapi': workspace:* '@mocks-server/plugin-proxy': workspace:* deepmerge: 4.2.2 dependencies: '@mocks-server/core': link:../core '@mocks-server/plugin-admin-api': link:../plugin-admin-api '@mocks-server/plugin-inquirer-cli': link:../plugin-inquirer-cli + '@mocks-server/plugin-openapi': link:../plugin-openapi '@mocks-server/plugin-proxy': link:../plugin-proxy deepmerge: 4.2.2 @@ -350,6 +352,12 @@ importers: devDependencies: '@mocks-server/core': link:../core + packages/plugin-openapi: + specifiers: + '@mocks-server/core': workspace:* + devDependencies: + '@mocks-server/core': link:../core + packages/plugin-proxy: specifiers: '@mocks-server/core': workspace:* diff --git a/tsconfig.base.json b/tsconfig.base.json index b0bf4e785..31b82316e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -42,6 +42,9 @@ "@mocks-server/plugin-inquirer-cli": [ "packages/plugin-inquirer-cli/index.js" ], + "@mocks-server/plugin-openapi": [ + "packages/plugin-openapi/dist/index.js" + ], "@mocks-server/plugin-proxy": [ "packages/plugin-proxy/index.js" ] diff --git a/workspace.json b/workspace.json index b012ce244..0a6e4b1dc 100644 --- a/workspace.json +++ b/workspace.json @@ -34,6 +34,7 @@ "plugin-admin-api-swagger-e2e": "test/plugin-admin-api-swagger-e2e", "plugin-inquirer-cli": "packages/plugin-inquirer-cli", "plugin-inquirer-cli-e2e": "test/plugin-inquirer-cli-e2e", + "plugin-openapi": "packages/plugin-openapi", "plugin-proxy": "packages/plugin-proxy" } } From e614179a72a8e2b9d9491864ce1be36a19c705b4 Mon Sep 17 00:00:00 2001 From: javierbrea Date: Wed, 17 Aug 2022 08:14:37 +0200 Subject: [PATCH 02/21] chore: Build branch --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b2d2a214d..6c21bac04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: branches: - master - release - - plugin-openapi + - feat-384-openapi pull_request: jobs: get-affected: From 9fabaa13a1715fa1136d38a5b9827d6c5974bcbf Mon Sep 17 00:00:00 2001 From: javierbrea Date: Thu, 18 Aug 2022 11:52:44 +0200 Subject: [PATCH 03/21] feat(#384): Create routes from openapi document --- .eslintignore | 2 + packages/plugin-openapi/jest.config.js | 2 +- packages/plugin-openapi/package.json | 6 +- packages/plugin-openapi/src/Plugin.ts | 42 ++++- packages/plugin-openapi/src/index.ts | 1 + .../plugin-openapi/src/mocks-server-core.d.ts | 67 +++++++ packages/plugin-openapi/src/openapi.ts | 168 ++++++++++++++++++ packages/plugin-openapi/src/types.ts | 8 + .../plugin-openapi/test/custom-ids.spec.js | 112 ++++++++++++ .../test/fixtures/api-users/collections.js | 1 + .../test/fixtures/api-users/openapi/api.js | 8 + .../test/fixtures/custom-ids/collections.js | 20 +++ .../test/fixtures/custom-ids/openapi/api.js | 26 +++ .../fixtures/empty-example/collections.js | 1 + .../fixtures/empty-example/openapi/api.js | 31 ++++ .../fixtures/empty-examples/collections.js | 1 + .../fixtures/empty-examples/openapi/api.js | 26 +++ .../test/fixtures/empty-media/collections.js | 1 + .../test/fixtures/empty-media/openapi/api.js | 24 +++ .../test/fixtures/empty-method/collections.js | 1 + .../test/fixtures/empty-method/openapi/api.js | 16 ++ .../test/fixtures/empty-path/collections.js | 1 + .../test/fixtures/empty-path/openapi/api.js | 13 ++ .../fixtures/empty-response/collections.js | 1 + .../fixtures/empty-response/openapi/api.js | 25 +++ .../fixtures/empty-responses/collections.js | 1 + .../fixtures/empty-responses/openapi/api.js | 20 +++ .../test/fixtures/no-paths/collections.js | 1 + .../test/fixtures/no-paths/openapi/api.js | 11 ++ .../fixtures/unknown-media/collections.js | 1 + .../fixtures/unknown-media/openapi/api.js | 28 +++ .../test/fixtures/users/collections.js | 1 + .../test/fixtures/users/openapi/api.js | 7 + .../test/incomplete-openapi.spec.js | 57 ++++++ .../test/openapi/users-collection.js | 20 +++ packages/plugin-openapi/test/openapi/users.js | 117 ++++++++++++ .../test/routes-from-file.spec.js | 166 +++++++++++++++++ .../plugin-openapi/test/support/helpers.js | 130 ++++++++++++++ packages/plugin-openapi/tsconfig.json | 3 + pnpm-lock.yaml | 9 + 40 files changed, 1171 insertions(+), 5 deletions(-) create mode 100644 packages/plugin-openapi/src/mocks-server-core.d.ts create mode 100644 packages/plugin-openapi/src/openapi.ts create mode 100644 packages/plugin-openapi/src/types.ts create mode 100644 packages/plugin-openapi/test/custom-ids.spec.js create mode 100644 packages/plugin-openapi/test/fixtures/api-users/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/api-users/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/custom-ids/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/custom-ids/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-example/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-example/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-examples/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-examples/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-media/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-media/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-method/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-method/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-path/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-path/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-response/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-response/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-responses/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/empty-responses/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/no-paths/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/no-paths/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/unknown-media/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/unknown-media/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/users/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/users/openapi/api.js create mode 100644 packages/plugin-openapi/test/incomplete-openapi.spec.js create mode 100644 packages/plugin-openapi/test/openapi/users-collection.js create mode 100644 packages/plugin-openapi/test/openapi/users.js create mode 100644 packages/plugin-openapi/test/routes-from-file.spec.js create mode 100644 packages/plugin-openapi/test/support/helpers.js diff --git a/.eslintignore b/.eslintignore index 2a8ec1a8b..e2ae93c3d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,7 +4,9 @@ /mocks/*/build /mocks/admin-api-client-data-provider-e2e-vanilla-app/public/js /packages/*/dist +/packages/*/dist-tsc /packages/*/coverage +/test/*/dist /test/core-e2e/src/fixtures /test/core-e2e-legacy/src/fixtures /test/main-e2e/src/fixtures \ No newline at end of file diff --git a/packages/plugin-openapi/jest.config.js b/packages/plugin-openapi/jest.config.js index 02b01e85a..6b43734d6 100644 --- a/packages/plugin-openapi/jest.config.js +++ b/packages/plugin-openapi/jest.config.js @@ -26,7 +26,7 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: ["/test/**/*.spec.js"], - // testMatch: ["/test/**/Plugin.spec.js"], + // testMatch: ["/test/**/custom-ids.spec.js"], // The test environment that will be used for testing testEnvironment: "node", diff --git a/packages/plugin-openapi/package.json b/packages/plugin-openapi/package.json index bf24bd6a2..3e941acdf 100644 --- a/packages/plugin-openapi/package.json +++ b/packages/plugin-openapi/package.json @@ -29,15 +29,17 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsc", - "test:unit": "jest" + "test:unit": "jest --runInBand" }, "peerDependencies": { "@mocks-server/core": ">=3.10.0 <4.x" }, "dependencies": { + "openapi-types": "12.0.0" }, "devDependencies": { - "@mocks-server/core": "workspace:*" + "@mocks-server/core": "workspace:*", + "@mocks-server/nested-collections": "workspace:*" }, "engines": { "node": ">=14.0.0" diff --git a/packages/plugin-openapi/src/Plugin.ts b/packages/plugin-openapi/src/Plugin.ts index ccb207af6..cbef8dafb 100644 --- a/packages/plugin-openapi/src/Plugin.ts +++ b/packages/plugin-openapi/src/Plugin.ts @@ -1,8 +1,46 @@ -const PLUGIN_NAME = "openapi"; +import type { Core, MockLoaders, FilesContents } from "@mocks-server/core"; + +import { openApiMockDocumentsToRoutes } from "./openapi"; + +const PLUGIN_ID = "openapi"; +const DEFAULT_FOLDER = "openapi"; + +// TODO, add options: path class Plugin { static get id() { - return PLUGIN_NAME; + return PLUGIN_ID; + } + + private _logger: Core["logger"] + private _alerts: Core["alerts"] + private _files: Core["files"] + private _loadRoutes: MockLoaders["loadRoutes"] + + constructor({ logger, alerts, mock, files }: Core) { + this._logger = logger; + this._alerts = alerts; + this._files = files; + + const { loadRoutes } = mock.createLoaders(); + this._loadRoutes = loadRoutes; + this._files.createLoader({ + id: PLUGIN_ID, + src: `${DEFAULT_FOLDER}/**/*`, + onLoad: this._onLoadFiles.bind(this), + }) + } + + _onLoadFiles(filesContents: FilesContents) { + const openApiMockDocuments = filesContents + .map((fileDetails) => { + return fileDetails.content; + }).flat(); + this._logger.debug(`Creating routes from openApi definitions: '${JSON.stringify(openApiMockDocuments)}'`); + const routes = openApiMockDocumentsToRoutes(openApiMockDocuments); + this._logger.debug(`Routes to load from openApi definitions: '${JSON.stringify(routes)}'`); + this._logger.verbose(`Loading ${routes.length} routes from openApi definitions found in '${this._files.path}/${DEFAULT_FOLDER}'`); + this._loadRoutes(routes); } } diff --git a/packages/plugin-openapi/src/index.ts b/packages/plugin-openapi/src/index.ts index 025bc4077..468a77034 100644 --- a/packages/plugin-openapi/src/index.ts +++ b/packages/plugin-openapi/src/index.ts @@ -1,3 +1,4 @@ import Plugin from "./Plugin"; +export * from "./types"; export default Plugin; diff --git a/packages/plugin-openapi/src/mocks-server-core.d.ts b/packages/plugin-openapi/src/mocks-server-core.d.ts new file mode 100644 index 000000000..41f2da420 --- /dev/null +++ b/packages/plugin-openapi/src/mocks-server-core.d.ts @@ -0,0 +1,67 @@ +declare module "@mocks-server/core" { + import type Collection from "@mocks-server/nested-collections"; + import type { OpenAPIV3 } from "openapi-types"; + + interface Logger { + verbose(message: string): void + debug(message: string): void + } + + interface FileContents { + path: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + content: any, + } + + interface FileErrors { + path: string, + error: Error, + } + + type FilesContents = FileContents[] + type FilesErrors = FileErrors[] + + type FilesLoaderOnLoad = (filesContents: FilesContents, filesErrors: FilesErrors) => void + + interface FilesLoaderOptions { + id: string, + src: string, + onLoad: FilesLoaderOnLoad, + } + + type RouteVariantType = "json" | "status" | "text" + + interface Files { + createLoader(options: FilesLoaderOptions): void + path: string + } + + interface RouteVariant { + id: string, + type: RouteVariantType + } + + interface Route { + id: string, + url: string, + method: OpenAPIV3.HttpMethods, + variants: RouteVariant[], + } + + type Routes = Route[] + + interface MockLoaders { + loadRoutes(routes: Routes): void + } + + interface Mock { + createLoaders(): MockLoaders + } + + interface Core { + logger: Logger + alerts: typeof Collection, + files: Files + mock: Mock + } +} diff --git a/packages/plugin-openapi/src/openapi.ts b/packages/plugin-openapi/src/openapi.ts new file mode 100644 index 000000000..1c5a5f942 --- /dev/null +++ b/packages/plugin-openapi/src/openapi.ts @@ -0,0 +1,168 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { Routes, RouteVariantType } from "@mocks-server/core"; + +import { OpenAPIV3 as OpenApiV3Object } from "openapi-types"; + +import type { OpenApiMockDocuments, OpenApiMockDocument } from "./types"; + +const MOCKS_SERVER_ROUTE_ID = "x-mocks-server-route-id"; +const methods = Object.values(OpenApiV3Object.HttpMethods); + +function notEmpty(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined; +} + +function replaceTemplateInPath(path: string): string { + // /api/users/{userId} -> api/users/:userId + return path.replace(/{(\S*)}/gim, ":$1") +} + +function pathToId(path: string): string { + // /api/users/{userId} -> api-users-userId + return path.replace(/^\//gim, "").replace(/\//gim, "-").replace(/[{}]/gim, "") +} + +function avoidDoubleSlashes(path: string): string { + // /api//users -> /api/users + return path.replace(/\/+/gim,"/"); +} + +function routeUrl(path: string, basePath: string): string { + if(basePath) { + return avoidDoubleSlashes(`${basePath}/${replaceTemplateInPath(path)}`); + } + return replaceTemplateInPath(path); +} + +function routeId(path: string, method: string, mocksServerId?: string): string { + if(mocksServerId) { + return mocksServerId; + } + return `${method}-${pathToId(path)}`; +} + +function isJsonMediaType(mediaType: string): boolean { + // TODO, make compatible with all possible media types + return mediaType === "application/json"; +} + +function isTextMediaType(mediaType: string): boolean { + // TODO, make compatible with all possible media types + return mediaType === "text/plain" || mediaType === "text/html"; +} + +function openApiResponseBaseVariant(variantType: RouteVariantType, code: string, exampleId?: string) { + const id = exampleId ? `${code}-${variantType}-${exampleId}` : `${code}-${variantType}` + return { + id, + type: variantType, + }; +} + +// TODO, support also ReferenceObject in examples +function openApiResponseExampleToVariant(exampleId: string, code: string, openApiResponseExample: OpenAPIV3.ExampleObject, variantType: RouteVariantType) { + if(!notEmpty(openApiResponseExample) || !notEmpty(openApiResponseExample.value)) { + return null; + } + + const baseVariant = openApiResponseBaseVariant(variantType, code, exampleId); + return { + ...baseVariant, + options: { + status: Number(code), + body: openApiResponseExample.value + } + } +} + +function openApiResponseNoContentToVariant(code: string) { + const baseVariant = openApiResponseBaseVariant("status", code); + return { + ...baseVariant, + options: { + status: Number(code), + } + } +} + +function openApiResponseExamplesToVariants(code: string, openApiResponseMediaType: OpenAPIV3.MediaTypeObject, variantType: RouteVariantType) { + const examples = openApiResponseMediaType.examples; + if(!notEmpty(examples)) { + return null; + } + return Object.keys(examples).map((exampleId: string) => { + // TODO, support also ReferenceObject in examples + return openApiResponseExampleToVariant(exampleId, code, examples[exampleId] as OpenAPIV3.ExampleObject, variantType); + }).filter(notEmpty); +} + +function openApiResponseMediaToVariants(code: string, mediaType: string, openApiResponseMediaType?: OpenAPIV3.MediaTypeObject) { + if(!notEmpty(openApiResponseMediaType)) { + return null; + } + if(isJsonMediaType(mediaType)) { + return openApiResponseExamplesToVariants(code, openApiResponseMediaType, "json"); + } + if(isTextMediaType(mediaType)) { + return openApiResponseExamplesToVariants(code, openApiResponseMediaType, "text"); + } + return null; +} + +function openApiResponseCodeToVariants(code: string, openApiResponse?: OpenAPIV3.ResponseObject) { + if(!notEmpty(openApiResponse)) { + return []; + } + const content = openApiResponse.content; + if(content) { + return Object.keys(content).map((mediaType: string) => { + return openApiResponseMediaToVariants(code, mediaType, content[mediaType]); + }).flat().filter(notEmpty); + } + return openApiResponseNoContentToVariant(code); +} + +function routeVariants(openApiResponses?: OpenAPIV3.ResponsesObject) { + if(!notEmpty(openApiResponses)) { + return []; + } + return Object.keys(openApiResponses).map((code: string) => { + const response = openApiResponses[code] as OpenAPIV3.ResponseObject; + return openApiResponseCodeToVariants(code, response); + }).flat().filter(notEmpty); +} + +function getMockServerRouteId(openApiOperation: OpenAPIV3.OperationObject<{[MOCKS_SERVER_ROUTE_ID]?: string}>): string | undefined { + return openApiOperation[MOCKS_SERVER_ROUTE_ID]; +} + +function openApiPathToRoutes(path: string, basePath = "", openApiPathObject?: OpenAPIV3.PathItemObject ): Routes | null { + if(!notEmpty(openApiPathObject)) { + return null; + } + return methods.map(method => { + if(notEmpty(openApiPathObject[method])) { + const openApiOperation = openApiPathObject[method] as OpenAPIV3.OperationObject; + return { + id: routeId(path, method, getMockServerRouteId(openApiOperation)), + url: routeUrl(path, basePath), + method, + variants: routeVariants(openApiOperation.responses), + }; + } + }).filter(notEmpty); +} + +function openApiMockDocumentToRoutes(openApiMockDocument: OpenApiMockDocument): Routes { + const openApiDocument = openApiMockDocument.document; + const basePath = openApiMockDocument.basePath; + + const paths = openApiDocument.paths || {}; + return Object.keys(paths).map((path: string) => { + return openApiPathToRoutes(path, basePath, paths[path]); + }).flat().filter(notEmpty); +} + +export function openApiMockDocumentsToRoutes(openApiMockDocuments: OpenApiMockDocuments): Routes { + return openApiMockDocuments.map(openApiMockDocumentToRoutes).flat(); +} diff --git a/packages/plugin-openapi/src/types.ts b/packages/plugin-openapi/src/types.ts new file mode 100644 index 000000000..3ad24f7f3 --- /dev/null +++ b/packages/plugin-openapi/src/types.ts @@ -0,0 +1,8 @@ +import type { OpenAPIV3 } from "openapi-types"; + +export interface OpenApiMockDocument { + basePath: string, + document: OpenAPIV3.Document +} + +export type OpenApiMockDocuments = OpenApiMockDocument[] diff --git a/packages/plugin-openapi/test/custom-ids.spec.js b/packages/plugin-openapi/test/custom-ids.spec.js new file mode 100644 index 000000000..0f449c1cb --- /dev/null +++ b/packages/plugin-openapi/test/custom-ids.spec.js @@ -0,0 +1,112 @@ +import { startServer, fetchJson, fetchText, waitForServer } from "./support/helpers"; + +describe("when openapi has custom ids", () => { + let server; + + beforeAll(async () => { + server = await startServer("custom-ids"); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + describe("routes", () => { + it("should have created routes from openapi document defined in files", async () => { + expect(server.mock.routes.plain).toEqual([ + { + id: "read-users", + url: "/api/users", + method: "get", + delay: null, + variants: ["read-users:200-json-one-user", "read-users:200-json-two-users"], + }, + { + id: "create-user", + url: "/api/users", + method: "post", + delay: null, + variants: ["create-user:201-status", "create-user:400-text-error-message"], + }, + { + id: "read-user", + url: "/api/users/:id", + method: "get", + delay: null, + variants: ["read-user:200-json-success", "read-user:404-json-not-found"], + }, + ]); + }); + }); + + describe("get-users route", () => { + it("should have 200-json-one-user variant available in base collection", async () => { + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + ]); + expect(response.status).toEqual(200); + }); + + it("should have 200-json-two-users variant available in all-users collection", async () => { + await server.mock.collections.select("all-users", { check: true }); + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ]); + expect(response.status).toEqual(200); + }); + }); + + describe("post-users route", () => { + it("should have 201-status variant available in all-users collection", async () => { + const response = await fetchJson("/api/users", { + method: "POST", + }); + expect(response.body).toBe(undefined); + expect(response.status).toEqual(201); + }); + + it("should have 400-text-error-message variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchText("/api/users", { + method: "POST", + }); + expect(response.body).toBe("Bad data"); + expect(response.status).toEqual(400); + }); + }); + + describe("get-users-id route", () => { + it("should have 200-json-success variant available in base collection", async () => { + await server.mock.collections.select("base", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + id: 1, + name: "John Doe", + }); + expect(response.status).toEqual(200); + }); + + it("should have 200-json-two-users variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + code: 404, + message: "Not found", + }); + expect(response.status).toEqual(404); + }); + }); +}); diff --git a/packages/plugin-openapi/test/fixtures/api-users/collections.js b/packages/plugin-openapi/test/fixtures/api-users/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/api-users/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/api-users/openapi/api.js b/packages/plugin-openapi/test/fixtures/api-users/openapi/api.js new file mode 100644 index 000000000..a27c01bdf --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/api-users/openapi/api.js @@ -0,0 +1,8 @@ +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + basePath: "/api", + document: openApiDocument, + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/custom-ids/collections.js b/packages/plugin-openapi/test/fixtures/custom-ids/collections.js new file mode 100644 index 000000000..0aadf7804 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/custom-ids/collections.js @@ -0,0 +1,20 @@ +module.exports = [ + { + id: "base", + routes: [ + "read-users:200-json-one-user", + "create-user:201-status", + "read-user:200-json-success", + ], + }, + { + id: "all-users", + from: "base", + routes: ["read-users:200-json-two-users"], + }, + { + id: "users-error", + from: "base", + routes: ["create-user:400-text-error-message", "read-user:404-json-not-found"], + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/custom-ids/openapi/api.js b/packages/plugin-openapi/test/fixtures/custom-ids/openapi/api.js new file mode 100644 index 000000000..18165079f --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/custom-ids/openapi/api.js @@ -0,0 +1,26 @@ +const deepMerge = require("deepmerge"); + +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + basePath: "/api", + document: deepMerge(openApiDocument, { + paths: { + "/users": { + post: { + "x-mocks-server-route-id": "create-user", + }, + get: { + "x-mocks-server-route-id": "read-users", + }, + }, + "/users/{id}": { + get: { + "x-mocks-server-route-id": "read-user", + }, + }, + }, + }), + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/empty-example/collections.js b/packages/plugin-openapi/test/fixtures/empty-example/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-example/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/empty-example/openapi/api.js b/packages/plugin-openapi/test/fixtures/empty-example/openapi/api.js new file mode 100644 index 000000000..39da986ab --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-example/openapi/api.js @@ -0,0 +1,31 @@ +const deepMerge = require("deepmerge"); + +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + document: deepMerge(openApiDocument, { + paths: { + "/users": { + post: undefined, + get: { + responses: { + "200": { + content: { + "application/json": { + examples: { + "one-user": undefined, + "two-users": { + value: undefined, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }), + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/empty-examples/collections.js b/packages/plugin-openapi/test/fixtures/empty-examples/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-examples/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/empty-examples/openapi/api.js b/packages/plugin-openapi/test/fixtures/empty-examples/openapi/api.js new file mode 100644 index 000000000..6651b44a9 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-examples/openapi/api.js @@ -0,0 +1,26 @@ +const deepMerge = require("deepmerge"); + +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + document: deepMerge(openApiDocument, { + paths: { + "/users": { + post: undefined, + get: { + responses: { + "200": { + content: { + "application/json": { + examples: undefined, + }, + }, + }, + }, + }, + }, + }, + }), + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/empty-media/collections.js b/packages/plugin-openapi/test/fixtures/empty-media/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-media/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/empty-media/openapi/api.js b/packages/plugin-openapi/test/fixtures/empty-media/openapi/api.js new file mode 100644 index 000000000..d8c789f69 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-media/openapi/api.js @@ -0,0 +1,24 @@ +const deepMerge = require("deepmerge"); + +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + document: deepMerge(openApiDocument, { + paths: { + "/users": { + post: undefined, + get: { + responses: { + "200": { + content: { + "application/json": undefined, + }, + }, + }, + }, + }, + }, + }), + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/empty-method/collections.js b/packages/plugin-openapi/test/fixtures/empty-method/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-method/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/empty-method/openapi/api.js b/packages/plugin-openapi/test/fixtures/empty-method/openapi/api.js new file mode 100644 index 000000000..ab6abbde5 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-method/openapi/api.js @@ -0,0 +1,16 @@ +const deepMerge = require("deepmerge"); + +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + document: deepMerge(openApiDocument, { + paths: { + "/users": { + get: undefined, + post: undefined, + }, + }, + }), + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/empty-path/collections.js b/packages/plugin-openapi/test/fixtures/empty-path/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-path/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/empty-path/openapi/api.js b/packages/plugin-openapi/test/fixtures/empty-path/openapi/api.js new file mode 100644 index 000000000..6b42dd979 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-path/openapi/api.js @@ -0,0 +1,13 @@ +const deepMerge = require("deepmerge"); + +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + document: deepMerge(openApiDocument, { + paths: { + "/users": undefined, + }, + }), + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/empty-response/collections.js b/packages/plugin-openapi/test/fixtures/empty-response/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-response/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/empty-response/openapi/api.js b/packages/plugin-openapi/test/fixtures/empty-response/openapi/api.js new file mode 100644 index 000000000..14deb2c5d --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-response/openapi/api.js @@ -0,0 +1,25 @@ +const deepMerge = require("deepmerge"); + +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + document: deepMerge(openApiDocument, { + paths: { + "/users": { + post: { + responses: { + "201": undefined, + "400": undefined, + }, + }, + get: { + responses: { + "200": undefined, + }, + }, + }, + }, + }), + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/empty-responses/collections.js b/packages/plugin-openapi/test/fixtures/empty-responses/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-responses/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/empty-responses/openapi/api.js b/packages/plugin-openapi/test/fixtures/empty-responses/openapi/api.js new file mode 100644 index 000000000..eb999cf79 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/empty-responses/openapi/api.js @@ -0,0 +1,20 @@ +const deepMerge = require("deepmerge"); + +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + document: deepMerge(openApiDocument, { + paths: { + "/users": { + post: { + responses: undefined, + }, + get: { + responses: undefined, + }, + }, + }, + }), + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/no-paths/collections.js b/packages/plugin-openapi/test/fixtures/no-paths/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/no-paths/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/no-paths/openapi/api.js b/packages/plugin-openapi/test/fixtures/no-paths/openapi/api.js new file mode 100644 index 000000000..05c93c9a9 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/no-paths/openapi/api.js @@ -0,0 +1,11 @@ +const deepMerge = require("deepmerge"); + +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + document: deepMerge(openApiDocument, { + paths: undefined, + }), + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/unknown-media/collections.js b/packages/plugin-openapi/test/fixtures/unknown-media/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/unknown-media/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/unknown-media/openapi/api.js b/packages/plugin-openapi/test/fixtures/unknown-media/openapi/api.js new file mode 100644 index 000000000..5f4e4d3c5 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/unknown-media/openapi/api.js @@ -0,0 +1,28 @@ +const deepMerge = require("deepmerge"); + +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + document: deepMerge(openApiDocument, { + paths: { + "/users": { + post: undefined, + get: { + responses: { + "200": { + content: { + "application/json": undefined, + "application/foo": + openApiDocument.paths["/users"].get.responses["200"].content[ + "application/json" + ], + }, + }, + }, + }, + }, + }, + }), + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/users/collections.js b/packages/plugin-openapi/test/fixtures/users/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/users/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/users/openapi/api.js b/packages/plugin-openapi/test/fixtures/users/openapi/api.js new file mode 100644 index 000000000..03368796e --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/users/openapi/api.js @@ -0,0 +1,7 @@ +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + document: openApiDocument, + }, +]; diff --git a/packages/plugin-openapi/test/incomplete-openapi.spec.js b/packages/plugin-openapi/test/incomplete-openapi.spec.js new file mode 100644 index 000000000..369487d1e --- /dev/null +++ b/packages/plugin-openapi/test/incomplete-openapi.spec.js @@ -0,0 +1,57 @@ +import { startServer, waitForServer } from "./support/helpers"; + +describe("when openapi has not enough properties", () => { + let server; + + describe("when openapi has no paths property", () => { + beforeAll(async () => { + server = await startServer("no-paths"); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + it("should have not created routes from openapi document defined in files", async () => { + expect(server.mock.routes.plain).toEqual([]); + }); + }); + + function checkOnlyUsersIdRouteIsAvailable(description, fixture) { + describe(`when openapi ${description}`, () => { + beforeAll(async () => { + server = await startServer(fixture); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + it("should omit not valid routes", async () => { + expect(server.mock.routes.plain).toEqual([ + { + id: "get-users-id", + url: "/users/:id", + method: "get", + delay: null, + variants: ["get-users-id:200-json-success", "get-users-id:404-json-not-found"], + }, + ]); + }); + }); + } + + checkOnlyUsersIdRouteIsAvailable("path is empty", "empty-path"); + checkOnlyUsersIdRouteIsAvailable("method is empty", "empty-method"); + checkOnlyUsersIdRouteIsAvailable("method responses is empty", "empty-responses"); + checkOnlyUsersIdRouteIsAvailable("response code is empty", "empty-response"); + checkOnlyUsersIdRouteIsAvailable("response media is empty", "empty-media"); + checkOnlyUsersIdRouteIsAvailable("response media is unknown", "unknown-media"); + checkOnlyUsersIdRouteIsAvailable("response media examples is empty", "empty-examples"); + checkOnlyUsersIdRouteIsAvailable( + "response media example or example value is empty", + "empty-example" + ); +}); diff --git a/packages/plugin-openapi/test/openapi/users-collection.js b/packages/plugin-openapi/test/openapi/users-collection.js new file mode 100644 index 000000000..475324da7 --- /dev/null +++ b/packages/plugin-openapi/test/openapi/users-collection.js @@ -0,0 +1,20 @@ +module.exports = [ + { + id: "base", + routes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + }, + { + id: "all-users", + from: "base", + routes: ["get-users:200-json-two-users"], + }, + { + id: "users-error", + from: "base", + routes: ["post-users:400-text-error-message", "get-users-id:404-json-not-found"], + }, +]; diff --git a/packages/plugin-openapi/test/openapi/users.js b/packages/plugin-openapi/test/openapi/users.js new file mode 100644 index 000000000..31fed5bef --- /dev/null +++ b/packages/plugin-openapi/test/openapi/users.js @@ -0,0 +1,117 @@ +module.exports = { + openapi: "3.1.0", + info: { + title: "Testing API", + description: "OpenApi document to create mock for testing purpses", + contact: { + email: "info@mocks-server.org", + }, + }, + paths: { + "/users": { + get: { + summary: "Return all users", + description: "Use it to get current users", + responses: { + "200": { + description: "successful operation", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/Users", + }, + examples: { + "one-user": { + summary: "One route", + value: [ + { + id: 1, + name: "John Doe", + }, + ], + }, + "two-users": { + summary: "Two users", + value: [ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ], + }, + }, + }, + }, + }, + }, + }, + post: { + summary: "Create an user", + responses: { + "201": { + description: "successful operation", + }, + "400": { + description: "bad data", + content: { + "text/plain": { + examples: { + "error-message": { + summary: "Error message", + value: "Bad data", + }, + }, + }, + }, + }, + }, + }, + }, + "/users/{id}": { + get: { + summary: "Return one user", + responses: { + "200": { + description: "successful operation", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/User", + }, + examples: { + success: { + summary: "One user", + value: { + id: 1, + name: "John Doe", + }, + }, + }, + }, + }, + }, + "404": { + description: "user not found", + content: { + "application/json": { + examples: { + "not-found": { + summary: "Not found error", + value: { + code: 404, + message: "Not found", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugin-openapi/test/routes-from-file.spec.js b/packages/plugin-openapi/test/routes-from-file.spec.js new file mode 100644 index 000000000..be7943710 --- /dev/null +++ b/packages/plugin-openapi/test/routes-from-file.spec.js @@ -0,0 +1,166 @@ +import { startServer, fetchJson, fetchText, waitForServer } from "./support/helpers"; + +describe("routes generated from openapi file", () => { + let server; + + describe("when fixture is api-users", () => { + beforeAll(async () => { + server = await startServer("api-users"); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + describe("routes", () => { + it("should have created routes from openapi document defined in files", async () => { + expect(server.mock.routes.plain).toEqual([ + { + id: "get-users", + url: "/api/users", + method: "get", + delay: null, + variants: ["get-users:200-json-one-user", "get-users:200-json-two-users"], + }, + { + id: "post-users", + url: "/api/users", + method: "post", + delay: null, + variants: ["post-users:201-status", "post-users:400-text-error-message"], + }, + { + id: "get-users-id", + url: "/api/users/:id", + method: "get", + delay: null, + variants: ["get-users-id:200-json-success", "get-users-id:404-json-not-found"], + }, + ]); + }); + }); + + describe("get-users route", () => { + it("should have 200-json-one-user variant available in base collection", async () => { + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + ]); + expect(response.status).toEqual(200); + }); + + it("should have 200-json-two-users variant available in all-users collection", async () => { + await server.mock.collections.select("all-users", { check: true }); + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ]); + expect(response.status).toEqual(200); + }); + }); + + describe("post-users route", () => { + it("should have 201-status variant available in all-users collection", async () => { + const response = await fetchJson("/api/users", { + method: "POST", + }); + expect(response.body).toBe(undefined); + expect(response.status).toEqual(201); + }); + + it("should have 400-text-error-message variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchText("/api/users", { + method: "POST", + }); + expect(response.body).toBe("Bad data"); + expect(response.status).toEqual(400); + }); + }); + + describe("get-users-id route", () => { + it("should have 200-json-success variant available in base collection", async () => { + await server.mock.collections.select("base", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + id: 1, + name: "John Doe", + }); + expect(response.status).toEqual(200); + }); + + it("should have 200-json-two-users variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + code: 404, + message: "Not found", + }); + expect(response.status).toEqual(404); + }); + }); + }); + + describe("when openapi mock has no basePath", () => { + beforeAll(async () => { + server = await startServer("users"); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + describe("routes", () => { + it("should have created routes from openapi document defined in files", async () => { + expect(server.mock.routes.plain).toEqual([ + { + id: "get-users", + url: "/users", + method: "get", + delay: null, + variants: ["get-users:200-json-one-user", "get-users:200-json-two-users"], + }, + { + id: "post-users", + url: "/users", + method: "post", + delay: null, + variants: ["post-users:201-status", "post-users:400-text-error-message"], + }, + { + id: "get-users-id", + url: "/users/:id", + method: "get", + delay: null, + variants: ["get-users-id:200-json-success", "get-users-id:404-json-not-found"], + }, + ]); + }); + }); + + describe("get-users route", () => { + it("should have 200-json-one-user variant available in base collection", async () => { + const response = await fetchJson("/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + ]); + expect(response.status).toEqual(200); + }); + }); + }); +}); diff --git a/packages/plugin-openapi/test/support/helpers.js b/packages/plugin-openapi/test/support/helpers.js new file mode 100644 index 000000000..096871e98 --- /dev/null +++ b/packages/plugin-openapi/test/support/helpers.js @@ -0,0 +1,130 @@ +import path from "path"; + +import Core from "@mocks-server/core"; +import crossFetch from "cross-fetch"; +import deepMerge from "deepmerge"; +import waitOn from "wait-on"; + +import Plugin from "../../src/index"; + +const DEFAULT_SERVER_PORT = 3100; + +const defaultOptions = { + server: { + port: DEFAULT_SERVER_PORT, + }, + log: "silent", + files: { + watch: false, + }, +}; + +export const defaultRequestOptions = { + method: "get", + headers: { + "Content-Type": "application/json", + }, +}; + +export const FIXTURES_PATH = path.resolve(__dirname, "..", "fixtures"); + +export function fixturesPath(folderName) { + return path.resolve(FIXTURES_PATH, folderName); +} + +export function createCore() { + return new Core({ + config: { + allowUnknownArguments: true, + readFile: false, + readArguments: false, + readEnvironment: false, + }, + plugins: { + register: [Plugin], + }, + }); +} + +export function startExistingCore(core, fixturePath, options = {}) { + return core + .init( + deepMerge.all([ + defaultOptions, + { + files: { + path: fixturesPath(fixturePath), + }, + }, + options, + ]) + ) + .then(() => { + return core.start().then(() => { + return Promise.resolve(core); + }); + }); +} + +export function serverUrl(port, protocol) { + const protocolToUse = protocol || "http"; + return `${protocolToUse}://127.0.0.1:${port || DEFAULT_SERVER_PORT}`; +} + +export function startServer(fixturePath, options = {}) { + return startExistingCore(createCore(), fixturePath, options); +} + +export function doFetch(uri, options = {}) { + const requestOptions = { + ...defaultRequestOptions, + ...options, + }; + if (requestOptions.body) { + requestOptions.body = JSON.stringify(requestOptions.body); + } + + return crossFetch(`${serverUrl(options.port, options.protocol)}${uri}`, { + ...requestOptions, + }).then((res) => { + return res[options.parser]() + .then((processedRes) => ({ + body: processedRes, + status: res.status, + headers: res.headers, + url: res.url, + })) + .catch(() => { + return { status: res.status, headers: res.headers, url: res.url }; + }); + }); +} + +export function doServerFetch(uri, options = {}) { + return doFetch(uri, { + port: DEFAULT_SERVER_PORT, + ...options, + }); +} + +export function fetchJson(uri, options = {}) { + return doServerFetch(uri, { + parser: "json", + ...options, + }); +} + +export function fetchText(uri, options = {}) { + return doServerFetch(uri, { + parser: "text", + ...options, + }); +} + +export function waitForServer(port) { + return waitOn({ resources: [`tcp:127.0.0.1:${port || DEFAULT_SERVER_PORT}`] }); +} + +export function waitForServerUrl(url, options = {}) { + return waitOn({ resources: [`${serverUrl(options.port, options.protocol)}${url}`] }); +} diff --git a/packages/plugin-openapi/tsconfig.json b/packages/plugin-openapi/tsconfig.json index 4c7e38547..b108dac1a 100644 --- a/packages/plugin-openapi/tsconfig.json +++ b/packages/plugin-openapi/tsconfig.json @@ -4,6 +4,9 @@ "outDir": "./dist", "declaration": true, "target": "es6", + "lib": [ + "es2019" + ], "strict": true, "strictNullChecks": true, "esModuleInterop": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd3152e4e..189159c3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -355,8 +355,13 @@ importers: packages/plugin-openapi: specifiers: '@mocks-server/core': workspace:* + '@mocks-server/nested-collections': workspace:* + openapi-types: 12.0.0 + dependencies: + openapi-types: 12.0.0 devDependencies: '@mocks-server/core': link:../core + '@mocks-server/nested-collections': link:../nested-collections packages/plugin-proxy: specifiers: @@ -12480,6 +12485,10 @@ packages: is-wsl: 2.2.0 dev: true + /openapi-types/12.0.0: + resolution: {integrity: sha512-6Wd9k8nmGQHgCbehZCP6wwWcfXcvinhybUTBatuhjRsCxUIujuYFZc9QnGeae75CyHASewBtxs0HX/qwREReUw==} + dev: false + /opn/5.5.0: resolution: {integrity: sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==} engines: {node: '>=4'} From 5d17593181e8814c7a5840758a8c7367b2c7629a Mon Sep 17 00:00:00 2001 From: javierbrea Date: Thu, 18 Aug 2022 12:51:52 +0200 Subject: [PATCH 04/21] feat(#384): Support defining custom ids for variants in openapi document --- packages/plugin-openapi/src/constants.ts | 2 + packages/plugin-openapi/src/openapi.ts | 33 +++++----- packages/plugin-openapi/src/types.ts | 7 +++ .../plugin-openapi/test/custom-ids.spec.js | 6 +- .../test/fixtures/custom-ids/collections.js | 10 +-- .../test/fixtures/custom-ids/openapi/api.js | 62 ++++++++++++++++++- 6 files changed, 93 insertions(+), 27 deletions(-) create mode 100644 packages/plugin-openapi/src/constants.ts diff --git a/packages/plugin-openapi/src/constants.ts b/packages/plugin-openapi/src/constants.ts new file mode 100644 index 000000000..42c65d8df --- /dev/null +++ b/packages/plugin-openapi/src/constants.ts @@ -0,0 +1,2 @@ +export const MOCKS_SERVER_ROUTE_ID = "x-mocks-server-route-id"; +export const MOCKS_SERVER_VARIANT_ID = "x-mocks-server-variant-id"; diff --git a/packages/plugin-openapi/src/openapi.ts b/packages/plugin-openapi/src/openapi.ts index 1c5a5f942..e5f0a2abc 100644 --- a/packages/plugin-openapi/src/openapi.ts +++ b/packages/plugin-openapi/src/openapi.ts @@ -2,10 +2,10 @@ import type { OpenAPIV3 } from "openapi-types"; import type { Routes, RouteVariantType } from "@mocks-server/core"; import { OpenAPIV3 as OpenApiV3Object } from "openapi-types"; +import type { OpenApiMockDocuments, OpenApiMockDocument, ResponseObjectWithVariantId, ExampleObjectWithVariantId, OperationObjectWithRouteId } from "./types"; -import type { OpenApiMockDocuments, OpenApiMockDocument } from "./types"; +import { MOCKS_SERVER_ROUTE_ID, MOCKS_SERVER_VARIANT_ID } from "./constants"; -const MOCKS_SERVER_ROUTE_ID = "x-mocks-server-route-id"; const methods = Object.values(OpenApiV3Object.HttpMethods); function notEmpty(value: TValue | null | undefined): value is TValue { @@ -51,8 +51,13 @@ function isTextMediaType(mediaType: string): boolean { return mediaType === "text/plain" || mediaType === "text/html"; } -function openApiResponseBaseVariant(variantType: RouteVariantType, code: string, exampleId?: string) { - const id = exampleId ? `${code}-${variantType}-${exampleId}` : `${code}-${variantType}` +function openApiResponseBaseVariant(variantType: RouteVariantType, code: string, options: { customId?: string, exampleId?: string }) { + let id; + if (options.customId) { + id = options.customId; + } else { + id = options.exampleId ? `${code}-${variantType}-${options.exampleId}` : `${code}-${variantType}` + } return { id, type: variantType, @@ -60,12 +65,12 @@ function openApiResponseBaseVariant(variantType: RouteVariantType, code: string, } // TODO, support also ReferenceObject in examples -function openApiResponseExampleToVariant(exampleId: string, code: string, openApiResponseExample: OpenAPIV3.ExampleObject, variantType: RouteVariantType) { +function openApiResponseExampleToVariant(exampleId: string, code: string, openApiResponseExample: ExampleObjectWithVariantId, variantType: RouteVariantType) { if(!notEmpty(openApiResponseExample) || !notEmpty(openApiResponseExample.value)) { return null; } - const baseVariant = openApiResponseBaseVariant(variantType, code, exampleId); + const baseVariant = openApiResponseBaseVariant(variantType, code, { exampleId, customId: openApiResponseExample[MOCKS_SERVER_VARIANT_ID] }); return { ...baseVariant, options: { @@ -75,8 +80,8 @@ function openApiResponseExampleToVariant(exampleId: string, code: string, openAp } } -function openApiResponseNoContentToVariant(code: string) { - const baseVariant = openApiResponseBaseVariant("status", code); +function openApiResponseNoContentToVariant(code: string, openApiResponse: ResponseObjectWithVariantId) { + const baseVariant = openApiResponseBaseVariant("status", code, { customId: openApiResponse[MOCKS_SERVER_VARIANT_ID] }); return { ...baseVariant, options: { @@ -92,7 +97,7 @@ function openApiResponseExamplesToVariants(code: string, openApiResponseMediaTyp } return Object.keys(examples).map((exampleId: string) => { // TODO, support also ReferenceObject in examples - return openApiResponseExampleToVariant(exampleId, code, examples[exampleId] as OpenAPIV3.ExampleObject, variantType); + return openApiResponseExampleToVariant(exampleId, code, examples[exampleId] as ExampleObjectWithVariantId, variantType); }).filter(notEmpty); } @@ -109,7 +114,7 @@ function openApiResponseMediaToVariants(code: string, mediaType: string, openApi return null; } -function openApiResponseCodeToVariants(code: string, openApiResponse?: OpenAPIV3.ResponseObject) { +function openApiResponseCodeToVariants(code: string, openApiResponse?: ResponseObjectWithVariantId) { if(!notEmpty(openApiResponse)) { return []; } @@ -119,7 +124,7 @@ function openApiResponseCodeToVariants(code: string, openApiResponse?: OpenAPIV3 return openApiResponseMediaToVariants(code, mediaType, content[mediaType]); }).flat().filter(notEmpty); } - return openApiResponseNoContentToVariant(code); + return openApiResponseNoContentToVariant(code, openApiResponse); } function routeVariants(openApiResponses?: OpenAPIV3.ResponsesObject) { @@ -127,12 +132,12 @@ function routeVariants(openApiResponses?: OpenAPIV3.ResponsesObject) { return []; } return Object.keys(openApiResponses).map((code: string) => { - const response = openApiResponses[code] as OpenAPIV3.ResponseObject; + const response = openApiResponses[code] as ResponseObjectWithVariantId; return openApiResponseCodeToVariants(code, response); }).flat().filter(notEmpty); } -function getMockServerRouteId(openApiOperation: OpenAPIV3.OperationObject<{[MOCKS_SERVER_ROUTE_ID]?: string}>): string | undefined { +function getMockServerRouteId(openApiOperation: OperationObjectWithRouteId): string | undefined { return openApiOperation[MOCKS_SERVER_ROUTE_ID]; } @@ -142,7 +147,7 @@ function openApiPathToRoutes(path: string, basePath = "", openApiPathObject?: Op } return methods.map(method => { if(notEmpty(openApiPathObject[method])) { - const openApiOperation = openApiPathObject[method] as OpenAPIV3.OperationObject; + const openApiOperation = openApiPathObject[method] as OperationObjectWithRouteId; return { id: routeId(path, method, getMockServerRouteId(openApiOperation)), url: routeUrl(path, basePath), diff --git a/packages/plugin-openapi/src/types.ts b/packages/plugin-openapi/src/types.ts index 3ad24f7f3..ee189e821 100644 --- a/packages/plugin-openapi/src/types.ts +++ b/packages/plugin-openapi/src/types.ts @@ -1,4 +1,5 @@ import type { OpenAPIV3 } from "openapi-types"; +import { MOCKS_SERVER_ROUTE_ID, MOCKS_SERVER_VARIANT_ID } from "./constants"; export interface OpenApiMockDocument { basePath: string, @@ -6,3 +7,9 @@ export interface OpenApiMockDocument { } export type OpenApiMockDocuments = OpenApiMockDocument[] + +export type ResponseObjectWithVariantId = OpenAPIV3.ResponseObject & { [MOCKS_SERVER_VARIANT_ID]?: string } +export type ExampleObjectWithVariantId = OpenAPIV3.ExampleObject & { [MOCKS_SERVER_VARIANT_ID]?: string } + +export type OperationObjectWithRouteId = OpenAPIV3.OperationObject<{[MOCKS_SERVER_ROUTE_ID]?: string}> + diff --git a/packages/plugin-openapi/test/custom-ids.spec.js b/packages/plugin-openapi/test/custom-ids.spec.js index 0f449c1cb..5a478cabf 100644 --- a/packages/plugin-openapi/test/custom-ids.spec.js +++ b/packages/plugin-openapi/test/custom-ids.spec.js @@ -20,21 +20,21 @@ describe("when openapi has custom ids", () => { url: "/api/users", method: "get", delay: null, - variants: ["read-users:200-json-one-user", "read-users:200-json-two-users"], + variants: ["read-users:one-user", "read-users:two-users"], }, { id: "create-user", url: "/api/users", method: "post", delay: null, - variants: ["create-user:201-status", "create-user:400-text-error-message"], + variants: ["create-user:success", "create-user:error"], }, { id: "read-user", url: "/api/users/:id", method: "get", delay: null, - variants: ["read-user:200-json-success", "read-user:404-json-not-found"], + variants: ["read-user:success", "read-user:not-found"], }, ]); }); diff --git a/packages/plugin-openapi/test/fixtures/custom-ids/collections.js b/packages/plugin-openapi/test/fixtures/custom-ids/collections.js index 0aadf7804..04e1fa23e 100644 --- a/packages/plugin-openapi/test/fixtures/custom-ids/collections.js +++ b/packages/plugin-openapi/test/fixtures/custom-ids/collections.js @@ -1,20 +1,16 @@ module.exports = [ { id: "base", - routes: [ - "read-users:200-json-one-user", - "create-user:201-status", - "read-user:200-json-success", - ], + routes: ["read-users:one-user", "create-user:success", "read-user:success"], }, { id: "all-users", from: "base", - routes: ["read-users:200-json-two-users"], + routes: ["read-users:two-users"], }, { id: "users-error", from: "base", - routes: ["create-user:400-text-error-message", "read-user:404-json-not-found"], + routes: ["create-user:error", "read-user:not-found"], }, ]; diff --git a/packages/plugin-openapi/test/fixtures/custom-ids/openapi/api.js b/packages/plugin-openapi/test/fixtures/custom-ids/openapi/api.js index 18165079f..831cd8c94 100644 --- a/packages/plugin-openapi/test/fixtures/custom-ids/openapi/api.js +++ b/packages/plugin-openapi/test/fixtures/custom-ids/openapi/api.js @@ -8,16 +8,72 @@ module.exports = [ document: deepMerge(openApiDocument, { paths: { "/users": { - post: { - "x-mocks-server-route-id": "create-user", - }, get: { "x-mocks-server-route-id": "read-users", + responses: { + "200": { + content: { + "application/json": { + examples: { + "one-user": { + "x-mocks-server-variant-id": "one-user", + }, + "two-users": { + "x-mocks-server-variant-id": "two-users", + }, + }, + }, + }, + }, + }, + }, + post: { + "x-mocks-server-route-id": "create-user", + responses: { + "201": { + "x-mocks-server-variant-id": "success", + }, + "400": { + content: { + "text/plain": { + examples: { + "error-message": { + "x-mocks-server-variant-id": "error", + }, + }, + }, + }, + }, + }, }, }, "/users/{id}": { get: { "x-mocks-server-route-id": "read-user", + responses: { + "200": { + content: { + "application/json": { + examples: { + success: { + "x-mocks-server-variant-id": "success", + }, + }, + }, + }, + }, + "404": { + content: { + "application/json": { + examples: { + "not-found": { + "x-mocks-server-variant-id": "not-found", + }, + }, + }, + }, + }, + }, }, }, }, From 12458d0d4901bf5f7307ed63c121921ad7ed9ee7 Mon Sep 17 00:00:00 2001 From: javierbrea Date: Fri, 19 Aug 2022 08:29:21 +0200 Subject: [PATCH 05/21] feat(#384): Support openapi response headers --- packages/plugin-openapi/jest.config.js | 2 +- packages/plugin-openapi/src/constants.ts | 8 ++ .../plugin-openapi/src/mocks-server-core.d.ts | 12 ++- packages/plugin-openapi/src/openapi.ts | 36 +++---- packages/plugin-openapi/src/types.ts | 1 + .../test/custom-headers.spec.js | 93 +++++++++++++++++++ .../fixtures/custom-headers/collections.js | 1 + .../fixtures/custom-headers/openapi/api.js | 49 ++++++++++ 8 files changed, 182 insertions(+), 20 deletions(-) create mode 100644 packages/plugin-openapi/test/custom-headers.spec.js create mode 100644 packages/plugin-openapi/test/fixtures/custom-headers/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/custom-headers/openapi/api.js diff --git a/packages/plugin-openapi/jest.config.js b/packages/plugin-openapi/jest.config.js index 6b43734d6..317523fb4 100644 --- a/packages/plugin-openapi/jest.config.js +++ b/packages/plugin-openapi/jest.config.js @@ -26,7 +26,7 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: ["/test/**/*.spec.js"], - // testMatch: ["/test/**/custom-ids.spec.js"], + // testMatch: ["/test/**/custom-headers.spec.js"], // The test environment that will be used for testing testEnvironment: "node", diff --git a/packages/plugin-openapi/src/constants.ts b/packages/plugin-openapi/src/constants.ts index 42c65d8df..84be73ed2 100644 --- a/packages/plugin-openapi/src/constants.ts +++ b/packages/plugin-openapi/src/constants.ts @@ -1,2 +1,10 @@ export const MOCKS_SERVER_ROUTE_ID = "x-mocks-server-route-id"; export const MOCKS_SERVER_VARIANT_ID = "x-mocks-server-variant-id"; + +export enum VariantTypes { + JSON = "json", + TEXT = "text", + STATUS = "status" +} + +export const CONTENT_TYPE_HEADER = "Content-Type" diff --git a/packages/plugin-openapi/src/mocks-server-core.d.ts b/packages/plugin-openapi/src/mocks-server-core.d.ts index 41f2da420..1f7df09a4 100644 --- a/packages/plugin-openapi/src/mocks-server-core.d.ts +++ b/packages/plugin-openapi/src/mocks-server-core.d.ts @@ -2,6 +2,14 @@ declare module "@mocks-server/core" { import type Collection from "@mocks-server/nested-collections"; import type { OpenAPIV3 } from "openapi-types"; + enum VariantTypes { + JSON = "json", + TEXT = "text", + STATUS = "status" + } + + type RouteVariantTypes = VariantTypes + interface Logger { verbose(message: string): void debug(message: string): void @@ -29,8 +37,6 @@ declare module "@mocks-server/core" { onLoad: FilesLoaderOnLoad, } - type RouteVariantType = "json" | "status" | "text" - interface Files { createLoader(options: FilesLoaderOptions): void path: string @@ -38,7 +44,7 @@ declare module "@mocks-server/core" { interface RouteVariant { id: string, - type: RouteVariantType + type: RouteVariantTypes } interface Route { diff --git a/packages/plugin-openapi/src/openapi.ts b/packages/plugin-openapi/src/openapi.ts index e5f0a2abc..fb7de9c68 100644 --- a/packages/plugin-openapi/src/openapi.ts +++ b/packages/plugin-openapi/src/openapi.ts @@ -1,10 +1,10 @@ import type { OpenAPIV3 } from "openapi-types"; -import type { Routes, RouteVariantType } from "@mocks-server/core"; +import type { Routes, RouteVariantTypes } from "@mocks-server/core"; import { OpenAPIV3 as OpenApiV3Object } from "openapi-types"; -import type { OpenApiMockDocuments, OpenApiMockDocument, ResponseObjectWithVariantId, ExampleObjectWithVariantId, OperationObjectWithRouteId } from "./types"; +import type { OpenApiMockDocuments, OpenApiMockDocument, ResponseObjectWithVariantId, ExampleObjectWithVariantId, OperationObjectWithRouteId, ResponseHeaders } from "./types"; -import { MOCKS_SERVER_ROUTE_ID, MOCKS_SERVER_VARIANT_ID } from "./constants"; +import { MOCKS_SERVER_ROUTE_ID, MOCKS_SERVER_VARIANT_ID, VariantTypes, CONTENT_TYPE_HEADER } from "./constants"; const methods = Object.values(OpenApiV3Object.HttpMethods); @@ -42,16 +42,14 @@ function routeId(path: string, method: string, mocksServerId?: string): string { } function isJsonMediaType(mediaType: string): boolean { - // TODO, make compatible with all possible media types - return mediaType === "application/json"; + return mediaType.includes("application/json"); } function isTextMediaType(mediaType: string): boolean { - // TODO, make compatible with all possible media types - return mediaType === "text/plain" || mediaType === "text/html"; + return mediaType.includes("text/"); } -function openApiResponseBaseVariant(variantType: RouteVariantType, code: string, options: { customId?: string, exampleId?: string }) { +function openApiResponseBaseVariant(variantType: RouteVariantTypes, code: string, options: { customId?: string, exampleId?: string }) { let id; if (options.customId) { id = options.customId; @@ -65,15 +63,20 @@ function openApiResponseBaseVariant(variantType: RouteVariantType, code: string, } // TODO, support also ReferenceObject in examples -function openApiResponseExampleToVariant(exampleId: string, code: string, openApiResponseExample: ExampleObjectWithVariantId, variantType: RouteVariantType) { +function openApiResponseExampleToVariant(exampleId: string, code: string, variantType: RouteVariantTypes, mediaType: string, openApiResponseExample: ExampleObjectWithVariantId, openApiResponseHeaders?: ResponseHeaders) { if(!notEmpty(openApiResponseExample) || !notEmpty(openApiResponseExample.value)) { return null; } const baseVariant = openApiResponseBaseVariant(variantType, code, { exampleId, customId: openApiResponseExample[MOCKS_SERVER_VARIANT_ID] }); + return { ...baseVariant, options: { + headers: { + ...openApiResponseHeaders, + [CONTENT_TYPE_HEADER]: mediaType, + }, status: Number(code), body: openApiResponseExample.value } @@ -81,35 +84,36 @@ function openApiResponseExampleToVariant(exampleId: string, code: string, openAp } function openApiResponseNoContentToVariant(code: string, openApiResponse: ResponseObjectWithVariantId) { - const baseVariant = openApiResponseBaseVariant("status", code, { customId: openApiResponse[MOCKS_SERVER_VARIANT_ID] }); + const baseVariant = openApiResponseBaseVariant(VariantTypes.STATUS, code, { customId: openApiResponse[MOCKS_SERVER_VARIANT_ID] }); return { ...baseVariant, options: { + headers: openApiResponse.headers, status: Number(code), } } } -function openApiResponseExamplesToVariants(code: string, openApiResponseMediaType: OpenAPIV3.MediaTypeObject, variantType: RouteVariantType) { +function openApiResponseExamplesToVariants(code: string, variantType: RouteVariantTypes, mediaType: string, openApiResponseMediaType: OpenAPIV3.MediaTypeObject, openApiResponseHeaders?: ResponseHeaders) { const examples = openApiResponseMediaType.examples; if(!notEmpty(examples)) { return null; } return Object.keys(examples).map((exampleId: string) => { // TODO, support also ReferenceObject in examples - return openApiResponseExampleToVariant(exampleId, code, examples[exampleId] as ExampleObjectWithVariantId, variantType); + return openApiResponseExampleToVariant(exampleId, code, variantType, mediaType, examples[exampleId] as ExampleObjectWithVariantId, openApiResponseHeaders); }).filter(notEmpty); } -function openApiResponseMediaToVariants(code: string, mediaType: string, openApiResponseMediaType?: OpenAPIV3.MediaTypeObject) { +function openApiResponseMediaToVariants(code: string, mediaType: string, openApiResponseMediaType?: OpenAPIV3.MediaTypeObject, openApiResponseHeaders?: ResponseHeaders) { if(!notEmpty(openApiResponseMediaType)) { return null; } if(isJsonMediaType(mediaType)) { - return openApiResponseExamplesToVariants(code, openApiResponseMediaType, "json"); + return openApiResponseExamplesToVariants(code, VariantTypes.JSON, mediaType, openApiResponseMediaType, openApiResponseHeaders); } if(isTextMediaType(mediaType)) { - return openApiResponseExamplesToVariants(code, openApiResponseMediaType, "text"); + return openApiResponseExamplesToVariants(code, VariantTypes.TEXT, mediaType, openApiResponseMediaType, openApiResponseHeaders); } return null; } @@ -121,7 +125,7 @@ function openApiResponseCodeToVariants(code: string, openApiResponse?: ResponseO const content = openApiResponse.content; if(content) { return Object.keys(content).map((mediaType: string) => { - return openApiResponseMediaToVariants(code, mediaType, content[mediaType]); + return openApiResponseMediaToVariants(code, mediaType, content[mediaType], openApiResponse.headers); }).flat().filter(notEmpty); } return openApiResponseNoContentToVariant(code, openApiResponse); diff --git a/packages/plugin-openapi/src/types.ts b/packages/plugin-openapi/src/types.ts index ee189e821..3328d1ded 100644 --- a/packages/plugin-openapi/src/types.ts +++ b/packages/plugin-openapi/src/types.ts @@ -10,6 +10,7 @@ export type OpenApiMockDocuments = OpenApiMockDocument[] export type ResponseObjectWithVariantId = OpenAPIV3.ResponseObject & { [MOCKS_SERVER_VARIANT_ID]?: string } export type ExampleObjectWithVariantId = OpenAPIV3.ExampleObject & { [MOCKS_SERVER_VARIANT_ID]?: string } +export type ResponseHeaders = OpenAPIV3.ResponseObject["headers"] export type OperationObjectWithRouteId = OpenAPIV3.OperationObject<{[MOCKS_SERVER_ROUTE_ID]?: string}> diff --git a/packages/plugin-openapi/test/custom-headers.spec.js b/packages/plugin-openapi/test/custom-headers.spec.js new file mode 100644 index 000000000..b469b85be --- /dev/null +++ b/packages/plugin-openapi/test/custom-headers.spec.js @@ -0,0 +1,93 @@ +import { startServer, fetchJson, fetchText, waitForServer } from "./support/helpers"; + +describe("when openapi response has headers", () => { + let server; + + beforeAll(async () => { + server = await startServer("custom-headers", { + log: "debug", + }); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + describe("routes", () => { + it("should have created routes from openapi document defined in files", async () => { + expect(server.mock.routes.plain).toEqual([ + { + id: "read-users", + url: "/api/users", + method: "get", + delay: null, + variants: ["read-users:one-user", "read-users:two-users"], + }, + { + id: "create-user", + url: "/api/users", + method: "post", + delay: null, + variants: ["create-user:success", "create-user:error"], + }, + ]); + }); + }); + + describe("get-users route", () => { + it("should have response headers in base collection", async () => { + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + ]); + expect(response.status).toEqual(200); + expect(response.headers.get("x-custom-header-read-users")).toEqual("read-users-value"); + expect(response.headers.get("Content-Type")).toEqual("application/json; charset=utf-8"); + }); + + it("should have response headers in all-users collection", async () => { + await server.mock.collections.select("all-users", { check: true }); + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ]); + expect(response.status).toEqual(200); + expect(response.headers.get("x-custom-header-read-users")).toEqual("read-users-value"); + expect(response.headers.get("Content-Type")).toEqual("application/json; charset=utf-8"); + }); + }); + + describe("post-users route", () => { + it("should have response headers in all-users collection", async () => { + const response = await fetchJson("/api/users", { + method: "POST", + }); + expect(response.body).toBe(undefined); + expect(response.status).toEqual(201); + expect(response.headers.get("x-custom-header-create-user")).toEqual("create-user-value"); + expect(response.headers.get("x-custom-header-create-user-2")).toEqual("create-user-value-2"); + expect(response.headers.get("Content-Type")).toEqual(null); + }); + + it("should have text/html header in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchText("/api/users", { + method: "POST", + }); + expect(response.body).toEqual("
Error
"); + expect(response.status).toEqual(400); + expect(response.headers.get("Content-Type")).toEqual("text/html; charset=utf-8"); + }); + }); +}); diff --git a/packages/plugin-openapi/test/fixtures/custom-headers/collections.js b/packages/plugin-openapi/test/fixtures/custom-headers/collections.js new file mode 100644 index 000000000..2151a20ab --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/custom-headers/collections.js @@ -0,0 +1 @@ +module.exports = require("../custom-ids/collections"); diff --git a/packages/plugin-openapi/test/fixtures/custom-headers/openapi/api.js b/packages/plugin-openapi/test/fixtures/custom-headers/openapi/api.js new file mode 100644 index 000000000..92f881db9 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/custom-headers/openapi/api.js @@ -0,0 +1,49 @@ +const deepMerge = require("deepmerge"); + +const openApiDocument = require("../../custom-ids/openapi/api")[0].document; + +module.exports = [ + { + basePath: "/api", + document: deepMerge(openApiDocument, { + paths: { + "/users": { + get: { + responses: { + "200": { + headers: { + "x-custom-header-read-users": "read-users-value", + "Content-Type": "foo", + }, + }, + }, + }, + post: { + responses: { + "201": { + headers: { + "x-custom-header-create-user": "create-user-value", + "x-custom-header-create-user-2": "create-user-value-2", + }, + }, + "400": { + content: { + "text/html": { + examples: { + "error-message": { + "x-mocks-server-variant-id": "error", + value: "
Error
", + }, + }, + }, + "text/plain": undefined, + }, + }, + }, + }, + }, + "/users/{id}": undefined, + }, + }), + }, +]; From db8a38a64af6499fe2b90768999d00d6567b1da8 Mon Sep 17 00:00:00 2001 From: javierbrea Date: Fri, 19 Aug 2022 09:54:31 +0200 Subject: [PATCH 06/21] feat(#384): Support default response --- packages/plugin-openapi/jest.config.js | 2 +- .../plugin-openapi/src/mocks-server-core.d.ts | 26 +++- packages/plugin-openapi/src/openapi.ts | 53 +++++--- .../test/code-wildcards.spec.js | 114 ++++++++++++++++ .../test/custom-headers.spec.js | 4 +- .../fixtures/code-wildcards/collections.js | 1 + .../fixtures/code-wildcards/openapi/api.js | 122 ++++++++++++++++++ 7 files changed, 301 insertions(+), 21 deletions(-) create mode 100644 packages/plugin-openapi/test/code-wildcards.spec.js create mode 100644 packages/plugin-openapi/test/fixtures/code-wildcards/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/code-wildcards/openapi/api.js diff --git a/packages/plugin-openapi/jest.config.js b/packages/plugin-openapi/jest.config.js index 317523fb4..9f8ff7acc 100644 --- a/packages/plugin-openapi/jest.config.js +++ b/packages/plugin-openapi/jest.config.js @@ -26,7 +26,7 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: ["/test/**/*.spec.js"], - // testMatch: ["/test/**/custom-headers.spec.js"], + // testMatch: ["/test/**/code-wildcards.spec.js"], // The test environment that will be used for testing testEnvironment: "node", diff --git a/packages/plugin-openapi/src/mocks-server-core.d.ts b/packages/plugin-openapi/src/mocks-server-core.d.ts index 1f7df09a4..1d71f0799 100644 --- a/packages/plugin-openapi/src/mocks-server-core.d.ts +++ b/packages/plugin-openapi/src/mocks-server-core.d.ts @@ -42,16 +42,40 @@ declare module "@mocks-server/core" { path: string } + interface HTTPHeaders { + [header: string]: string; + } + + interface JsonVariantOptions { + status: number, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + body: object | [], + headers?: HTTPHeaders, + } + + interface TextVariantOptions { + status: number, + body: string, + headers?: HTTPHeaders, + } + + interface StatusVariantOptions { + status: number, + } + interface RouteVariant { id: string, type: RouteVariantTypes + options: JsonVariantOptions | TextVariantOptions | StatusVariantOptions, } + type RouteVariants = RouteVariant[] | null + interface Route { id: string, url: string, method: OpenAPIV3.HttpMethods, - variants: RouteVariant[], + variants: RouteVariants, } type Routes = Route[] diff --git a/packages/plugin-openapi/src/openapi.ts b/packages/plugin-openapi/src/openapi.ts index fb7de9c68..dc7dfff39 100644 --- a/packages/plugin-openapi/src/openapi.ts +++ b/packages/plugin-openapi/src/openapi.ts @@ -1,5 +1,5 @@ import type { OpenAPIV3 } from "openapi-types"; -import type { Routes, RouteVariantTypes } from "@mocks-server/core"; +import type { HTTPHeaders, Routes, RouteVariant, RouteVariants, RouteVariantTypes } from "@mocks-server/core"; import { OpenAPIV3 as OpenApiV3Object } from "openapi-types"; import type { OpenApiMockDocuments, OpenApiMockDocument, ResponseObjectWithVariantId, ExampleObjectWithVariantId, OperationObjectWithRouteId, ResponseHeaders } from "./types"; @@ -49,7 +49,7 @@ function isTextMediaType(mediaType: string): boolean { return mediaType.includes("text/"); } -function openApiResponseBaseVariant(variantType: RouteVariantTypes, code: string, options: { customId?: string, exampleId?: string }) { +function openApiResponseBaseVariant(variantType: RouteVariantTypes, code: number, options: { customId?: string, exampleId?: string }): Partial { let id; if (options.customId) { id = options.customId; @@ -62,8 +62,26 @@ function openApiResponseBaseVariant(variantType: RouteVariantTypes, code: string }; } +function replaceCodeWildcards(code: string): string { + return code.replace(/X/gim, "0"); +} + +function hasAnySuccessCode(codes: string[]): boolean { + return !!codes.find((code) => { + return code.startsWith("1") || code.startsWith("2") || code.startsWith("3"); + }); +} + +function getStatusCode(code: string, codes: string[]): number { + const statusCodes = codes.map(replaceCodeWildcards) + if (code === "default") { + return hasAnySuccessCode(statusCodes) ? 400 : 200; + } + return Number(replaceCodeWildcards(code)); +} + // TODO, support also ReferenceObject in examples -function openApiResponseExampleToVariant(exampleId: string, code: string, variantType: RouteVariantTypes, mediaType: string, openApiResponseExample: ExampleObjectWithVariantId, openApiResponseHeaders?: ResponseHeaders) { +function openApiResponseExampleToVariant(exampleId: string, code: number, variantType: RouteVariantTypes, mediaType: string, openApiResponseExample: ExampleObjectWithVariantId, openApiResponseHeaders?: ResponseHeaders): RouteVariant | null { if(!notEmpty(openApiResponseExample) || !notEmpty(openApiResponseExample.value)) { return null; } @@ -77,24 +95,25 @@ function openApiResponseExampleToVariant(exampleId: string, code: string, varian ...openApiResponseHeaders, [CONTENT_TYPE_HEADER]: mediaType, }, - status: Number(code), + status: code, body: openApiResponseExample.value } - } + } as RouteVariant; } -function openApiResponseNoContentToVariant(code: string, openApiResponse: ResponseObjectWithVariantId) { +function openApiResponseNoContentToVariant(code: number, openApiResponse: ResponseObjectWithVariantId): RouteVariant { const baseVariant = openApiResponseBaseVariant(VariantTypes.STATUS, code, { customId: openApiResponse[MOCKS_SERVER_VARIANT_ID] }); return { ...baseVariant, options: { - headers: openApiResponse.headers, - status: Number(code), + // TODO, convert possible ref + headers: openApiResponse.headers as HTTPHeaders, + status: code, } - } + } as RouteVariant; } -function openApiResponseExamplesToVariants(code: string, variantType: RouteVariantTypes, mediaType: string, openApiResponseMediaType: OpenAPIV3.MediaTypeObject, openApiResponseHeaders?: ResponseHeaders) { +function openApiResponseExamplesToVariants(code: number, variantType: RouteVariantTypes, mediaType: string, openApiResponseMediaType: OpenAPIV3.MediaTypeObject, openApiResponseHeaders?: ResponseHeaders): RouteVariants { const examples = openApiResponseMediaType.examples; if(!notEmpty(examples)) { return null; @@ -105,7 +124,7 @@ function openApiResponseExamplesToVariants(code: string, variantType: RouteVaria }).filter(notEmpty); } -function openApiResponseMediaToVariants(code: string, mediaType: string, openApiResponseMediaType?: OpenAPIV3.MediaTypeObject, openApiResponseHeaders?: ResponseHeaders) { +function openApiResponseMediaToVariants(code: number, mediaType: string, openApiResponseMediaType?: OpenAPIV3.MediaTypeObject, openApiResponseHeaders?: ResponseHeaders): RouteVariants { if(!notEmpty(openApiResponseMediaType)) { return null; } @@ -118,7 +137,7 @@ function openApiResponseMediaToVariants(code: string, mediaType: string, openApi return null; } -function openApiResponseCodeToVariants(code: string, openApiResponse?: ResponseObjectWithVariantId) { +function openApiResponseCodeToVariants(code: number, openApiResponse?: ResponseObjectWithVariantId): RouteVariants { if(!notEmpty(openApiResponse)) { return []; } @@ -128,16 +147,18 @@ function openApiResponseCodeToVariants(code: string, openApiResponse?: ResponseO return openApiResponseMediaToVariants(code, mediaType, content[mediaType], openApiResponse.headers); }).flat().filter(notEmpty); } - return openApiResponseNoContentToVariant(code, openApiResponse); + return [openApiResponseNoContentToVariant(code, openApiResponse)]; } -function routeVariants(openApiResponses?: OpenAPIV3.ResponsesObject) { +function routeVariants(openApiResponses?: OpenAPIV3.ResponsesObject): RouteVariants { if(!notEmpty(openApiResponses)) { return []; } - return Object.keys(openApiResponses).map((code: string) => { + const codes = Object.keys(openApiResponses); + + return codes.map((code: string) => { const response = openApiResponses[code] as ResponseObjectWithVariantId; - return openApiResponseCodeToVariants(code, response); + return openApiResponseCodeToVariants(getStatusCode(code, codes), response); }).flat().filter(notEmpty); } diff --git a/packages/plugin-openapi/test/code-wildcards.spec.js b/packages/plugin-openapi/test/code-wildcards.spec.js new file mode 100644 index 000000000..db6b93cc2 --- /dev/null +++ b/packages/plugin-openapi/test/code-wildcards.spec.js @@ -0,0 +1,114 @@ +import { startServer, fetchJson, fetchText, waitForServer } from "./support/helpers"; + +describe("when openapi codes include have wildcards", () => { + let server; + + describe("when fixture is api-users", () => { + beforeAll(async () => { + server = await startServer("code-wildcards"); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + describe("routes", () => { + it("should have created routes from openapi document defined in files", async () => { + expect(server.mock.routes.plain).toEqual([ + { + id: "get-users", + url: "/api/users", + method: "get", + delay: null, + variants: ["get-users:200-json-one-user", "get-users:200-json-two-users"], + }, + { + id: "post-users", + url: "/api/users", + method: "post", + delay: null, + variants: ["post-users:201-status", "post-users:400-text-error-message"], + }, + { + id: "get-users-id", + url: "/api/users/:id", + method: "get", + delay: null, + variants: ["get-users-id:404-json-not-found", "get-users-id:200-json-success"], + }, + ]); + }); + }); + + describe("get-users route", () => { + it("should have 200-json-one-user variant available in base collection", async () => { + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + ]); + expect(response.status).toEqual(200); + }); + + it("should have 200-json-two-users variant available in all-users collection", async () => { + await server.mock.collections.select("all-users", { check: true }); + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ]); + expect(response.status).toEqual(200); + }); + }); + + describe("post-users route", () => { + it("should have 201-status variant available in all-users collection", async () => { + const response = await fetchJson("/api/users", { + method: "POST", + }); + expect(response.body).toBe(undefined); + expect(response.status).toEqual(201); + }); + + it("should have 400-text-error-message variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchText("/api/users", { + method: "POST", + }); + expect(response.body).toBe("Bad data"); + expect(response.status).toEqual(400); + }); + }); + + describe("get-users-id route", () => { + it("should have 200-json-success variant available in base collection", async () => { + await server.mock.collections.select("base", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + id: 1, + name: "John Doe", + }); + expect(response.status).toEqual(200); + }); + + it("should have 200-json-two-users variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + code: 404, + message: "Not found", + }); + expect(response.status).toEqual(404); + }); + }); + }); +}); diff --git a/packages/plugin-openapi/test/custom-headers.spec.js b/packages/plugin-openapi/test/custom-headers.spec.js index b469b85be..095e44286 100644 --- a/packages/plugin-openapi/test/custom-headers.spec.js +++ b/packages/plugin-openapi/test/custom-headers.spec.js @@ -4,9 +4,7 @@ describe("when openapi response has headers", () => { let server; beforeAll(async () => { - server = await startServer("custom-headers", { - log: "debug", - }); + server = await startServer("custom-headers"); await waitForServer(); }); diff --git a/packages/plugin-openapi/test/fixtures/code-wildcards/collections.js b/packages/plugin-openapi/test/fixtures/code-wildcards/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/code-wildcards/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/code-wildcards/openapi/api.js b/packages/plugin-openapi/test/fixtures/code-wildcards/openapi/api.js new file mode 100644 index 000000000..3b68ecf54 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/code-wildcards/openapi/api.js @@ -0,0 +1,122 @@ +module.exports = [ + { + basePath: "/api", + document: { + openapi: "3.1.0", + info: { + title: "Testing API", + description: "OpenApi document to create mock for testing purpses", + contact: { + email: "info@mocks-server.org", + }, + }, + paths: { + "/users": { + get: { + summary: "Return all users", + description: "Use it to get current users", + responses: { + default: { + description: "successful operation", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/Users", + }, + examples: { + "one-user": { + summary: "One route", + value: [ + { + id: 1, + name: "John Doe", + }, + ], + }, + "two-users": { + summary: "Two users", + value: [ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ], + }, + }, + }, + }, + }, + }, + }, + post: { + summary: "Create an user", + responses: { + "201": { + description: "successful operation", + }, + default: { + description: "bad data", + content: { + "text/plain": { + examples: { + "error-message": { + summary: "Error message", + value: "Bad data", + }, + }, + }, + }, + }, + }, + }, + }, + "/users/{id}": { + get: { + summary: "Return one user", + responses: { + "2XX": { + description: "successful operation", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/User", + }, + examples: { + success: { + summary: "One user", + value: { + id: 1, + name: "John Doe", + }, + }, + }, + }, + }, + }, + "404": { + description: "user not found", + content: { + "application/json": { + examples: { + "not-found": { + summary: "Not found error", + value: { + code: 404, + message: "Not found", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +]; From a2f9989df4c6ca6e9681c2f1e163386a46d1791b Mon Sep 17 00:00:00 2001 From: javierbrea Date: Fri, 19 Aug 2022 10:12:32 +0200 Subject: [PATCH 07/21] feat(#384): Support openapi operationId. Use it as route id if present --- packages/plugin-openapi/src/openapi.ts | 6 +++--- .../plugin-openapi/test/fixtures/custom-ids/openapi/api.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/plugin-openapi/src/openapi.ts b/packages/plugin-openapi/src/openapi.ts index dc7dfff39..8710298c5 100644 --- a/packages/plugin-openapi/src/openapi.ts +++ b/packages/plugin-openapi/src/openapi.ts @@ -162,8 +162,8 @@ function routeVariants(openApiResponses?: OpenAPIV3.ResponsesObject): RouteVaria }).flat().filter(notEmpty); } -function getMockServerRouteId(openApiOperation: OperationObjectWithRouteId): string | undefined { - return openApiOperation[MOCKS_SERVER_ROUTE_ID]; +function getCustomRouteId(openApiOperation: OperationObjectWithRouteId): string | undefined { + return openApiOperation[MOCKS_SERVER_ROUTE_ID] || openApiOperation.operationId; } function openApiPathToRoutes(path: string, basePath = "", openApiPathObject?: OpenAPIV3.PathItemObject ): Routes | null { @@ -174,7 +174,7 @@ function openApiPathToRoutes(path: string, basePath = "", openApiPathObject?: Op if(notEmpty(openApiPathObject[method])) { const openApiOperation = openApiPathObject[method] as OperationObjectWithRouteId; return { - id: routeId(path, method, getMockServerRouteId(openApiOperation)), + id: routeId(path, method, getCustomRouteId(openApiOperation)), url: routeUrl(path, basePath), method, variants: routeVariants(openApiOperation.responses), diff --git a/packages/plugin-openapi/test/fixtures/custom-ids/openapi/api.js b/packages/plugin-openapi/test/fixtures/custom-ids/openapi/api.js index 831cd8c94..4c2622b97 100644 --- a/packages/plugin-openapi/test/fixtures/custom-ids/openapi/api.js +++ b/packages/plugin-openapi/test/fixtures/custom-ids/openapi/api.js @@ -28,7 +28,7 @@ module.exports = [ }, }, post: { - "x-mocks-server-route-id": "create-user", + operationId: "create-user", responses: { "201": { "x-mocks-server-variant-id": "success", From 3b8ef72322c652839d19aab04752c56fd5e619f1 Mon Sep 17 00:00:00 2001 From: javierbrea Date: Fri, 19 Aug 2022 13:17:23 +0200 Subject: [PATCH 08/21] feat(#384): Support openapi refs --- packages/plugin-openapi/jest.config.js | 2 +- packages/plugin-openapi/package.json | 3 +- packages/plugin-openapi/src/Plugin.ts | 66 +++++- .../plugin-openapi/src/mocks-server-core.d.ts | 10 +- packages/plugin-openapi/src/openapi.ts | 2 +- packages/plugin-openapi/src/types.ts | 8 +- .../fixtures/code-wildcards/openapi/api.js | 18 +- .../test/fixtures/refs/collections.js | 1 + .../test/fixtures/refs/openapi/api.js | 145 +++++++++++++ .../wrong-refs-options/collections.js | 1 + .../wrong-refs-options/openapi/api.js | 143 +++++++++++++ .../test/fixtures/wrong-refs/collections.js | 1 + .../test/fixtures/wrong-refs/openapi/api.js | 139 ++++++++++++ packages/plugin-openapi/test/openapi/users.js | 18 +- packages/plugin-openapi/test/refs.spec.js | 202 ++++++++++++++++++ pnpm-lock.yaml | 113 ++++++++-- 16 files changed, 832 insertions(+), 40 deletions(-) create mode 100644 packages/plugin-openapi/test/fixtures/refs/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/refs/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/wrong-refs-options/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/wrong-refs-options/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/wrong-refs/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/wrong-refs/openapi/api.js create mode 100644 packages/plugin-openapi/test/refs.spec.js diff --git a/packages/plugin-openapi/jest.config.js b/packages/plugin-openapi/jest.config.js index 9f8ff7acc..767ad15a2 100644 --- a/packages/plugin-openapi/jest.config.js +++ b/packages/plugin-openapi/jest.config.js @@ -26,7 +26,7 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: ["/test/**/*.spec.js"], - // testMatch: ["/test/**/code-wildcards.spec.js"], + // testMatch: ["/test/**/refs.spec.js"], // The test environment that will be used for testing testEnvironment: "node", diff --git a/packages/plugin-openapi/package.json b/packages/plugin-openapi/package.json index 3e941acdf..6d3ff80d5 100644 --- a/packages/plugin-openapi/package.json +++ b/packages/plugin-openapi/package.json @@ -35,7 +35,8 @@ "@mocks-server/core": ">=3.10.0 <4.x" }, "dependencies": { - "openapi-types": "12.0.0" + "openapi-types": "12.0.0", + "json-refs": "3.0.15" }, "devDependencies": { "@mocks-server/core": "workspace:*", diff --git a/packages/plugin-openapi/src/Plugin.ts b/packages/plugin-openapi/src/Plugin.ts index cbef8dafb..e07dcb8f3 100644 --- a/packages/plugin-openapi/src/Plugin.ts +++ b/packages/plugin-openapi/src/Plugin.ts @@ -1,11 +1,26 @@ +import { resolveRefs } from "json-refs"; + import type { Core, MockLoaders, FilesContents } from "@mocks-server/core"; +import type { OpenAPIV3 } from "openapi-types"; +import type { ResolvedRefsResults, UnresolvedRefDetails } from "json-refs"; +import type { OpenApiMockDocuments, OpenApiMockDocument } from "./types"; -import { openApiMockDocumentsToRoutes } from "./openapi"; +import { openApiMockDocumentsToRoutes, notEmpty } from "./openapi"; const PLUGIN_ID = "openapi"; const DEFAULT_FOLDER = "openapi"; -// TODO, add options: path +function documentRefsErrors(refsResults: ResolvedRefsResults): Error[] { + const refs = refsResults.refs; + return Object.keys(refs).map((refKey) => { + // @ts-expect-error expression of type 'string' can't be used to index type 'ResolvedRefDetails', but resolvedRefDetails.refs is in fact an object + const ref = refs[refKey] as UnresolvedRefDetails; + if(ref.error) { + return new Error(ref.error); + } + return null; + }).filter(notEmpty) +} class Plugin { static get id() { @@ -16,12 +31,15 @@ class Plugin { private _alerts: Core["alerts"] private _files: Core["files"] private _loadRoutes: MockLoaders["loadRoutes"] + private _documentsAlerts: Core["alerts"] constructor({ logger, alerts, mock, files }: Core) { this._logger = logger; this._alerts = alerts; this._files = files; + this._documentsAlerts = this._alerts.collection("documents"); + const { loadRoutes } = mock.createLoaders(); this._loadRoutes = loadRoutes; this._files.createLoader({ @@ -29,15 +47,53 @@ class Plugin { src: `${DEFAULT_FOLDER}/**/*`, onLoad: this._onLoadFiles.bind(this), }) + this._resolveMockDocumentRefs = this._resolveMockDocumentRefs.bind(this); + this._addOpenApiRefAlert = this._addOpenApiRefAlert.bind(this); + } + + _addOpenApiRefAlert(error: Error): void { + this._documentsAlerts.set(String(this._documentsAlerts.flat.length), "Error resolving openapi $ref", error); + } + + _resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions?: OpenApiMockDocument["refs"]): Promise { + return resolveRefs(document, refsOptions).then((res) => { + this._logger.silly(`Document with resolved refs: '${JSON.stringify(res)}'`); + const refsErrors = documentRefsErrors(res); + refsErrors.forEach(this._addOpenApiRefAlert) + return res.resolved as OpenAPIV3.Document; + }).catch((error) => { + this._documentsAlerts.set(String(this._documentsAlerts.flat.length), "Error loading openapi definition", error); + return null; + }); + } + + async _resolveMockDocumentRefs(documentMock: OpenApiMockDocument): Promise { + const document = await this._resolveDocumentRefs(documentMock.document, documentMock.refs); + if(document) { + return { + ...documentMock, + document, + } + } + return null; + } + + _resolveMockDocumentsRefs(documents: OpenApiMockDocuments): Promise { + return Promise.all(documents.map(this._resolveMockDocumentRefs)).then((resolvedDocuments) => { + return resolvedDocuments.filter(notEmpty); + }) } - _onLoadFiles(filesContents: FilesContents) { + async _onLoadFiles(filesContents: FilesContents) { + this._documentsAlerts.clean(); const openApiMockDocuments = filesContents .map((fileDetails) => { return fileDetails.content; }).flat(); - this._logger.debug(`Creating routes from openApi definitions: '${JSON.stringify(openApiMockDocuments)}'`); - const routes = openApiMockDocumentsToRoutes(openApiMockDocuments); + this._logger.debug(`Resolving refs in openApi definitions: '${JSON.stringify(openApiMockDocuments)}'`); + const resolvedOpenApiMockDocuments = await this._resolveMockDocumentsRefs(openApiMockDocuments); + this._logger.debug(`Creating routes from openApi definitions: '${JSON.stringify(resolvedOpenApiMockDocuments)}'`); + const routes = openApiMockDocumentsToRoutes(resolvedOpenApiMockDocuments); this._logger.debug(`Routes to load from openApi definitions: '${JSON.stringify(routes)}'`); this._logger.verbose(`Loading ${routes.length} routes from openApi definitions found in '${this._files.path}/${DEFAULT_FOLDER}'`); this._loadRoutes(routes); diff --git a/packages/plugin-openapi/src/mocks-server-core.d.ts b/packages/plugin-openapi/src/mocks-server-core.d.ts index 1d71f0799..ac439ef31 100644 --- a/packages/plugin-openapi/src/mocks-server-core.d.ts +++ b/packages/plugin-openapi/src/mocks-server-core.d.ts @@ -1,5 +1,5 @@ declare module "@mocks-server/core" { - import type Collection from "@mocks-server/nested-collections"; + import type { NestedCollections, Item } from "@mocks-server/nested-collections"; import type { OpenAPIV3 } from "openapi-types"; enum VariantTypes { @@ -13,6 +13,7 @@ declare module "@mocks-server/core" { interface Logger { verbose(message: string): void debug(message: string): void + silly(message: string): void } interface FileContents { @@ -88,9 +89,14 @@ declare module "@mocks-server/core" { createLoaders(): MockLoaders } + class Alerts extends NestedCollections { + // @ts-expect-error Nested collections must be extended in core + set(id: string, value: string, error: Error): Item + } + interface Core { logger: Logger - alerts: typeof Collection, + alerts: Alerts files: Files mock: Mock } diff --git a/packages/plugin-openapi/src/openapi.ts b/packages/plugin-openapi/src/openapi.ts index 8710298c5..0faa9d96f 100644 --- a/packages/plugin-openapi/src/openapi.ts +++ b/packages/plugin-openapi/src/openapi.ts @@ -8,7 +8,7 @@ import { MOCKS_SERVER_ROUTE_ID, MOCKS_SERVER_VARIANT_ID, VariantTypes, CONTENT_T const methods = Object.values(OpenApiV3Object.HttpMethods); -function notEmpty(value: TValue | null | undefined): value is TValue { +export function notEmpty(value: TValue | null | undefined): value is TValue { return value !== null && value !== undefined; } diff --git a/packages/plugin-openapi/src/types.ts b/packages/plugin-openapi/src/types.ts index 3328d1ded..6fca54526 100644 --- a/packages/plugin-openapi/src/types.ts +++ b/packages/plugin-openapi/src/types.ts @@ -1,8 +1,15 @@ import type { OpenAPIV3 } from "openapi-types"; + import { MOCKS_SERVER_ROUTE_ID, MOCKS_SERVER_VARIANT_ID } from "./constants"; +export interface RefsOptions { + location: string, + subDocPath: string, +} + export interface OpenApiMockDocument { basePath: string, + refs: RefsOptions, document: OpenAPIV3.Document } @@ -13,4 +20,3 @@ export type ExampleObjectWithVariantId = OpenAPIV3.ExampleObject & { [MOCKS_SERV export type ResponseHeaders = OpenAPIV3.ResponseObject["headers"] export type OperationObjectWithRouteId = OpenAPIV3.OperationObject<{[MOCKS_SERVER_ROUTE_ID]?: string}> - diff --git a/packages/plugin-openapi/test/fixtures/code-wildcards/openapi/api.js b/packages/plugin-openapi/test/fixtures/code-wildcards/openapi/api.js index 3b68ecf54..93897032a 100644 --- a/packages/plugin-openapi/test/fixtures/code-wildcards/openapi/api.js +++ b/packages/plugin-openapi/test/fixtures/code-wildcards/openapi/api.js @@ -5,6 +5,7 @@ module.exports = [ openapi: "3.1.0", info: { title: "Testing API", + version: "1.0.0", description: "OpenApi document to create mock for testing purpses", contact: { email: "info@mocks-server.org", @@ -20,9 +21,6 @@ module.exports = [ description: "successful operation", content: { "application/json": { - schema: { - $ref: "#/components/schemas/Users", - }, examples: { "one-user": { summary: "One route", @@ -76,15 +74,23 @@ module.exports = [ }, "/users/{id}": { get: { + parameters: [ + { + name: "id", + in: "path", + description: "ID the user", + required: true, + schema: { + type: "string", + }, + }, + ], summary: "Return one user", responses: { "2XX": { description: "successful operation", content: { "application/json": { - schema: { - $ref: "#/components/schemas/User", - }, examples: { success: { summary: "One user", diff --git a/packages/plugin-openapi/test/fixtures/refs/collections.js b/packages/plugin-openapi/test/fixtures/refs/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/refs/openapi/api.js b/packages/plugin-openapi/test/fixtures/refs/openapi/api.js new file mode 100644 index 000000000..b679e7ab6 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs/openapi/api.js @@ -0,0 +1,145 @@ +module.exports = [ + { + basePath: "/api", + document: { + openapi: "3.1.0", + info: { + title: "Testing API", + version: "1.0.0", + description: "OpenApi document to create mock for testing purpses", + contact: { + email: "info@mocks-server.org", + }, + }, + paths: { + "/users": { + $ref: "#/components/pathItems/Users", + }, + "/users/{id}": { + get: { + parameters: [ + { + name: "id", + in: "path", + description: "ID the user", + required: true, + schema: { + type: "string", + }, + }, + ], + summary: "Return one user", + responses: { + "200": { + $ref: "#/components/responses/User", + }, + "404": { + description: "user not found", + content: { + "application/json": { + examples: { + "not-found": { + $ref: "#/components/examples/NotFound", + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + pathItems: { + Users: { + get: { + summary: "Return all users", + description: "Use it to get current users", + responses: { + "200": { + description: "successful operation", + content: { + "application/json": { + examples: { + "one-user": { + summary: "One route", + value: [ + { + id: 1, + name: "John Doe", + }, + ], + }, + "two-users": { + summary: "Two users", + value: [ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ], + }, + }, + }, + }, + }, + }, + }, + post: { + summary: "Create an user", + responses: { + "201": { + description: "successful operation", + }, + "400": { + description: "bad data", + content: { + "text/plain": { + examples: { + "error-message": { + summary: "Error message", + value: "Bad data", + }, + }, + }, + }, + }, + }, + }, + }, + }, + responses: { + User: { + description: "successful operation", + content: { + "application/json": { + examples: { + success: { + summary: "One user", + value: { + id: 1, + name: "John Doe", + }, + }, + }, + }, + }, + }, + }, + examples: { + NotFound: { + summary: "Not found error", + value: { + code: 404, + message: "Not found", + }, + }, + }, + }, + }, + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/wrong-refs-options/collections.js b/packages/plugin-openapi/test/fixtures/wrong-refs-options/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/wrong-refs-options/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/wrong-refs-options/openapi/api.js b/packages/plugin-openapi/test/fixtures/wrong-refs-options/openapi/api.js new file mode 100644 index 000000000..cc06051f6 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/wrong-refs-options/openapi/api.js @@ -0,0 +1,143 @@ +module.exports = [ + { + basePath: "/api", + refs: { + subDocPath: "dasd", + location: "foo.json", + }, + document: { + openapi: "3.1.0", + info: { + title: "Testing API", + version: "1.0.0", + description: "OpenApi document to create mock for testing purpses", + contact: { + email: "info@mocks-server.org", + }, + }, + paths: { + "/users": { + $ref: "#/componednts/pathItems/Users", + }, + "/users/{id}": { + get: { + parameters: [ + { + name: "id", + in: "path", + description: "ID the user", + required: true, + schema: { + type: "string", + }, + }, + ], + summary: "Return one user", + responses: { + "200": { + $ref: "#/components/responses/User", + }, + "404": { + description: "user not found", + content: { + "application/json": { + examples: { + "not-found": { + $ref: "#/components/examples/NotFound", + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + pathItems: { + Users: { + get: { + summary: "Return all users", + description: "Use it to get current users", + responses: { + "200": { + description: "successful operation", + content: { + "application/json": { + examples: { + "one-user": { + $ref: 3, + }, + "two-users": { + summary: "Two users", + value: [ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ], + }, + }, + }, + }, + }, + }, + }, + post: { + summary: "Create an user", + responses: { + "201": { + description: "successful operation", + }, + "400": { + description: "bad data", + content: { + "text/plain": { + examples: { + "error-message": { + summary: "Error message", + value: "Bad data", + }, + }, + }, + }, + }, + }, + }, + }, + }, + responses: { + User: { + description: "successful operation", + content: { + "application/json": { + examples: { + success: { + summary: "One user", + value: { + id: 1, + name: "John Doe", + }, + }, + }, + }, + }, + }, + }, + examples: { + NotFound: { + summary: "Not found error", + value: { + code: 404, + message: "Not found", + }, + }, + }, + }, + }, + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/wrong-refs/collections.js b/packages/plugin-openapi/test/fixtures/wrong-refs/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/wrong-refs/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/wrong-refs/openapi/api.js b/packages/plugin-openapi/test/fixtures/wrong-refs/openapi/api.js new file mode 100644 index 000000000..592358ffb --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/wrong-refs/openapi/api.js @@ -0,0 +1,139 @@ +module.exports = [ + { + basePath: "/api", + document: { + openapi: "3.1.0", + info: { + title: "Testing API", + version: "1.0.0", + description: "OpenApi document to create mock for testing purpses", + contact: { + email: "info@mocks-server.org", + }, + }, + paths: { + "/users": { + $ref: "#/componednts/pathItems/Users", + }, + "/users/{id}": { + get: { + parameters: [ + { + name: "id", + in: "path", + description: "ID the user", + required: true, + schema: { + type: "string", + }, + }, + ], + summary: "Return one user", + responses: { + "200": { + $ref: "#/components/responses/User", + }, + "404": { + description: "user not found", + content: { + "application/json": { + examples: { + "not-found": { + $ref: "#/components/examples/NotFound", + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + pathItems: { + Users: { + get: { + summary: "Return all users", + description: "Use it to get current users", + responses: { + "200": { + description: "successful operation", + content: { + "application/json": { + examples: { + "one-user": { + $ref: 3, + }, + "two-users": { + summary: "Two users", + value: [ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ], + }, + }, + }, + }, + }, + }, + }, + post: { + summary: "Create an user", + responses: { + "201": { + description: "successful operation", + }, + "400": { + description: "bad data", + content: { + "text/plain": { + examples: { + "error-message": { + summary: "Error message", + value: "Bad data", + }, + }, + }, + }, + }, + }, + }, + }, + }, + responses: { + User: { + description: "successful operation", + content: { + "application/json": { + examples: { + success: { + summary: "One user", + value: { + id: 1, + name: "John Doe", + }, + }, + }, + }, + }, + }, + }, + examples: { + NotFound: { + summary: "Not found error", + value: { + code: 404, + message: "Not found", + }, + }, + }, + }, + }, + }, +]; diff --git a/packages/plugin-openapi/test/openapi/users.js b/packages/plugin-openapi/test/openapi/users.js index 31fed5bef..e1d98daa8 100644 --- a/packages/plugin-openapi/test/openapi/users.js +++ b/packages/plugin-openapi/test/openapi/users.js @@ -1,6 +1,7 @@ module.exports = { openapi: "3.1.0", info: { + version: "1.0.0", title: "Testing API", description: "OpenApi document to create mock for testing purpses", contact: { @@ -17,9 +18,6 @@ module.exports = { description: "successful operation", content: { "application/json": { - schema: { - $ref: "#/components/schemas/Users", - }, examples: { "one-user": { summary: "One route", @@ -73,15 +71,23 @@ module.exports = { }, "/users/{id}": { get: { + parameters: [ + { + name: "id", + in: "path", + description: "ID the user", + required: true, + schema: { + type: "string", + }, + }, + ], summary: "Return one user", responses: { "200": { description: "successful operation", content: { "application/json": { - schema: { - $ref: "#/components/schemas/User", - }, examples: { success: { summary: "One user", diff --git a/packages/plugin-openapi/test/refs.spec.js b/packages/plugin-openapi/test/refs.spec.js new file mode 100644 index 000000000..974434893 --- /dev/null +++ b/packages/plugin-openapi/test/refs.spec.js @@ -0,0 +1,202 @@ +import { startServer, fetchJson, fetchText, waitForServer } from "./support/helpers"; + +describe("when openapi has refs", () => { + let server; + + describe("when fixture is refs", () => { + beforeAll(async () => { + server = await startServer("refs"); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + describe("routes", () => { + it("should have created routes from openapi document defined in files", async () => { + expect(server.mock.routes.plain).toEqual([ + { + id: "get-users", + url: "/api/users", + method: "get", + delay: null, + variants: ["get-users:200-json-one-user", "get-users:200-json-two-users"], + }, + { + id: "post-users", + url: "/api/users", + method: "post", + delay: null, + variants: ["post-users:201-status", "post-users:400-text-error-message"], + }, + { + id: "get-users-id", + url: "/api/users/:id", + method: "get", + delay: null, + variants: ["get-users-id:200-json-success", "get-users-id:404-json-not-found"], + }, + ]); + }); + }); + + describe("get-users route", () => { + it("should have 200-json-one-user variant available in base collection", async () => { + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + ]); + expect(response.status).toEqual(200); + }); + + it("should have 200-json-two-users variant available in all-users collection", async () => { + await server.mock.collections.select("all-users", { check: true }); + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ]); + expect(response.status).toEqual(200); + }); + }); + + describe("post-users route", () => { + it("should have 201-status variant available in all-users collection", async () => { + const response = await fetchJson("/api/users", { + method: "POST", + }); + expect(response.body).toBe(undefined); + expect(response.status).toEqual(201); + }); + + it("should have 400-text-error-message variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchText("/api/users", { + method: "POST", + }); + expect(response.body).toBe("Bad data"); + expect(response.status).toEqual(400); + }); + }); + + describe("get-users-id route", () => { + it("should have 200-json-success variant available in base collection", async () => { + await server.mock.collections.select("base", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + id: 1, + name: "John Doe", + }); + expect(response.status).toEqual(200); + }); + + it("should have 200-json-two-users variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + code: 404, + message: "Not found", + }); + expect(response.status).toEqual(404); + }); + }); + }); + + describe("when fixture has wrong refs", () => { + beforeAll(async () => { + server = await startServer("wrong-refs"); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + describe("routes", () => { + it("should have created routes from openapi document defined in files", async () => { + expect(server.mock.routes.plain).toEqual([ + { + id: "get-users-id", + url: "/api/users/:id", + method: "get", + delay: null, + variants: ["get-users-id:200-json-success", "get-users-id:404-json-not-found"], + }, + ]); + }); + }); + + describe("alerts", () => { + it("should have added an alert about wrong refs", async () => { + const alert = + server.alerts.find((serverAlert) => serverAlert.id.includes("openapi")) || {}; + expect(alert.id).toEqual("plugins:openapi:documents:0"); + expect(alert.message).toEqual("Error resolving openapi $ref"); + expect(alert.error.message).toEqual( + "JSON Pointer points to missing location: #/componednts/pathItems/Users" + ); + }); + }); + + describe("get-users-id route", () => { + it("should have 200-json-success variant available in base collection", async () => { + await server.mock.collections.select("base", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + id: 1, + name: "John Doe", + }); + expect(response.status).toEqual(200); + }); + + it("should have 200-json-two-users variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + code: 404, + message: "Not found", + }); + expect(response.status).toEqual(404); + }); + }); + }); + + describe("when location option is set wrongly", () => { + beforeAll(async () => { + server = await startServer("wrong-refs-options"); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + describe("routes", () => { + it("should have created routes from openapi document defined in files", async () => { + expect(server.mock.routes.plain).toEqual([]); + }); + }); + + describe("alerts", () => { + it("should have added an alert about wrong openapi", async () => { + const alert = + server.alerts.find((serverAlert) => serverAlert.id.includes("openapi")) || {}; + expect(alert.id).toEqual("plugins:openapi:documents:0"); + expect(alert.message).toEqual("Error loading openapi definition"); + expect(alert.error.message).toEqual( + "options.subDocPath must be an Array of path segments or a valid JSON Pointer" + ); + }); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 189159c3d..54ef0c946 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -356,8 +356,10 @@ importers: specifiers: '@mocks-server/core': workspace:* '@mocks-server/nested-collections': workspace:* + json-refs: 3.0.15 openapi-types: 12.0.0 dependencies: + json-refs: 3.0.15 openapi-types: 12.0.0 devDependencies: '@mocks-server/core': link:../core @@ -4663,7 +4665,6 @@ packages: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: sprintf-js: 1.0.3 - dev: true /argparse/2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -4765,8 +4766,7 @@ packages: dev: true /asap/2.0.6: - resolution: {integrity: sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=} - dev: true + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} /asn1.js/5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} @@ -6161,7 +6161,6 @@ packages: /commander/4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - dev: true /commander/5.1.0: resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} @@ -6193,7 +6192,6 @@ packages: /component-emitter/1.3.0: resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} - dev: true /compose-function/3.0.3: resolution: {integrity: sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=} @@ -6328,6 +6326,10 @@ packages: engines: {node: '>= 0.6'} dev: false + /cookiejar/2.1.3: + resolution: {integrity: sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==} + dev: false + /copy-concurrently/1.0.5: resolution: {integrity: sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==} dependencies: @@ -7086,6 +7088,13 @@ packages: - supports-color dev: true + /dezalgo/1.0.3: + resolution: {integrity: sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==} + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dev: false + /diff-sequences/26.6.2: resolution: {integrity: sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==} engines: {node: '>= 10.14.2'} @@ -7953,7 +7962,6 @@ packages: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - dev: true /esquery/1.4.0: resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} @@ -8404,6 +8412,10 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true + /fast-safe-stringify/2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + dev: false + /fast-url-parser/1.1.3: resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==} dependencies: @@ -8709,6 +8721,15 @@ packages: combined-stream: 1.0.8 mime-types: 2.1.34 + /formidable/2.0.1: + resolution: {integrity: sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==} + dependencies: + dezalgo: 1.0.3 + hexoid: 1.0.0 + once: 1.4.0 + qs: 6.9.3 + dev: false + /forwarded/0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -9048,6 +9069,12 @@ packages: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true + /graphlib/2.1.8: + resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} + dependencies: + lodash: 4.17.21 + dev: false + /growly/1.3.0: resolution: {integrity: sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=} dev: true @@ -9185,6 +9212,11 @@ packages: resolution: {integrity: sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==} dev: true + /hexoid/1.0.0: + resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} + engines: {node: '>=8'} + dev: false + /hmac-drbg/1.0.1: resolution: {integrity: sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=} dependencies: @@ -11179,7 +11211,6 @@ packages: dependencies: argparse: 1.0.10 esprima: 4.0.1 - dev: true /js-yaml/4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} @@ -11255,6 +11286,23 @@ packages: /json-parse-even-better-errors/2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + /json-refs/3.0.15: + resolution: {integrity: sha512-0vOQd9eLNBL18EGl5yYaO44GhixmImes2wiYn9Z3sag3QnehWrYWlB9AFtMxCL2Bj3fyxgDYkxGFEU/chlYssw==} + engines: {node: '>=0.8'} + hasBin: true + dependencies: + commander: 4.1.1 + graphlib: 2.1.8 + js-yaml: 3.14.1 + lodash: 4.17.21 + native-promise-only: 0.8.1 + path-loader: 1.0.12 + slash: 3.0.0 + uri-js: 4.4.1 + transitivePeerDependencies: + - supports-color + dev: false + /json-schema-traverse/0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true @@ -11797,7 +11845,7 @@ packages: dev: true /methods/1.1.2: - resolution: {integrity: sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=} + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} /microevent.ts/0.1.1: @@ -11893,7 +11941,6 @@ packages: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} engines: {node: '>=4.0.0'} hasBin: true - dev: true /mimic-fn/2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} @@ -12108,6 +12155,10 @@ packages: - supports-color dev: true + /native-promise-only/0.8.1: + resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==} + dev: false + /native-url/0.2.6: resolution: {integrity: sha512-k4bDC87WtgrdD362gZz6zoiXQrl40kYlBmpfmSjwRO1VU0V5ccwJTlxuE72F6m3V0vc1xOf6n3UCP9QyerRqmA==} dependencies: @@ -12342,12 +12393,8 @@ packages: kind-of: 3.2.2 dev: true - /object-inspect/1.12.0: - resolution: {integrity: sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==} - /object-inspect/1.12.2: resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} - dev: true /object-is/1.1.5: resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} @@ -12770,6 +12817,15 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + /path-loader/1.0.12: + resolution: {integrity: sha512-n7oDG8B+k/p818uweWrOixY9/Dsr89o2TkCm6tOTex3fpdo2+BFDgR+KpB37mGKBRsBAlR8CIJMFN0OEy/7hIQ==} + dependencies: + native-promise-only: 0.8.1 + superagent: 7.1.6 + transitivePeerDependencies: + - supports-color + dev: false + /path-parse/1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true @@ -13773,6 +13829,11 @@ packages: engines: {node: '>=0.6'} dev: true + /qs/6.9.3: + resolution: {integrity: sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==} + engines: {node: '>=0.6'} + dev: false + /qs/6.9.6: resolution: {integrity: sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==} engines: {node: '>=0.6'} @@ -15054,7 +15115,7 @@ packages: dependencies: call-bind: 1.0.2 get-intrinsic: 1.1.1 - object-inspect: 1.12.0 + object-inspect: 1.12.2 /signal-exit/3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -15082,7 +15143,6 @@ packages: /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - dev: true /slice-ansi/3.0.0: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} @@ -15301,8 +15361,7 @@ packages: dev: true /sprintf-js/1.0.3: - resolution: {integrity: sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=} - dev: true + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} /sshpk/1.17.0: resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} @@ -15613,6 +15672,26 @@ packages: postcss-selector-parser: 3.1.2 dev: true + /superagent/7.1.6: + resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} + engines: {node: '>=6.4.0 <13 || >=14'} + deprecated: Please downgrade to v7.1.5 if you need IE/ActiveXObject support OR upgrade to v8.0.0 as we no longer support IE and published an incorrect patch version (see https://github.com/visionmedia/superagent/issues/1731) + dependencies: + component-emitter: 1.3.0 + cookiejar: 2.1.3 + debug: 4.3.4 + fast-safe-stringify: 2.1.1 + form-data: 4.0.0 + formidable: 2.0.1 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.10.3 + readable-stream: 3.6.0 + semver: 7.3.7 + transitivePeerDependencies: + - supports-color + dev: false + /supports-color/5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} From 18b3aa441cccbf941e8041340bcc9708f9462866 Mon Sep 17 00:00:00 2001 From: javierbrea Date: Fri, 19 Aug 2022 13:51:03 +0200 Subject: [PATCH 09/21] feat(#384): Set refs location option to current file location by default --- packages/plugin-openapi/src/Plugin.ts | 31 +-- packages/plugin-openapi/src/types.ts | 4 +- .../test/fixtures/refs-files/collections.js | 1 + .../refs-files/openapi-refs/user.json | 16 ++ .../refs-files/openapi-refs/users.json | 55 +++++ .../test/fixtures/refs-files/openapi/api.js | 65 ++++++ packages/plugin-openapi/test/refs.spec.js | 189 +++++++++--------- 7 files changed, 256 insertions(+), 105 deletions(-) create mode 100644 packages/plugin-openapi/test/fixtures/refs-files/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/refs-files/openapi-refs/user.json create mode 100644 packages/plugin-openapi/test/fixtures/refs-files/openapi-refs/users.json create mode 100644 packages/plugin-openapi/test/fixtures/refs-files/openapi/api.js diff --git a/packages/plugin-openapi/src/Plugin.ts b/packages/plugin-openapi/src/Plugin.ts index e07dcb8f3..2e05e8e9b 100644 --- a/packages/plugin-openapi/src/Plugin.ts +++ b/packages/plugin-openapi/src/Plugin.ts @@ -67,8 +67,8 @@ class Plugin { }); } - async _resolveMockDocumentRefs(documentMock: OpenApiMockDocument): Promise { - const document = await this._resolveDocumentRefs(documentMock.document, documentMock.refs); + async _resolveMockDocumentRefs(documentMock: OpenApiMockDocument, location: string): Promise { + const document = await this._resolveDocumentRefs(documentMock.document, {location, ...documentMock.refs}); if(document) { return { ...documentMock, @@ -78,22 +78,29 @@ class Plugin { return null; } - _resolveMockDocumentsRefs(documents: OpenApiMockDocuments): Promise { - return Promise.all(documents.map(this._resolveMockDocumentRefs)).then((resolvedDocuments) => { + _resolveMockDocumentsRefs(documents: OpenApiMockDocuments, location: string): Promise { + this._logger.debug(`Resolving refs in openApi definitions: '${JSON.stringify(documents)}'`); + return Promise.all(documents.map((document) => { + return this._resolveMockDocumentRefs(document, location); + })).then((resolvedDocuments) => { return resolvedDocuments.filter(notEmpty); }) } + async _loadMockDocumentsFromFilesContents(filesContents: FilesContents): Promise { + const openApiMockDocuments = await Promise.all( + filesContents.map((fileDetails) => { + return this._resolveMockDocumentsRefs(fileDetails.content, fileDetails.path); + }) + ); + return openApiMockDocuments.flat(); + } + async _onLoadFiles(filesContents: FilesContents) { this._documentsAlerts.clean(); - const openApiMockDocuments = filesContents - .map((fileDetails) => { - return fileDetails.content; - }).flat(); - this._logger.debug(`Resolving refs in openApi definitions: '${JSON.stringify(openApiMockDocuments)}'`); - const resolvedOpenApiMockDocuments = await this._resolveMockDocumentsRefs(openApiMockDocuments); - this._logger.debug(`Creating routes from openApi definitions: '${JSON.stringify(resolvedOpenApiMockDocuments)}'`); - const routes = openApiMockDocumentsToRoutes(resolvedOpenApiMockDocuments); + const openApiMockDocuments = await this._loadMockDocumentsFromFilesContents(filesContents); + this._logger.debug(`Creating routes from openApi definitions: '${JSON.stringify(openApiMockDocuments)}'`); + const routes = openApiMockDocumentsToRoutes(openApiMockDocuments); this._logger.debug(`Routes to load from openApi definitions: '${JSON.stringify(routes)}'`); this._logger.verbose(`Loading ${routes.length} routes from openApi definitions found in '${this._files.path}/${DEFAULT_FOLDER}'`); this._loadRoutes(routes); diff --git a/packages/plugin-openapi/src/types.ts b/packages/plugin-openapi/src/types.ts index 6fca54526..4f210e800 100644 --- a/packages/plugin-openapi/src/types.ts +++ b/packages/plugin-openapi/src/types.ts @@ -3,8 +3,8 @@ import type { OpenAPIV3 } from "openapi-types"; import { MOCKS_SERVER_ROUTE_ID, MOCKS_SERVER_VARIANT_ID } from "./constants"; export interface RefsOptions { - location: string, - subDocPath: string, + location?: string, + subDocPath?: string, } export interface OpenApiMockDocument { diff --git a/packages/plugin-openapi/test/fixtures/refs-files/collections.js b/packages/plugin-openapi/test/fixtures/refs-files/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs-files/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/refs-files/openapi-refs/user.json b/packages/plugin-openapi/test/fixtures/refs-files/openapi-refs/user.json new file mode 100644 index 000000000..17929514d --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs-files/openapi-refs/user.json @@ -0,0 +1,16 @@ +{ + "description": "successful operation", + "content": { + "application/json": { + "examples": { + "success": { + "summary": "One user", + "value": { + "id": 1, + "name": "John Doe" + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/plugin-openapi/test/fixtures/refs-files/openapi-refs/users.json b/packages/plugin-openapi/test/fixtures/refs-files/openapi-refs/users.json new file mode 100644 index 000000000..17fd2b221 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs-files/openapi-refs/users.json @@ -0,0 +1,55 @@ +{ + "get": { + "summary": "Return all users", + "description": "Use it to get current users", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "examples": { + "one-user": { + "summary": "One route", + "value": [{ + "id": 1, + "name": "John Doe" + }] + }, + "two-users": { + "summary": "Two users", + "value": [{ + "id": 1, + "name": "John Doe" + }, { + "id": 2, + "name": "Jane Doe" + }] + } + } + } + } + } + } + }, + "post": { + "summary": "Create an user", + "responses": { + "201": { + "description": "successful operation" + }, + "400": { + "description": "bad data", + "content": { + "text/plain": { + "examples": { + "error-message": { + "summary": "Error message", + "value": "Bad data" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/plugin-openapi/test/fixtures/refs-files/openapi/api.js b/packages/plugin-openapi/test/fixtures/refs-files/openapi/api.js new file mode 100644 index 000000000..5c8a404ff --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs-files/openapi/api.js @@ -0,0 +1,65 @@ +module.exports = [ + { + basePath: "/api", + document: { + openapi: "3.1.0", + info: { + title: "Testing API", + version: "1.0.0", + description: "OpenApi document to create mock for testing purpses", + contact: { + email: "info@mocks-server.org", + }, + }, + paths: { + "/users": { + $ref: "../openapi-refs/users.json", + }, + "/users/{id}": { + get: { + parameters: [ + { + name: "id", + in: "path", + description: "ID the user", + required: true, + schema: { + type: "string", + }, + }, + ], + summary: "Return one user", + responses: { + "200": { + $ref: "../openapi-refs/user.json", + }, + "404": { + description: "user not found", + content: { + "application/json": { + examples: { + "not-found": { + $ref: "#/components/examples/NotFound", + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + examples: { + NotFound: { + summary: "Not found error", + value: { + code: 404, + message: "Not found", + }, + }, + }, + }, + }, + }, +]; diff --git a/packages/plugin-openapi/test/refs.spec.js b/packages/plugin-openapi/test/refs.spec.js index 974434893..02a154fe6 100644 --- a/packages/plugin-openapi/test/refs.spec.js +++ b/packages/plugin-openapi/test/refs.spec.js @@ -3,114 +3,121 @@ import { startServer, fetchJson, fetchText, waitForServer } from "./support/help describe("when openapi has refs", () => { let server; - describe("when fixture is refs", () => { - beforeAll(async () => { - server = await startServer("refs"); - await waitForServer(); - }); - - afterAll(async () => { - await server.stop(); - }); - - describe("routes", () => { - it("should have created routes from openapi document defined in files", async () => { - expect(server.mock.routes.plain).toEqual([ - { - id: "get-users", - url: "/api/users", - method: "get", - delay: null, - variants: ["get-users:200-json-one-user", "get-users:200-json-two-users"], - }, - { - id: "post-users", - url: "/api/users", - method: "post", - delay: null, - variants: ["post-users:201-status", "post-users:400-text-error-message"], - }, - { - id: "get-users-id", - url: "/api/users/:id", - method: "get", - delay: null, - variants: ["get-users-id:200-json-success", "get-users-id:404-json-not-found"], - }, - ]); + function testValidRefs(fixture) { + describe(`when fixture is ${fixture}`, () => { + beforeAll(async () => { + server = await startServer(fixture, { + log: "debug", + }); + await waitForServer(); }); - }); - describe("get-users route", () => { - it("should have 200-json-one-user variant available in base collection", async () => { - const response = await fetchJson("/api/users"); - expect(response.body).toEqual([ - { - id: 1, - name: "John Doe", - }, - ]); - expect(response.status).toEqual(200); + afterAll(async () => { + await server.stop(); }); - it("should have 200-json-two-users variant available in all-users collection", async () => { - await server.mock.collections.select("all-users", { check: true }); - const response = await fetchJson("/api/users"); - expect(response.body).toEqual([ - { - id: 1, - name: "John Doe", - }, - { - id: 2, - name: "Jane Doe", - }, - ]); - expect(response.status).toEqual(200); + describe("routes", () => { + it("should have created routes from openapi document defined in files", async () => { + expect(server.mock.routes.plain).toEqual([ + { + id: "get-users", + url: "/api/users", + method: "get", + delay: null, + variants: ["get-users:200-json-one-user", "get-users:200-json-two-users"], + }, + { + id: "post-users", + url: "/api/users", + method: "post", + delay: null, + variants: ["post-users:201-status", "post-users:400-text-error-message"], + }, + { + id: "get-users-id", + url: "/api/users/:id", + method: "get", + delay: null, + variants: ["get-users-id:200-json-success", "get-users-id:404-json-not-found"], + }, + ]); + }); }); - }); - describe("post-users route", () => { - it("should have 201-status variant available in all-users collection", async () => { - const response = await fetchJson("/api/users", { - method: "POST", + describe("get-users route", () => { + it("should have 200-json-one-user variant available in base collection", async () => { + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + ]); + expect(response.status).toEqual(200); }); - expect(response.body).toBe(undefined); - expect(response.status).toEqual(201); - }); - it("should have 400-text-error-message variant available in users-error collection", async () => { - await server.mock.collections.select("users-error", { check: true }); - const response = await fetchText("/api/users", { - method: "POST", + it("should have 200-json-two-users variant available in all-users collection", async () => { + await server.mock.collections.select("all-users", { check: true }); + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ]); + expect(response.status).toEqual(200); }); - expect(response.body).toBe("Bad data"); - expect(response.status).toEqual(400); }); - }); - describe("get-users-id route", () => { - it("should have 200-json-success variant available in base collection", async () => { - await server.mock.collections.select("base", { check: true }); - const response = await fetchJson("/api/users/2"); - expect(response.body).toEqual({ - id: 1, - name: "John Doe", + describe("post-users route", () => { + it("should have 201-status variant available in all-users collection", async () => { + const response = await fetchJson("/api/users", { + method: "POST", + }); + expect(response.body).toBe(undefined); + expect(response.status).toEqual(201); + }); + + it("should have 400-text-error-message variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchText("/api/users", { + method: "POST", + }); + expect(response.body).toBe("Bad data"); + expect(response.status).toEqual(400); }); - expect(response.status).toEqual(200); }); - it("should have 200-json-two-users variant available in users-error collection", async () => { - await server.mock.collections.select("users-error", { check: true }); - const response = await fetchJson("/api/users/2"); - expect(response.body).toEqual({ - code: 404, - message: "Not found", + describe("get-users-id route", () => { + it("should have 200-json-success variant available in base collection", async () => { + await server.mock.collections.select("base", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + id: 1, + name: "John Doe", + }); + expect(response.status).toEqual(200); + }); + + it("should have 200-json-two-users variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + code: 404, + message: "Not found", + }); + expect(response.status).toEqual(404); }); - expect(response.status).toEqual(404); }); }); - }); + } + + testValidRefs("refs"); + testValidRefs("refs-files"); describe("when fixture has wrong refs", () => { beforeAll(async () => { From 14c96f1b34dd7b0fb94f540c179ac40089d6d0bf Mon Sep 17 00:00:00 2001 From: javierbrea Date: Mon, 22 Aug 2022 10:32:04 +0200 Subject: [PATCH 10/21] test(#384): Test remote refs in openapi definition --- .../refs-remote-server/collections.yaml | 3 + .../refs-remote-server/routes/refs.js | 17 +++++ .../refs-remote-server/static/user.json | 16 +++++ .../refs-remote-server/static/users.json | 55 ++++++++++++++++ .../test/fixtures/refs-remote/collections.js | 1 + .../test/fixtures/refs-remote/openapi/api.js | 65 +++++++++++++++++++ packages/plugin-openapi/test/refs.spec.js | 19 ++++++ 7 files changed, 176 insertions(+) create mode 100644 packages/plugin-openapi/test/fixtures/refs-remote-server/collections.yaml create mode 100644 packages/plugin-openapi/test/fixtures/refs-remote-server/routes/refs.js create mode 100644 packages/plugin-openapi/test/fixtures/refs-remote-server/static/user.json create mode 100644 packages/plugin-openapi/test/fixtures/refs-remote-server/static/users.json create mode 100644 packages/plugin-openapi/test/fixtures/refs-remote/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/refs-remote/openapi/api.js diff --git a/packages/plugin-openapi/test/fixtures/refs-remote-server/collections.yaml b/packages/plugin-openapi/test/fixtures/refs-remote-server/collections.yaml new file mode 100644 index 000000000..3c2610085 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs-remote-server/collections.yaml @@ -0,0 +1,3 @@ +- id: "base" + routes: + - static:success \ No newline at end of file diff --git a/packages/plugin-openapi/test/fixtures/refs-remote-server/routes/refs.js b/packages/plugin-openapi/test/fixtures/refs-remote-server/routes/refs.js new file mode 100644 index 000000000..1833ccb7e --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs-remote-server/routes/refs.js @@ -0,0 +1,17 @@ +const path = require("path"); + +module.exports = [ + { + id: "static", + url: "/", + variants: [ + { + id: "success", + type: "static", + options: { + path: path.resolve(__dirname, "..", "static"), + }, + }, + ], + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/refs-remote-server/static/user.json b/packages/plugin-openapi/test/fixtures/refs-remote-server/static/user.json new file mode 100644 index 000000000..17929514d --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs-remote-server/static/user.json @@ -0,0 +1,16 @@ +{ + "description": "successful operation", + "content": { + "application/json": { + "examples": { + "success": { + "summary": "One user", + "value": { + "id": 1, + "name": "John Doe" + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/plugin-openapi/test/fixtures/refs-remote-server/static/users.json b/packages/plugin-openapi/test/fixtures/refs-remote-server/static/users.json new file mode 100644 index 000000000..17fd2b221 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs-remote-server/static/users.json @@ -0,0 +1,55 @@ +{ + "get": { + "summary": "Return all users", + "description": "Use it to get current users", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "examples": { + "one-user": { + "summary": "One route", + "value": [{ + "id": 1, + "name": "John Doe" + }] + }, + "two-users": { + "summary": "Two users", + "value": [{ + "id": 1, + "name": "John Doe" + }, { + "id": 2, + "name": "Jane Doe" + }] + } + } + } + } + } + } + }, + "post": { + "summary": "Create an user", + "responses": { + "201": { + "description": "successful operation" + }, + "400": { + "description": "bad data", + "content": { + "text/plain": { + "examples": { + "error-message": { + "summary": "Error message", + "value": "Bad data" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/plugin-openapi/test/fixtures/refs-remote/collections.js b/packages/plugin-openapi/test/fixtures/refs-remote/collections.js new file mode 100644 index 000000000..a65111041 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs-remote/collections.js @@ -0,0 +1 @@ +module.exports = require("../../openapi/users-collection"); diff --git a/packages/plugin-openapi/test/fixtures/refs-remote/openapi/api.js b/packages/plugin-openapi/test/fixtures/refs-remote/openapi/api.js new file mode 100644 index 000000000..5e2792428 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs-remote/openapi/api.js @@ -0,0 +1,65 @@ +module.exports = [ + { + basePath: "/api", + document: { + openapi: "3.1.0", + info: { + title: "Testing API", + version: "1.0.0", + description: "OpenApi document to create mock for testing purpses", + contact: { + email: "info@mocks-server.org", + }, + }, + paths: { + "/users": { + $ref: "http://127.0.0.1:3200/users.json", + }, + "/users/{id}": { + get: { + parameters: [ + { + name: "id", + in: "path", + description: "ID the user", + required: true, + schema: { + type: "string", + }, + }, + ], + summary: "Return one user", + responses: { + "200": { + $ref: "http://127.0.0.1:3200/user.json", + }, + "404": { + description: "user not found", + content: { + "application/json": { + examples: { + "not-found": { + $ref: "#/components/examples/NotFound", + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + examples: { + NotFound: { + summary: "Not found error", + value: { + code: 404, + message: "Not found", + }, + }, + }, + }, + }, + }, +]; diff --git a/packages/plugin-openapi/test/refs.spec.js b/packages/plugin-openapi/test/refs.spec.js index 02a154fe6..84a7131fe 100644 --- a/packages/plugin-openapi/test/refs.spec.js +++ b/packages/plugin-openapi/test/refs.spec.js @@ -119,6 +119,25 @@ describe("when openapi has refs", () => { testValidRefs("refs"); testValidRefs("refs-files"); + describe("Remote refs", () => { + let refsServer; + beforeAll(async () => { + refsServer = await startServer("refs-remote-server", { + log: "debug", + server: { + port: 3200, + }, + }); + await waitForServer(3200); + }); + + afterAll(async () => { + await refsServer.stop(); + }); + + testValidRefs("refs-remote"); + }); + describe("when fixture has wrong refs", () => { beforeAll(async () => { server = await startServer("wrong-refs"); From 8a57ba3fce0b4ef4e22b8cd43afecbcc4b769d53 Mon Sep 17 00:00:00 2001 From: javierbrea Date: Mon, 22 Aug 2022 13:02:36 +0200 Subject: [PATCH 11/21] feat(#384): Export function to create routes from openapi definition --- packages/plugin-openapi/src/Plugin.ts | 76 +---- packages/plugin-openapi/src/index.ts | 1 + packages/plugin-openapi/src/openapi.ts | 80 +++++- packages/plugin-openapi/src/types.ts | 10 + .../refs-remote-server/static/openapi.json | 58 ++++ .../test/openapi-function-errors.spec.js | 72 +++++ packages/plugin-openapi/test/refs.spec.js | 269 ++++++++++++------ ...outes-from-file.spec.js => routes.spec.js} | 60 +++- 8 files changed, 450 insertions(+), 176 deletions(-) create mode 100644 packages/plugin-openapi/test/fixtures/refs-remote-server/static/openapi.json create mode 100644 packages/plugin-openapi/test/openapi-function-errors.spec.js rename packages/plugin-openapi/test/{routes-from-file.spec.js => routes.spec.js} (86%) diff --git a/packages/plugin-openapi/src/Plugin.ts b/packages/plugin-openapi/src/Plugin.ts index 2e05e8e9b..35d79c7e3 100644 --- a/packages/plugin-openapi/src/Plugin.ts +++ b/packages/plugin-openapi/src/Plugin.ts @@ -1,27 +1,10 @@ -import { resolveRefs } from "json-refs"; +import type { Routes, Core, MockLoaders, FilesContents } from "@mocks-server/core"; -import type { Core, MockLoaders, FilesContents } from "@mocks-server/core"; -import type { OpenAPIV3 } from "openapi-types"; -import type { ResolvedRefsResults, UnresolvedRefDetails } from "json-refs"; -import type { OpenApiMockDocuments, OpenApiMockDocument } from "./types"; - -import { openApiMockDocumentsToRoutes, notEmpty } from "./openapi"; +import { openApisToRoutes } from "./openapi"; const PLUGIN_ID = "openapi"; const DEFAULT_FOLDER = "openapi"; -function documentRefsErrors(refsResults: ResolvedRefsResults): Error[] { - const refs = refsResults.refs; - return Object.keys(refs).map((refKey) => { - // @ts-expect-error expression of type 'string' can't be used to index type 'ResolvedRefDetails', but resolvedRefDetails.refs is in fact an object - const ref = refs[refKey] as UnresolvedRefDetails; - if(ref.error) { - return new Error(ref.error); - } - return null; - }).filter(notEmpty) -} - class Plugin { static get id() { return PLUGIN_ID; @@ -47,50 +30,19 @@ class Plugin { src: `${DEFAULT_FOLDER}/**/*`, onLoad: this._onLoadFiles.bind(this), }) - this._resolveMockDocumentRefs = this._resolveMockDocumentRefs.bind(this); - this._addOpenApiRefAlert = this._addOpenApiRefAlert.bind(this); - } - - _addOpenApiRefAlert(error: Error): void { - this._documentsAlerts.set(String(this._documentsAlerts.flat.length), "Error resolving openapi $ref", error); - } - - _resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions?: OpenApiMockDocument["refs"]): Promise { - return resolveRefs(document, refsOptions).then((res) => { - this._logger.silly(`Document with resolved refs: '${JSON.stringify(res)}'`); - const refsErrors = documentRefsErrors(res); - refsErrors.forEach(this._addOpenApiRefAlert) - return res.resolved as OpenAPIV3.Document; - }).catch((error) => { - this._documentsAlerts.set(String(this._documentsAlerts.flat.length), "Error loading openapi definition", error); - return null; - }); - } - - async _resolveMockDocumentRefs(documentMock: OpenApiMockDocument, location: string): Promise { - const document = await this._resolveDocumentRefs(documentMock.document, {location, ...documentMock.refs}); - if(document) { - return { - ...documentMock, - document, - } - } - return null; - } - - _resolveMockDocumentsRefs(documents: OpenApiMockDocuments, location: string): Promise { - this._logger.debug(`Resolving refs in openApi definitions: '${JSON.stringify(documents)}'`); - return Promise.all(documents.map((document) => { - return this._resolveMockDocumentRefs(document, location); - })).then((resolvedDocuments) => { - return resolvedDocuments.filter(notEmpty); - }) } - async _loadMockDocumentsFromFilesContents(filesContents: FilesContents): Promise { + async _getRoutesFromFilesContents(filesContents: FilesContents): Promise { const openApiMockDocuments = await Promise.all( filesContents.map((fileDetails) => { - return this._resolveMockDocumentsRefs(fileDetails.content, fileDetails.path); + const fileContent = fileDetails.content; + // TODO, validate file content + this._logger.debug(`Creating routes from openApi definitions: '${JSON.stringify(fileContent)}'`); + return openApisToRoutes(fileContent, { + defaultLocation: fileDetails.path, + logger: this._logger, + alerts: this._documentsAlerts + }); }) ); return openApiMockDocuments.flat(); @@ -98,11 +50,9 @@ class Plugin { async _onLoadFiles(filesContents: FilesContents) { this._documentsAlerts.clean(); - const openApiMockDocuments = await this._loadMockDocumentsFromFilesContents(filesContents); - this._logger.debug(`Creating routes from openApi definitions: '${JSON.stringify(openApiMockDocuments)}'`); - const routes = openApiMockDocumentsToRoutes(openApiMockDocuments); + const routes = await this._getRoutesFromFilesContents(filesContents); this._logger.debug(`Routes to load from openApi definitions: '${JSON.stringify(routes)}'`); - this._logger.verbose(`Loading ${routes.length} routes from openApi definitions found in '${this._files.path}/${DEFAULT_FOLDER}'`); + this._logger.verbose(`Loading ${routes.length} routes from openApi definitions found in folder '${this._files.path}/${DEFAULT_FOLDER}'`); this._loadRoutes(routes); } } diff --git a/packages/plugin-openapi/src/index.ts b/packages/plugin-openapi/src/index.ts index 468a77034..b45172fe2 100644 --- a/packages/plugin-openapi/src/index.ts +++ b/packages/plugin-openapi/src/index.ts @@ -1,4 +1,5 @@ import Plugin from "./Plugin"; export * from "./types"; +export * from "./openapi"; export default Plugin; diff --git a/packages/plugin-openapi/src/openapi.ts b/packages/plugin-openapi/src/openapi.ts index 0faa9d96f..03961cb80 100644 --- a/packages/plugin-openapi/src/openapi.ts +++ b/packages/plugin-openapi/src/openapi.ts @@ -1,8 +1,10 @@ -import type { OpenAPIV3 } from "openapi-types"; -import type { HTTPHeaders, Routes, RouteVariant, RouteVariants, RouteVariantTypes } from "@mocks-server/core"; - import { OpenAPIV3 as OpenApiV3Object } from "openapi-types"; -import type { OpenApiMockDocuments, OpenApiMockDocument, ResponseObjectWithVariantId, ExampleObjectWithVariantId, OperationObjectWithRouteId, ResponseHeaders } from "./types"; +import { resolveRefs } from "json-refs"; + +import type { ResolvedRefsResults, UnresolvedRefDetails } from "json-refs"; +import type { OpenAPIV3 } from "openapi-types"; +import type { Alerts, HTTPHeaders, Routes, RouteVariant, RouteVariants, RouteVariantTypes } from "@mocks-server/core"; +import type { OpenApiMockDocuments, OpenApiMockDocument, ResponseObjectWithVariantId, ExampleObjectWithVariantId, OperationObjectWithRouteId, ResponseHeaders, OpenApiToRoutesAdvancedOptions } from "./types"; import { MOCKS_SERVER_ROUTE_ID, MOCKS_SERVER_VARIANT_ID, VariantTypes, CONTENT_TYPE_HEADER } from "./constants"; @@ -183,7 +185,7 @@ function openApiPathToRoutes(path: string, basePath = "", openApiPathObject?: Op }).filter(notEmpty); } -function openApiMockDocumentToRoutes(openApiMockDocument: OpenApiMockDocument): Routes { +function openApiDocumentToRoutes(openApiMockDocument: OpenApiMockDocument): Routes { const openApiDocument = openApiMockDocument.document; const basePath = openApiMockDocument.basePath; @@ -193,6 +195,70 @@ function openApiMockDocumentToRoutes(openApiMockDocument: OpenApiMockDocument): }).flat().filter(notEmpty); } -export function openApiMockDocumentsToRoutes(openApiMockDocuments: OpenApiMockDocuments): Routes { - return openApiMockDocuments.map(openApiMockDocumentToRoutes).flat(); +function documentRefsErrors(refsResults: ResolvedRefsResults): Error[] { + const refs = refsResults.refs; + return Object.keys(refs).map((refKey) => { + // @ts-expect-error expression of type 'string' can't be used to index type 'ResolvedRefDetails', but resolvedRefDetails.refs is in fact an object + const ref = refs[refKey] as UnresolvedRefDetails; + if(ref.error) { + return new Error(ref.error); + } + return null; + }).filter(notEmpty) +} + +function addOpenApiRefAlert(alerts: Alerts, error: Error): void { + alerts.set(String(alerts.flat.length), "Error resolving openapi $ref", error); +} + +function resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions: OpenApiMockDocument["refs"], { alerts, logger }: OpenApiToRoutesAdvancedOptions): Promise { + return resolveRefs(document, refsOptions).then((res) => { + if (logger) { + logger.silly(`Document with resolved refs: '${JSON.stringify(res)}'`); + } + const refsErrors = documentRefsErrors(res); + if (refsErrors.length) { + if(alerts) { + refsErrors.forEach((error: Error) => { + addOpenApiRefAlert(alerts, error); + }) + } else { + throw new Error(refsErrors.map((error) => error.message).join(". ")); + } + } + return res.resolved as OpenAPIV3.Document; + }).catch((error) => { + if(alerts) { + alerts.set(String(alerts.flat.length), "Error loading openapi definition", error); + return null; + } + return Promise.reject(error); + }); +} + +async function resolveOpenApiDocumentRefs(documentMock: OpenApiMockDocument, { defaultLocation, alerts, logger }: OpenApiToRoutesAdvancedOptions = {}): Promise { + const document = await resolveDocumentRefs(documentMock.document, {location: defaultLocation, ...documentMock.refs}, { alerts, logger }); + if(document) { + return { + ...documentMock, + document, + } + } + return null; +} + +export async function openApiToRoutes(openApiMockDocument: OpenApiMockDocument, advancedOptions?: OpenApiToRoutesAdvancedOptions): Promise { + const openApiDocument = await resolveOpenApiDocumentRefs(openApiMockDocument, advancedOptions); + if(!openApiDocument) { + return []; + } + return openApiDocumentToRoutes(openApiDocument); +} + +export function openApisToRoutes(openApiMockDocuments: OpenApiMockDocuments, advancedOptions?: OpenApiToRoutesAdvancedOptions): Promise { + return Promise.all(openApiMockDocuments.map((openApiMockDocument) => { + return openApiToRoutes(openApiMockDocument, advancedOptions); + })).then((allRoutes) => { + return allRoutes.flat(); + }); } diff --git a/packages/plugin-openapi/src/types.ts b/packages/plugin-openapi/src/types.ts index 4f210e800..e79e9f9a2 100644 --- a/packages/plugin-openapi/src/types.ts +++ b/packages/plugin-openapi/src/types.ts @@ -20,3 +20,13 @@ export type ExampleObjectWithVariantId = OpenAPIV3.ExampleObject & { [MOCKS_SERV export type ResponseHeaders = OpenAPIV3.ResponseObject["headers"] export type OperationObjectWithRouteId = OpenAPIV3.OperationObject<{[MOCKS_SERVER_ROUTE_ID]?: string}> + +export interface OpenApiToRoutesAdvancedOptions { + defaultLocation?: string, + // TODO, add alerts type when exported by core + // eslint-disable-next-line @typescript-eslint/no-explicit-any + alerts?: any, + // TODO, add alerts type when exported by core + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logger?: any +} diff --git a/packages/plugin-openapi/test/fixtures/refs-remote-server/static/openapi.json b/packages/plugin-openapi/test/fixtures/refs-remote-server/static/openapi.json new file mode 100644 index 000000000..7344ca8d1 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/refs-remote-server/static/openapi.json @@ -0,0 +1,58 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Testing API", + "version": "1.0.0", + "description": "OpenApi document to create mock for testing purpses", + "contact": { + "email": "info@mocks-server.org" + } + }, + "paths": { + "/users": { + "$ref": "http://127.0.0.1:3200/users.json" + }, + "/users/{id}": { + "get": { + "parameters": [{ + "name": "id", + "in": "path", + "description": "ID the user", + "required": true, + "schema": { + "type": "string" + } + }], + "summary": "Return one user", + "responses": { + "200": { + "$ref": "http://127.0.0.1:3200/user.json" + }, + "404": { + "description": "user not found", + "content": { + "application/json": { + "examples": { + "not-found": { + "$ref": "#/components/examples/NotFound" + } + } + } + } + } + } + } + } + }, + "components": { + "examples": { + "NotFound": { + "summary": "Not found error", + "value": { + "code": 404, + "message": "Not found" + } + } + } + } +} diff --git a/packages/plugin-openapi/test/openapi-function-errors.spec.js b/packages/plugin-openapi/test/openapi-function-errors.spec.js new file mode 100644 index 000000000..ccaef7fa3 --- /dev/null +++ b/packages/plugin-openapi/test/openapi-function-errors.spec.js @@ -0,0 +1,72 @@ +import deepMerge from "deepmerge"; + +import openApiDocument from "./openapi/users"; +import { openApiToRoutes } from "../src/index"; + +describe("when function is used and openapi definition is wrong", () => { + describe("when has a wrong ref", () => { + it("should throw an error with message", async () => { + await expect( + openApiToRoutes({ + basePath: "/api", + document: deepMerge(openApiDocument, { + paths: { + "/users": { + $ref: "#/foo/pathItems/Users", + }, + }, + }), + }) + ).rejects.toEqual( + new Error("JSON Pointer points to missing location: #/foo/pathItems/Users") + ); + }); + }); + + describe("when has multiple wrong refs", () => { + it("should throw an error with all messages", async () => { + await expect( + openApiToRoutes({ + basePath: "/api", + document: deepMerge(openApiDocument, { + paths: { + "/users": { + $ref: "#/foo/pathItems/Users", + }, + "/users/{id}": { + get: { + responses: { + "200": { + $ref: "#/components/foo/User", + }, + }, + }, + }, + }, + }), + }) + ).rejects.toEqual( + new Error( + "JSON Pointer points to missing location: #/foo/pathItems/Users. JSON Pointer points to missing location: #/components/foo/User" + ) + ); + }); + }); + + describe("when options are wrong", () => { + it("should throw an error with message", async () => { + await expect( + openApiToRoutes({ + basePath: "/api", + refs: { + subDocPath: "dasd", + location: "foo.json", + }, + document: openApiDocument, + }) + ).rejects.toEqual( + new Error("options.subDocPath must be an Array of path segments or a valid JSON Pointer") + ); + }); + }); +}); diff --git a/packages/plugin-openapi/test/refs.spec.js b/packages/plugin-openapi/test/refs.spec.js index 84a7131fe..25ddb7d05 100644 --- a/packages/plugin-openapi/test/refs.spec.js +++ b/packages/plugin-openapi/test/refs.spec.js @@ -1,118 +1,133 @@ -import { startServer, fetchJson, fetchText, waitForServer } from "./support/helpers"; +import path from "path"; + +import { + startServer, + fetchJson, + fetchText, + waitForServer, + waitForServerUrl, +} from "./support/helpers"; + +import { openApiToRoutes } from "../src/index"; describe("when openapi has refs", () => { let server; - function testValidRefs(fixture) { - describe(`when fixture is ${fixture}`, () => { - beforeAll(async () => { - server = await startServer(fixture, { - log: "debug", - }); - await waitForServer(); + function testRouteDefinitions() { + describe("routes", () => { + it("should have created routes from openapi document defined in files", async () => { + expect(server.mock.routes.plain).toEqual([ + { + id: "get-users", + url: "/api/users", + method: "get", + delay: null, + variants: ["get-users:200-json-one-user", "get-users:200-json-two-users"], + }, + { + id: "post-users", + url: "/api/users", + method: "post", + delay: null, + variants: ["post-users:201-status", "post-users:400-text-error-message"], + }, + { + id: "get-users-id", + url: "/api/users/:id", + method: "get", + delay: null, + variants: ["get-users-id:200-json-success", "get-users-id:404-json-not-found"], + }, + ]); }); + }); + } - afterAll(async () => { - await server.stop(); + function testRoutes() { + describe("get-users route", () => { + it("should have 200-json-one-user variant available in base collection", async () => { + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + ]); + expect(response.status).toEqual(200); }); - describe("routes", () => { - it("should have created routes from openapi document defined in files", async () => { - expect(server.mock.routes.plain).toEqual([ - { - id: "get-users", - url: "/api/users", - method: "get", - delay: null, - variants: ["get-users:200-json-one-user", "get-users:200-json-two-users"], - }, - { - id: "post-users", - url: "/api/users", - method: "post", - delay: null, - variants: ["post-users:201-status", "post-users:400-text-error-message"], - }, - { - id: "get-users-id", - url: "/api/users/:id", - method: "get", - delay: null, - variants: ["get-users-id:200-json-success", "get-users-id:404-json-not-found"], - }, - ]); - }); + it("should have 200-json-two-users variant available in all-users collection", async () => { + await server.mock.collections.select("all-users", { check: true }); + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ]); + expect(response.status).toEqual(200); }); + }); - describe("get-users route", () => { - it("should have 200-json-one-user variant available in base collection", async () => { - const response = await fetchJson("/api/users"); - expect(response.body).toEqual([ - { - id: 1, - name: "John Doe", - }, - ]); - expect(response.status).toEqual(200); + describe("post-users route", () => { + it("should have 201-status variant available in all-users collection", async () => { + const response = await fetchJson("/api/users", { + method: "POST", }); + expect(response.body).toBe(undefined); + expect(response.status).toEqual(201); + }); - it("should have 200-json-two-users variant available in all-users collection", async () => { - await server.mock.collections.select("all-users", { check: true }); - const response = await fetchJson("/api/users"); - expect(response.body).toEqual([ - { - id: 1, - name: "John Doe", - }, - { - id: 2, - name: "Jane Doe", - }, - ]); - expect(response.status).toEqual(200); + it("should have 400-text-error-message variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchText("/api/users", { + method: "POST", }); + expect(response.body).toBe("Bad data"); + expect(response.status).toEqual(400); }); + }); - describe("post-users route", () => { - it("should have 201-status variant available in all-users collection", async () => { - const response = await fetchJson("/api/users", { - method: "POST", - }); - expect(response.body).toBe(undefined); - expect(response.status).toEqual(201); + describe("get-users-id route", () => { + it("should have 200-json-success variant available in base collection", async () => { + await server.mock.collections.select("base", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + id: 1, + name: "John Doe", }); + expect(response.status).toEqual(200); + }); - it("should have 400-text-error-message variant available in users-error collection", async () => { - await server.mock.collections.select("users-error", { check: true }); - const response = await fetchText("/api/users", { - method: "POST", - }); - expect(response.body).toBe("Bad data"); - expect(response.status).toEqual(400); + it("should have 200-json-two-users variant available in users-error collection", async () => { + await server.mock.collections.select("users-error", { check: true }); + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + code: 404, + message: "Not found", }); + expect(response.status).toEqual(404); }); + }); + } - describe("get-users-id route", () => { - it("should have 200-json-success variant available in base collection", async () => { - await server.mock.collections.select("base", { check: true }); - const response = await fetchJson("/api/users/2"); - expect(response.body).toEqual({ - id: 1, - name: "John Doe", - }); - expect(response.status).toEqual(200); - }); + function testValidRefs(fixture) { + describe(`when fixture is ${fixture}`, () => { + beforeAll(async () => { + server = await startServer(fixture); + await waitForServer(); + }); - it("should have 200-json-two-users variant available in users-error collection", async () => { - await server.mock.collections.select("users-error", { check: true }); - const response = await fetchJson("/api/users/2"); - expect(response.body).toEqual({ - code: 404, - message: "Not found", - }); - expect(response.status).toEqual(404); - }); + afterAll(async () => { + await server.stop(); }); + + testRouteDefinitions(); + testRoutes(); }); } @@ -123,7 +138,6 @@ describe("when openapi has refs", () => { let refsServer; beforeAll(async () => { refsServer = await startServer("refs-remote-server", { - log: "debug", server: { port: 3200, }, @@ -138,6 +152,73 @@ describe("when openapi has refs", () => { testValidRefs("refs-remote"); }); + describe("Remote full openapi ref using function", () => { + let refsServer; + beforeAll(async () => { + refsServer = await startServer("refs-remote-server", { + server: { + port: 3200, + }, + }); + await waitForServer(3200); + server = await startServer("no-paths", { + log: "debug", + }); + await waitForServer(); + const { loadRoutes } = server.mock.createLoaders(); + const routes = await openApiToRoutes({ + basePath: "/api", + document: { + $ref: "http://127.0.0.1:3200/openapi.json", + }, + }); + loadRoutes(routes); + await waitForServerUrl("/api/users"); + }); + + afterAll(async () => { + await server.stop(); + await refsServer.stop(); + }); + + testRoutes(); + }); + + describe("file openapi ref using function", () => { + let refsServer; + beforeAll(async () => { + refsServer = await startServer("refs-remote-server", { + server: { + port: 3200, + }, + }); + await waitForServer(3200); + server = await startServer("no-paths", { + log: "debug", + }); + await waitForServer(); + const { loadRoutes } = server.mock.createLoaders(); + const routes = await openApiToRoutes({ + basePath: "/api", + refs: { + location: path.resolve(__dirname, "refs.spec.js"), + }, + document: { + $ref: "./fixtures/refs-remote-server/static/openapi.json", + }, + }); + loadRoutes(routes); + await waitForServerUrl("/api/users"); + }); + + afterAll(async () => { + await server.stop(); + await refsServer.stop(); + }); + + testRoutes(); + }); + describe("when fixture has wrong refs", () => { beforeAll(async () => { server = await startServer("wrong-refs"); diff --git a/packages/plugin-openapi/test/routes-from-file.spec.js b/packages/plugin-openapi/test/routes.spec.js similarity index 86% rename from packages/plugin-openapi/test/routes-from-file.spec.js rename to packages/plugin-openapi/test/routes.spec.js index be7943710..1df33efee 100644 --- a/packages/plugin-openapi/test/routes-from-file.spec.js +++ b/packages/plugin-openapi/test/routes.spec.js @@ -1,18 +1,18 @@ -import { startServer, fetchJson, fetchText, waitForServer } from "./support/helpers"; +import { + startServer, + fetchJson, + fetchText, + waitForServer, + waitForServerUrl, +} from "./support/helpers"; +import openApiDocument from "./openapi/users"; -describe("routes generated from openapi file", () => { - let server; +import { openApiToRoutes } from "../src/index"; - describe("when fixture is api-users", () => { - beforeAll(async () => { - server = await startServer("api-users"); - await waitForServer(); - }); - - afterAll(async () => { - await server.stop(); - }); +describe("generated routes", () => { + let server; + function testRoutes() { describe("routes", () => { it("should have created routes from openapi document defined in files", async () => { expect(server.mock.routes.plain).toEqual([ @@ -110,6 +110,42 @@ describe("routes generated from openapi file", () => { expect(response.status).toEqual(404); }); }); + } + + describe("when fixture is api-users", () => { + beforeAll(async () => { + server = await startServer("api-users"); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + testRoutes(); + }); + + describe("when openapi definition is loaded using function", () => { + beforeAll(async () => { + server = await startServer("no-paths"); + await waitForServer(); + const { loadRoutes } = server.mock.createLoaders(); + + const routes = await openApiToRoutes({ + basePath: "/api", + document: openApiDocument, + }); + + loadRoutes(routes); + + await waitForServerUrl("/api/users"); + }); + + afterAll(async () => { + await server.stop(); + }); + + testRoutes(); }); describe("when openapi mock has no basePath", () => { From 77cc84b6199954075d4698ba2e7f7159b751f826 Mon Sep 17 00:00:00 2001 From: javierbrea Date: Tue, 23 Aug 2022 06:58:02 +0200 Subject: [PATCH 12/21] refactor(#384): Export types in namespaces --- packages/plugin-openapi/src/Plugin.ts | 4 +- packages/plugin-openapi/src/openapi.ts | 39 ++++++----- packages/plugin-openapi/src/types.ts | 64 ++++++++++++------- .../test/openapi-function-errors.spec.js | 8 +-- packages/plugin-openapi/test/refs.spec.js | 14 ++-- packages/plugin-openapi/test/routes.spec.js | 4 +- 6 files changed, 73 insertions(+), 60 deletions(-) diff --git a/packages/plugin-openapi/src/Plugin.ts b/packages/plugin-openapi/src/Plugin.ts index 35d79c7e3..b176173b6 100644 --- a/packages/plugin-openapi/src/Plugin.ts +++ b/packages/plugin-openapi/src/Plugin.ts @@ -1,6 +1,6 @@ import type { Routes, Core, MockLoaders, FilesContents } from "@mocks-server/core"; -import { openApisToRoutes } from "./openapi"; +import { openApisRoutes } from "./openapi"; const PLUGIN_ID = "openapi"; const DEFAULT_FOLDER = "openapi"; @@ -38,7 +38,7 @@ class Plugin { const fileContent = fileDetails.content; // TODO, validate file content this._logger.debug(`Creating routes from openApi definitions: '${JSON.stringify(fileContent)}'`); - return openApisToRoutes(fileContent, { + return openApisRoutes(fileContent, { defaultLocation: fileDetails.path, logger: this._logger, alerts: this._documentsAlerts diff --git a/packages/plugin-openapi/src/openapi.ts b/packages/plugin-openapi/src/openapi.ts index 03961cb80..f64415d92 100644 --- a/packages/plugin-openapi/src/openapi.ts +++ b/packages/plugin-openapi/src/openapi.ts @@ -1,16 +1,15 @@ -import { OpenAPIV3 as OpenApiV3Object } from "openapi-types"; +import { OpenAPIV3 as OpenAPIV3Object } from "openapi-types"; import { resolveRefs } from "json-refs"; import type { ResolvedRefsResults, UnresolvedRefDetails } from "json-refs"; -import type { OpenAPIV3 } from "openapi-types"; import type { Alerts, HTTPHeaders, Routes, RouteVariant, RouteVariants, RouteVariantTypes } from "@mocks-server/core"; -import type { OpenApiMockDocuments, OpenApiMockDocument, ResponseObjectWithVariantId, ExampleObjectWithVariantId, OperationObjectWithRouteId, ResponseHeaders, OpenApiToRoutesAdvancedOptions } from "./types"; +import type { OpenApiRoutes, OpenAPIV3 } from "./types"; import { MOCKS_SERVER_ROUTE_ID, MOCKS_SERVER_VARIANT_ID, VariantTypes, CONTENT_TYPE_HEADER } from "./constants"; -const methods = Object.values(OpenApiV3Object.HttpMethods); +const methods = Object.values(OpenAPIV3Object.HttpMethods); -export function notEmpty(value: TValue | null | undefined): value is TValue { +function notEmpty(value: TValue | null | undefined): value is TValue { return value !== null && value !== undefined; } @@ -83,7 +82,7 @@ function getStatusCode(code: string, codes: string[]): number { } // TODO, support also ReferenceObject in examples -function openApiResponseExampleToVariant(exampleId: string, code: number, variantType: RouteVariantTypes, mediaType: string, openApiResponseExample: ExampleObjectWithVariantId, openApiResponseHeaders?: ResponseHeaders): RouteVariant | null { +function openApiResponseExampleToVariant(exampleId: string, code: number, variantType: RouteVariantTypes, mediaType: string, openApiResponseExample: OpenAPIV3.ExampleObject, openApiResponseHeaders?: OpenAPIV3.ResponseHeaders): RouteVariant | null { if(!notEmpty(openApiResponseExample) || !notEmpty(openApiResponseExample.value)) { return null; } @@ -103,7 +102,7 @@ function openApiResponseExampleToVariant(exampleId: string, code: number, varian } as RouteVariant; } -function openApiResponseNoContentToVariant(code: number, openApiResponse: ResponseObjectWithVariantId): RouteVariant { +function openApiResponseNoContentToVariant(code: number, openApiResponse: OpenAPIV3.ResponseObject ): RouteVariant { const baseVariant = openApiResponseBaseVariant(VariantTypes.STATUS, code, { customId: openApiResponse[MOCKS_SERVER_VARIANT_ID] }); return { ...baseVariant, @@ -115,18 +114,18 @@ function openApiResponseNoContentToVariant(code: number, openApiResponse: Respon } as RouteVariant; } -function openApiResponseExamplesToVariants(code: number, variantType: RouteVariantTypes, mediaType: string, openApiResponseMediaType: OpenAPIV3.MediaTypeObject, openApiResponseHeaders?: ResponseHeaders): RouteVariants { +function openApiResponseExamplesToVariants(code: number, variantType: RouteVariantTypes, mediaType: string, openApiResponseMediaType: OpenAPIV3.MediaTypeObject, openApiResponseHeaders?: OpenAPIV3.ResponseHeaders): RouteVariants { const examples = openApiResponseMediaType.examples; if(!notEmpty(examples)) { return null; } return Object.keys(examples).map((exampleId: string) => { // TODO, support also ReferenceObject in examples - return openApiResponseExampleToVariant(exampleId, code, variantType, mediaType, examples[exampleId] as ExampleObjectWithVariantId, openApiResponseHeaders); + return openApiResponseExampleToVariant(exampleId, code, variantType, mediaType, examples[exampleId] as OpenAPIV3.ExampleObject , openApiResponseHeaders); }).filter(notEmpty); } -function openApiResponseMediaToVariants(code: number, mediaType: string, openApiResponseMediaType?: OpenAPIV3.MediaTypeObject, openApiResponseHeaders?: ResponseHeaders): RouteVariants { +function openApiResponseMediaToVariants(code: number, mediaType: string, openApiResponseMediaType?: OpenAPIV3.MediaTypeObject, openApiResponseHeaders?: OpenAPIV3.ResponseHeaders): RouteVariants { if(!notEmpty(openApiResponseMediaType)) { return null; } @@ -139,7 +138,7 @@ function openApiResponseMediaToVariants(code: number, mediaType: string, openApi return null; } -function openApiResponseCodeToVariants(code: number, openApiResponse?: ResponseObjectWithVariantId): RouteVariants { +function openApiResponseCodeToVariants(code: number, openApiResponse?: OpenAPIV3.ResponseObject): RouteVariants { if(!notEmpty(openApiResponse)) { return []; } @@ -159,12 +158,12 @@ function routeVariants(openApiResponses?: OpenAPIV3.ResponsesObject): RouteVaria const codes = Object.keys(openApiResponses); return codes.map((code: string) => { - const response = openApiResponses[code] as ResponseObjectWithVariantId; + const response = openApiResponses[code] as OpenAPIV3.ResponseObject; return openApiResponseCodeToVariants(getStatusCode(code, codes), response); }).flat().filter(notEmpty); } -function getCustomRouteId(openApiOperation: OperationObjectWithRouteId): string | undefined { +function getCustomRouteId(openApiOperation: OpenAPIV3.OperationObject): string | undefined { return openApiOperation[MOCKS_SERVER_ROUTE_ID] || openApiOperation.operationId; } @@ -174,7 +173,7 @@ function openApiPathToRoutes(path: string, basePath = "", openApiPathObject?: Op } return methods.map(method => { if(notEmpty(openApiPathObject[method])) { - const openApiOperation = openApiPathObject[method] as OperationObjectWithRouteId; + const openApiOperation = openApiPathObject[method] as OpenAPIV3.OperationObject; return { id: routeId(path, method, getCustomRouteId(openApiOperation)), url: routeUrl(path, basePath), @@ -185,7 +184,7 @@ function openApiPathToRoutes(path: string, basePath = "", openApiPathObject?: Op }).filter(notEmpty); } -function openApiDocumentToRoutes(openApiMockDocument: OpenApiMockDocument): Routes { +function openApiDocumentToRoutes(openApiMockDocument: OpenApiRoutes.Document): Routes { const openApiDocument = openApiMockDocument.document; const basePath = openApiMockDocument.basePath; @@ -211,7 +210,7 @@ function addOpenApiRefAlert(alerts: Alerts, error: Error): void { alerts.set(String(alerts.flat.length), "Error resolving openapi $ref", error); } -function resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions: OpenApiMockDocument["refs"], { alerts, logger }: OpenApiToRoutesAdvancedOptions): Promise { +function resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions: OpenApiRoutes.RefsOptions, { alerts, logger }: OpenApiRoutes.Options): Promise { return resolveRefs(document, refsOptions).then((res) => { if (logger) { logger.silly(`Document with resolved refs: '${JSON.stringify(res)}'`); @@ -236,7 +235,7 @@ function resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions: OpenApiM }); } -async function resolveOpenApiDocumentRefs(documentMock: OpenApiMockDocument, { defaultLocation, alerts, logger }: OpenApiToRoutesAdvancedOptions = {}): Promise { +async function resolveOpenApiDocumentRefs(documentMock: OpenApiRoutes.Document, { defaultLocation, alerts, logger }: OpenApiRoutes.Options = {}): Promise { const document = await resolveDocumentRefs(documentMock.document, {location: defaultLocation, ...documentMock.refs}, { alerts, logger }); if(document) { return { @@ -247,7 +246,7 @@ async function resolveOpenApiDocumentRefs(documentMock: OpenApiMockDocument, { d return null; } -export async function openApiToRoutes(openApiMockDocument: OpenApiMockDocument, advancedOptions?: OpenApiToRoutesAdvancedOptions): Promise { +export async function openApiRoutes(openApiMockDocument: OpenApiRoutes.Document, advancedOptions?: OpenApiRoutes.Options): Promise { const openApiDocument = await resolveOpenApiDocumentRefs(openApiMockDocument, advancedOptions); if(!openApiDocument) { return []; @@ -255,9 +254,9 @@ export async function openApiToRoutes(openApiMockDocument: OpenApiMockDocument, return openApiDocumentToRoutes(openApiDocument); } -export function openApisToRoutes(openApiMockDocuments: OpenApiMockDocuments, advancedOptions?: OpenApiToRoutesAdvancedOptions): Promise { +export function openApisRoutes(openApiMockDocuments: OpenApiRoutes.Document[], advancedOptions?: OpenApiRoutes.Options): Promise { return Promise.all(openApiMockDocuments.map((openApiMockDocument) => { - return openApiToRoutes(openApiMockDocument, advancedOptions); + return openApiRoutes(openApiMockDocument, advancedOptions); })).then((allRoutes) => { return allRoutes.flat(); }); diff --git a/packages/plugin-openapi/src/types.ts b/packages/plugin-openapi/src/types.ts index e79e9f9a2..df5f43e92 100644 --- a/packages/plugin-openapi/src/types.ts +++ b/packages/plugin-openapi/src/types.ts @@ -1,32 +1,50 @@ -import type { OpenAPIV3 } from "openapi-types"; +/* eslint-disable @typescript-eslint/no-empty-interface */ import { MOCKS_SERVER_ROUTE_ID, MOCKS_SERVER_VARIANT_ID } from "./constants"; -export interface RefsOptions { - location?: string, - subDocPath?: string, -} +import type { OpenAPIV3 as OriginalOpenApiV3 } from "openapi-types"; -export interface OpenApiMockDocument { - basePath: string, - refs: RefsOptions, - document: OpenAPIV3.Document -} +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace OpenAPIV3 { + export interface ResponseObject extends OriginalOpenApiV3.ResponseObject { [MOCKS_SERVER_VARIANT_ID]?: string } + export interface ExampleObject extends OriginalOpenApiV3.ExampleObject { [MOCKS_SERVER_VARIANT_ID]?: string } + + + export interface ResponsesObject extends OriginalOpenApiV3.ResponsesObject {} + export interface MediaTypeObject extends OriginalOpenApiV3.MediaTypeObject {} + export interface PathItemObject extends OriginalOpenApiV3.PathItemObject {} -export type OpenApiMockDocuments = OpenApiMockDocument[] + export type ResponseHeaders = ResponseObject["headers"] + + export interface OperationObject extends OriginalOpenApiV3.OperationObject { [MOCKS_SERVER_ROUTE_ID]?: string } + + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export type Document = OriginalOpenApiV3.Document<{ + [MOCKS_SERVER_VARIANT_ID]?: string + [MOCKS_SERVER_ROUTE_ID]?: string + }> +} -export type ResponseObjectWithVariantId = OpenAPIV3.ResponseObject & { [MOCKS_SERVER_VARIANT_ID]?: string } -export type ExampleObjectWithVariantId = OpenAPIV3.ExampleObject & { [MOCKS_SERVER_VARIANT_ID]?: string } -export type ResponseHeaders = OpenAPIV3.ResponseObject["headers"] +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace OpenApiRoutes { + export interface Options { + defaultLocation?: string, + // TODO, add alerts type when exported by core + // eslint-disable-next-line @typescript-eslint/no-explicit-any + alerts?: any, + // TODO, add alerts type when exported by core + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logger?: any + } -export type OperationObjectWithRouteId = OpenAPIV3.OperationObject<{[MOCKS_SERVER_ROUTE_ID]?: string}> + export interface RefsOptions { + location?: string, + subDocPath?: string, + } -export interface OpenApiToRoutesAdvancedOptions { - defaultLocation?: string, - // TODO, add alerts type when exported by core - // eslint-disable-next-line @typescript-eslint/no-explicit-any - alerts?: any, - // TODO, add alerts type when exported by core - // eslint-disable-next-line @typescript-eslint/no-explicit-any - logger?: any + export interface Document { + basePath: string, + refs?: RefsOptions, + document: OpenAPIV3.Document + } } diff --git a/packages/plugin-openapi/test/openapi-function-errors.spec.js b/packages/plugin-openapi/test/openapi-function-errors.spec.js index ccaef7fa3..c48679922 100644 --- a/packages/plugin-openapi/test/openapi-function-errors.spec.js +++ b/packages/plugin-openapi/test/openapi-function-errors.spec.js @@ -1,13 +1,13 @@ import deepMerge from "deepmerge"; import openApiDocument from "./openapi/users"; -import { openApiToRoutes } from "../src/index"; +import { openApiRoutes } from "../src/index"; describe("when function is used and openapi definition is wrong", () => { describe("when has a wrong ref", () => { it("should throw an error with message", async () => { await expect( - openApiToRoutes({ + openApiRoutes({ basePath: "/api", document: deepMerge(openApiDocument, { paths: { @@ -26,7 +26,7 @@ describe("when function is used and openapi definition is wrong", () => { describe("when has multiple wrong refs", () => { it("should throw an error with all messages", async () => { await expect( - openApiToRoutes({ + openApiRoutes({ basePath: "/api", document: deepMerge(openApiDocument, { paths: { @@ -56,7 +56,7 @@ describe("when function is used and openapi definition is wrong", () => { describe("when options are wrong", () => { it("should throw an error with message", async () => { await expect( - openApiToRoutes({ + openApiRoutes({ basePath: "/api", refs: { subDocPath: "dasd", diff --git a/packages/plugin-openapi/test/refs.spec.js b/packages/plugin-openapi/test/refs.spec.js index 25ddb7d05..4c90824c1 100644 --- a/packages/plugin-openapi/test/refs.spec.js +++ b/packages/plugin-openapi/test/refs.spec.js @@ -8,7 +8,7 @@ import { waitForServerUrl, } from "./support/helpers"; -import { openApiToRoutes } from "../src/index"; +import { openApiRoutes } from "../src/index"; describe("when openapi has refs", () => { let server; @@ -161,12 +161,10 @@ describe("when openapi has refs", () => { }, }); await waitForServer(3200); - server = await startServer("no-paths", { - log: "debug", - }); + server = await startServer("no-paths"); await waitForServer(); const { loadRoutes } = server.mock.createLoaders(); - const routes = await openApiToRoutes({ + const routes = await openApiRoutes({ basePath: "/api", document: { $ref: "http://127.0.0.1:3200/openapi.json", @@ -193,12 +191,10 @@ describe("when openapi has refs", () => { }, }); await waitForServer(3200); - server = await startServer("no-paths", { - log: "debug", - }); + server = await startServer("no-paths"); await waitForServer(); const { loadRoutes } = server.mock.createLoaders(); - const routes = await openApiToRoutes({ + const routes = await openApiRoutes({ basePath: "/api", refs: { location: path.resolve(__dirname, "refs.spec.js"), diff --git a/packages/plugin-openapi/test/routes.spec.js b/packages/plugin-openapi/test/routes.spec.js index 1df33efee..ca845e00c 100644 --- a/packages/plugin-openapi/test/routes.spec.js +++ b/packages/plugin-openapi/test/routes.spec.js @@ -7,7 +7,7 @@ import { } from "./support/helpers"; import openApiDocument from "./openapi/users"; -import { openApiToRoutes } from "../src/index"; +import { openApiRoutes } from "../src/index"; describe("generated routes", () => { let server; @@ -131,7 +131,7 @@ describe("generated routes", () => { await waitForServer(); const { loadRoutes } = server.mock.createLoaders(); - const routes = await openApiToRoutes({ + const routes = await openApiRoutes({ basePath: "/api", document: openApiDocument, }); From 774b079bb1e3ed8d5e7daef845947fdd29c737b6 Mon Sep 17 00:00:00 2001 From: javierbrea Date: Tue, 23 Aug 2022 07:48:15 +0200 Subject: [PATCH 13/21] feat: Support async in files --- packages/core/CHANGELOG.md | 6 ++ packages/core/src/files/FilesLoaders.js | 28 +++++- packages/core/test/files/FilesLoaders.spec.js | 80 ++++++++++++++++ test/core-e2e/src/async-files.spec.js | 94 +++++++++++++++++++ .../web-tutorial-async/collections.js | 26 +++++ .../fixtures/web-tutorial-async/db/users.js | 32 +++++++ .../web-tutorial-async/routes/user.js | 60 ++++++++++++ .../web-tutorial-async/routes/users.js | 45 +++++++++ 8 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 test/core-e2e/src/async-files.spec.js create mode 100644 test/core-e2e/src/fixtures/web-tutorial-async/collections.js create mode 100644 test/core-e2e/src/fixtures/web-tutorial-async/db/users.js create mode 100644 test/core-e2e/src/fixtures/web-tutorial-async/routes/user.js create mode 100644 test/core-e2e/src/fixtures/web-tutorial-async/routes/users.js diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 95e71c266..e7fe98e1b 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed ### Removed +## [unreleased] + +### Added +- feat: Support asynchronies in files. Files now can export a function. In that case, the loader will receive the result of the function. If function returns a promise, it will receive the result of the promise once it is resolved (rejected promises are treated as file load errors). + + ## [3.10.0] - 2022-08-11 ### Added diff --git a/packages/core/src/files/FilesLoaders.js b/packages/core/src/files/FilesLoaders.js index 5d06caeac..45b6ced7a 100644 --- a/packages/core/src/files/FilesLoaders.js +++ b/packages/core/src/files/FilesLoaders.js @@ -13,7 +13,7 @@ const path = require("path"); const globule = require("globule"); const watch = require("node-watch"); const fsExtra = require("fs-extra"); -const { map, debounce } = require("lodash"); +const { map, debounce, isFunction } = require("lodash"); const isPromise = require("is-promise"); const CollectionsLoader = require("./loaders/Collections"); @@ -144,7 +144,31 @@ class FilesLoaders { return new Promise((resolve, reject) => { try { const content = this._require(filePath); - resolve((content && content.default) || content); + const exportedContent = (content && content.default) || content; + if (isFunction(exportedContent)) { + this._logger.debug( + `Function exported by '${filePath}'. Executing it to return its result` + ); + const exportedContentResult = exportedContent(); + if (isPromise(exportedContentResult)) { + this._logger.debug( + `Function in '${filePath}' returned a promise. Waiting for it to resolve its result` + ); + exportedContentResult + .then((exportedContentPromiseResult) => { + this._logger.silly(`Promise in '${filePath}' was resolved`); + resolve(exportedContentPromiseResult); + }) + .catch((error) => { + this._logger.silly(`Promise in '${filePath}' was rejected`); + reject(error); + }); + } else { + resolve(exportedContentResult); + } + } else { + resolve(exportedContent); + } } catch (error) { reject(error); } diff --git a/packages/core/test/files/FilesLoaders.spec.js b/packages/core/test/files/FilesLoaders.spec.js index ad84389f1..7681e3541 100644 --- a/packages/core/test/files/FilesLoaders.spec.js +++ b/packages/core/test/files/FilesLoaders.spec.js @@ -488,6 +488,86 @@ describe("FilesLoaders", () => { ]); }); + it("should call to its load function passing the result when file exports a function", async () => { + libsMocks.stubs.globule.find.returns(["foo/foo-file.js"]); + const spy = sandbox.spy(); + filesLoader = new FilesLoaders(pluginMethods, { + requireCache, + require: () => () => ["foo-content"], + }); + filesLoader._pathOption = pathOption; + filesLoader._watchOption = watchOption; + filesLoader._babelRegisterOption = babelRegisterOption; + filesLoader._babelRegisterOptionsOption = babelRegisterOptionsOption; + filesLoader.createLoader({ + id: "foo", + src: "foo/foo-path/**/*", + onLoad: spy, + }); + await filesLoader.init(); + expect(spy.getCall(0).args[0]).toEqual([ + { path: "foo/foo-file.js", content: ["foo-content"] }, + ]); + }); + + it("should call to its load function passing the promise resolved result when file exports a function returning a promise", async () => { + libsMocks.stubs.globule.find.returns(["foo/foo-file.js"]); + const exportedFunction = () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(["foo-promise-content"]); + }, 200); + }); + }; + const spy = sandbox.spy(); + filesLoader = new FilesLoaders(pluginMethods, { + requireCache, + require: () => exportedFunction, + }); + filesLoader._pathOption = pathOption; + filesLoader._watchOption = watchOption; + filesLoader._babelRegisterOption = babelRegisterOption; + filesLoader._babelRegisterOptionsOption = babelRegisterOptionsOption; + filesLoader.createLoader({ + id: "foo", + src: "foo/foo-path/**/*", + onLoad: spy, + }); + await filesLoader.init(); + expect(spy.getCall(0).args[0]).toEqual([ + { path: "foo/foo-file.js", content: ["foo-promise-content"] }, + ]); + }); + + it("should catch the error and pass it as an errored file when the file exports a promise that is rejected", async () => { + const promiseError = new Error("Foo error"); + libsMocks.stubs.globule.find.returns(["foo/foo-file.js"]); + const exportedFunction = () => { + return new Promise((_resolve, reject) => { + setTimeout(() => { + reject(promiseError); + }, 200); + }); + }; + const spy = sandbox.spy(); + filesLoader = new FilesLoaders(pluginMethods, { + requireCache, + require: () => exportedFunction, + }); + filesLoader._pathOption = pathOption; + filesLoader._watchOption = watchOption; + filesLoader._babelRegisterOption = babelRegisterOption; + filesLoader._babelRegisterOptionsOption = babelRegisterOptionsOption; + filesLoader.createLoader({ + id: "foo", + src: "foo/foo-path/**/*", + onLoad: spy, + }); + await filesLoader.init(); + expect(spy.getCall(0).args[0]).toEqual([]); + expect(spy.getCall(0).args[1]).toEqual([{ path: "foo/foo-file.js", error: promiseError }]); + }); + it("should support async onLoad functions", async () => { libsMocks.stubs.globule.find.returns(["foo/foo-path/**"]); const spy = sandbox.spy(); diff --git a/test/core-e2e/src/async-files.spec.js b/test/core-e2e/src/async-files.spec.js new file mode 100644 index 000000000..67d095849 --- /dev/null +++ b/test/core-e2e/src/async-files.spec.js @@ -0,0 +1,94 @@ +/* +Copyright 2021 Javier Brea + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +*/ + +const { + startCore, + doFetch, + waitForServer, + findAlert, + removeConfigFile, +} = require("./support/helpers"); + +describe("async files", () => { + let core, changeCollection; + + beforeAll(async () => { + core = await startCore("web-tutorial-async"); + await waitForServer(); + changeCollection = (name) => { + core.config.namespace("mock").namespace("collections").option("selected").value = name; + }; + }); + + afterAll(async () => { + removeConfigFile(); + await core.stop(); + }); + + describe("collection by default", () => { + it("should have added an alert about collection was not defined", () => { + expect(findAlert("mock:collections:selected", core.alerts).message).toEqual( + expect.stringContaining("Option 'mock.collections.selected' was not defined") + ); + }); + + it("should serve users under the /api/users path", async () => { + const users = await doFetch("/api/users"); + expect(users.status).toEqual(200); + expect(users.body).toEqual([ + { id: 1, name: "John Doe" }, + { id: 2, name: "Jane Doe" }, + ]); + }); + + it("should serve user 1 under the /api/users/1 path", async () => { + const users = await doFetch("/api/users/1"); + expect(users.status).toEqual(200); + expect(users.body).toEqual({ id: 1, name: "John Doe" }); + }); + + it("should serve user 1 under the /api/users/2 path", async () => { + const users = await doFetch("/api/users/2"); + expect(users.status).toEqual(200); + expect(users.body).toEqual({ id: 1, name: "John Doe" }); + }); + }); + + describe('when changing collection to "user-2"', () => { + beforeAll(() => { + changeCollection("user-2"); + }); + + it("should have removed alert", () => { + expect(findAlert("mock:collections:selected", core.alerts)).toEqual(undefined); + }); + + it("should serve users collection under the /api/users path", async () => { + const users = await doFetch("/api/users"); + expect(users.status).toEqual(200); + expect(users.body).toEqual([ + { id: 1, name: "John Doe" }, + { id: 2, name: "Jane Doe" }, + ]); + }); + + it("should serve user 2 under the /api/users/1 path", async () => { + const users = await doFetch("/api/users/1"); + expect(users.status).toEqual(200); + expect(users.body).toEqual({ id: 2, name: "Jane Doe" }); + }); + + it("should serve user 2 under the /api/users/2 path", async () => { + const users = await doFetch("/api/users/2"); + expect(users.status).toEqual(200); + expect(users.body).toEqual({ id: 2, name: "Jane Doe" }); + }); + }); +}); diff --git a/test/core-e2e/src/fixtures/web-tutorial-async/collections.js b/test/core-e2e/src/fixtures/web-tutorial-async/collections.js new file mode 100644 index 000000000..8af8046c4 --- /dev/null +++ b/test/core-e2e/src/fixtures/web-tutorial-async/collections.js @@ -0,0 +1,26 @@ +/* +Copyright 2021 Javier Brea + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +*/ + +module.exports = [ + { + id: "base", + routes: ["get-users:success", "get-user:1"], + }, + { + id: "user-2", + from: "base", + routes: ["get-user:2"], + }, + { + id: "user-real", + from: "base", + routes: ["get-user:real"], + }, +]; diff --git a/test/core-e2e/src/fixtures/web-tutorial-async/db/users.js b/test/core-e2e/src/fixtures/web-tutorial-async/db/users.js new file mode 100644 index 000000000..32a79271a --- /dev/null +++ b/test/core-e2e/src/fixtures/web-tutorial-async/db/users.js @@ -0,0 +1,32 @@ +/* +Copyright 2021 Javier Brea + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +*/ + +const USERS = [ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, +]; + +function getUsers() { + return new Promise((resolve) => { + setTimeout(() => { + resolve(USERS); + }, 500); + }); +} + +module.exports = { + getUsers, +}; diff --git a/test/core-e2e/src/fixtures/web-tutorial-async/routes/user.js b/test/core-e2e/src/fixtures/web-tutorial-async/routes/user.js new file mode 100644 index 000000000..fe7e76843 --- /dev/null +++ b/test/core-e2e/src/fixtures/web-tutorial-async/routes/user.js @@ -0,0 +1,60 @@ +/* +Copyright 2021 Javier Brea + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +*/ + +const { getUsers } = require("../db/users"); + +module.exports = async () => { + const users = await getUsers(); + return [ + { + id: "get-user", + url: "/api/users/:id", + method: "get", + variants: [ + { + id: "1", + type: "json", + options: { + status: 200, + body: users[0], + }, + }, + { + id: "2", + type: "json", + options: { + status: 200, + body: users[1], + }, + }, + { + id: "real", + type: "middleware", + options: { + middleware: (req, res) => { + const userId = req.params.id; + const user = users.find((userData) => userData.id === Number(userId)); + if (user) { + res.status(200); + res.send(user); + } else { + res.status(404); + res.send({ + message: "User not found", + }); + } + } + }, + }, + ], + }, + ]; +} + diff --git a/test/core-e2e/src/fixtures/web-tutorial-async/routes/users.js b/test/core-e2e/src/fixtures/web-tutorial-async/routes/users.js new file mode 100644 index 000000000..4537a679f --- /dev/null +++ b/test/core-e2e/src/fixtures/web-tutorial-async/routes/users.js @@ -0,0 +1,45 @@ +/* +Copyright 2021 Javier Brea + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +*/ + +const { getUsers } = require("../db/users"); + +function getUsersRoutes() { + return getUsers().then((users) => { + return [ + { + id: "get-users", + url: "/api/users", + method: "GET", + variants: [ + { + id: "success", + type: "json", + options: { + status: 200, + body: users, + }, + }, + { + id: "error", + type: "json", + options: { + status: 403, + body: { + message: "Bad data", + }, + }, + }, + ], + }, + ]; + }); +} + +module.exports = getUsersRoutes; From c205517ad7d350fdb292451f80ba7f313e8c88aa Mon Sep 17 00:00:00 2001 From: javierbrea Date: Wed, 24 Aug 2022 12:51:44 +0200 Subject: [PATCH 14/21] feat(#384): Create collection with first variant of each route created from OpenAPI definition --- packages/core/CHANGELOG.md | 3 + packages/core/jest.config.js | 2 +- packages/core/src/mock/validations.js | 2 +- packages/core/test/mock/validations.spec.js | 6 + packages/plugin-openapi/jest.config.js | 2 +- packages/plugin-openapi/src/Plugin.ts | 92 ++++++++++++--- .../plugin-openapi/src/mocks-server-core.d.ts | 30 ++++- packages/plugin-openapi/src/openapi.ts | 18 +-- packages/plugin-openapi/src/types.ts | 14 ++- .../plugin-openapi/test/collections.spec.js | 108 ++++++++++++++++++ .../collections.js | 1 + .../openapi/api.js | 29 +++++ .../api-users-collection/collections.js | 1 + .../api-users-collection/openapi/api.js | 11 ++ 14 files changed, 281 insertions(+), 38 deletions(-) create mode 100644 packages/plugin-openapi/test/collections.spec.js create mode 100644 packages/plugin-openapi/test/fixtures/api-users-collection-no-variants/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/api-users-collection-no-variants/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/api-users-collection/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/api-users-collection/openapi/api.js diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index e7fe98e1b..ccade5a1a 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - feat: Support asynchronies in files. Files now can export a function. In that case, the loader will receive the result of the function. If function returns a promise, it will receive the result of the promise once it is resolved (rejected promises are treated as file load errors). +### Fixed +- fix: Collections and routes validation was throwing when undefined was passed as value + ## [3.10.0] - 2022-08-11 diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js index a9fbbd83f..4022d65d1 100644 --- a/packages/core/jest.config.js +++ b/packages/core/jest.config.js @@ -26,7 +26,7 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: ["/test/**/*.spec.js"], - // testMatch: ["/test/**/Server.spec.js"], + // testMatch: ["/test/**/validations.spec.js"], // The test environment that will be used for testing testEnvironment: "node", diff --git a/packages/core/src/mock/validations.js b/packages/core/src/mock/validations.js index 87face35d..d8ef8a6e2 100644 --- a/packages/core/src/mock/validations.js +++ b/packages/core/src/mock/validations.js @@ -245,7 +245,7 @@ function customValidationSingleMessage(errors) { .join(". "); } -function validationSingleMessage(schema, data, errors) { +function validationSingleMessage(schema, data = {}, errors) { const formattedJson = betterAjvErrors(schema, data, errors, { format: "js", }); diff --git a/packages/core/test/mock/validations.spec.js b/packages/core/test/mock/validations.spec.js index 98087e295..b82fbe5c2 100644 --- a/packages/core/test/mock/validations.spec.js +++ b/packages/core/test/mock/validations.spec.js @@ -430,6 +430,12 @@ describe("mocks validations", () => { ).toEqual(null); }); + it("should return error if collection is undefined", () => { + const errors = collectionValidationErrors(); + expect(errors.message).toEqual(expect.stringContaining("type must be object")); + expect(errors.errors.length).toEqual(4); + }); + it("should return error if mock has not id", () => { const errors = collectionValidationErrors({ routes: [], diff --git a/packages/plugin-openapi/jest.config.js b/packages/plugin-openapi/jest.config.js index 767ad15a2..338ca8694 100644 --- a/packages/plugin-openapi/jest.config.js +++ b/packages/plugin-openapi/jest.config.js @@ -26,7 +26,7 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: ["/test/**/*.spec.js"], - // testMatch: ["/test/**/refs.spec.js"], + // testMatch: ["/test/**/collections.spec.js"], // The test environment that will be used for testing testEnvironment: "node", diff --git a/packages/plugin-openapi/src/Plugin.ts b/packages/plugin-openapi/src/Plugin.ts index b176173b6..7514942b4 100644 --- a/packages/plugin-openapi/src/Plugin.ts +++ b/packages/plugin-openapi/src/Plugin.ts @@ -1,30 +1,73 @@ -import type { Routes, Core, MockLoaders, FilesContents } from "@mocks-server/core"; +import type { Route, Routes, Collections, Collection, Core, MockLoaders, FilesContents, ConfigOption } from "@mocks-server/core"; -import { openApisRoutes } from "./openapi"; +import { openApiRoutes } from "./openapi"; +import type { OpenApiDefinition } from "./types"; const PLUGIN_ID = "openapi"; const DEFAULT_FOLDER = "openapi"; +const COLLECTION_NAMESPACE = "collection"; + +const COLLECTION_OPTIONS = [ + { + description: "Name for the collection created from OpenAPI definitions", + name: "id", + type: "string", + default: "openapi", + }, + { + description: "Name of the collection to extend from", + name: "from", + type: "string" + }, +]; + +interface RoutesAndCollections { + routes: Routes, + collections: Collections, +} + +function getRoutesCollection(routes: Routes, collectionOptions?: OpenApiDefinition.Collection): Collection | null { + if (!collectionOptions) { + return null; + } + return routes.reduce((collection, route: Route) => { + if (route.variants && route.variants.length) { + collection.routes.push(`${route.id}:${route.variants[0].id}`) + } + return collection; + }, { id: collectionOptions.id, from: collectionOptions.from, routes: [] } as Collection); +} + class Plugin { static get id() { return PLUGIN_ID; } + private _config: Core["config"] private _logger: Core["logger"] private _alerts: Core["alerts"] private _files: Core["files"] private _loadRoutes: MockLoaders["loadRoutes"] + private _loadCollections: MockLoaders["loadCollections"] private _documentsAlerts: Core["alerts"] + private _collectionNameOption: ConfigOption + private _collectionFromOption: ConfigOption - constructor({ logger, alerts, mock, files }: Core) { + constructor({ logger, alerts, mock, files, config }: Core) { + this._config = config; this._logger = logger; this._alerts = alerts; this._files = files; + const configCollection = this._config.addNamespace(COLLECTION_NAMESPACE); + [this._collectionNameOption, this._collectionFromOption] = configCollection.addOptions(COLLECTION_OPTIONS); + this._documentsAlerts = this._alerts.collection("documents"); - const { loadRoutes } = mock.createLoaders(); + const { loadRoutes, loadCollections } = mock.createLoaders(); this._loadRoutes = loadRoutes; + this._loadCollections = loadCollections; this._files.createLoader({ id: PLUGIN_ID, src: `${DEFAULT_FOLDER}/**/*`, @@ -32,28 +75,45 @@ class Plugin { }) } - async _getRoutesFromFilesContents(filesContents: FilesContents): Promise { - const openApiMockDocuments = await Promise.all( + async _getRoutesAndCollectionsFromFilesContents(filesContents: FilesContents): Promise { + const openApiRoutesAndCollections = await Promise.all( filesContents.map((fileDetails) => { const fileContent = fileDetails.content; - // TODO, validate file content - this._logger.debug(`Creating routes from openApi definitions: '${JSON.stringify(fileContent)}'`); - return openApisRoutes(fileContent, { - defaultLocation: fileDetails.path, - logger: this._logger, - alerts: this._documentsAlerts + return fileContent.map((openAPIDefinition: OpenApiDefinition.Definition) => { + this._logger.debug(`Creating routes from openApi definition: '${JSON.stringify(openAPIDefinition)}'`); + return openApiRoutes(openAPIDefinition, { + defaultLocation: fileDetails.path, + logger: this._logger, + alerts: this._documentsAlerts + }).then((routes) => { + return { + routes, + collection: getRoutesCollection(routes, openAPIDefinition.collection) + } + }); }); - }) + }).flat() ); - return openApiMockDocuments.flat(); + + return openApiRoutesAndCollections.reduce((allRoutesAndCollections, definitionRoutesAndCollections) => { + allRoutesAndCollections.routes = allRoutesAndCollections.routes.concat(definitionRoutesAndCollections.routes); + if(definitionRoutesAndCollections.collection) { + allRoutesAndCollections.collections = allRoutesAndCollections.collections.concat(definitionRoutesAndCollections.collection); + } + return allRoutesAndCollections; + }, { routes: [], collections: []}); } async _onLoadFiles(filesContents: FilesContents) { this._documentsAlerts.clean(); - const routes = await this._getRoutesFromFilesContents(filesContents); + const { routes, collections } = await this._getRoutesAndCollectionsFromFilesContents(filesContents); + const folderTrace = `from OpenAPI definitions found in folder '${this._files.path}/${DEFAULT_FOLDER}'`; this._logger.debug(`Routes to load from openApi definitions: '${JSON.stringify(routes)}'`); - this._logger.verbose(`Loading ${routes.length} routes from openApi definitions found in folder '${this._files.path}/${DEFAULT_FOLDER}'`); + this._logger.verbose(`Loading ${routes.length} routes ${folderTrace}`); this._loadRoutes(routes); + this._logger.debug(`Collections to load from OpenAPI definitions: '${JSON.stringify(collections)}'`); + this._logger.verbose(`Loading ${collections.length} collections ${folderTrace}`); + this._loadCollections(collections); } } diff --git a/packages/plugin-openapi/src/mocks-server-core.d.ts b/packages/plugin-openapi/src/mocks-server-core.d.ts index ac439ef31..355ac014c 100644 --- a/packages/plugin-openapi/src/mocks-server-core.d.ts +++ b/packages/plugin-openapi/src/mocks-server-core.d.ts @@ -79,10 +79,35 @@ declare module "@mocks-server/core" { variants: RouteVariants, } + interface Collection { + id: string, + from: string, + routes: string[], + } + + interface OptionProperties { + description: string, + name: string, + type: string, + default?: unknown, + } + + interface ConfigOption { + addNamespace(): Config + addOptions(): Config + } + + interface Config { + addNamespace(name: string): Config + addOptions(options: OptionProperties[]): ConfigOption[] + } + type Routes = Route[] + type Collections = Collection[] interface MockLoaders { - loadRoutes(routes: Routes): void + loadRoutes(routes: Routes): void, + loadCollections(collections: Collections): void } interface Mock { @@ -98,6 +123,7 @@ declare module "@mocks-server/core" { logger: Logger alerts: Alerts files: Files - mock: Mock + mock: Mock, + config: Config, } } diff --git a/packages/plugin-openapi/src/openapi.ts b/packages/plugin-openapi/src/openapi.ts index f64415d92..b788c3f91 100644 --- a/packages/plugin-openapi/src/openapi.ts +++ b/packages/plugin-openapi/src/openapi.ts @@ -3,7 +3,7 @@ import { resolveRefs } from "json-refs"; import type { ResolvedRefsResults, UnresolvedRefDetails } from "json-refs"; import type { Alerts, HTTPHeaders, Routes, RouteVariant, RouteVariants, RouteVariantTypes } from "@mocks-server/core"; -import type { OpenApiRoutes, OpenAPIV3 } from "./types"; +import type { OpenApiDefinition, OpenAPIV3 } from "./types"; import { MOCKS_SERVER_ROUTE_ID, MOCKS_SERVER_VARIANT_ID, VariantTypes, CONTENT_TYPE_HEADER } from "./constants"; @@ -184,7 +184,7 @@ function openApiPathToRoutes(path: string, basePath = "", openApiPathObject?: Op }).filter(notEmpty); } -function openApiDocumentToRoutes(openApiMockDocument: OpenApiRoutes.Document): Routes { +function openApiDocumentToRoutes(openApiMockDocument: OpenApiDefinition.Definition): Routes { const openApiDocument = openApiMockDocument.document; const basePath = openApiMockDocument.basePath; @@ -210,7 +210,7 @@ function addOpenApiRefAlert(alerts: Alerts, error: Error): void { alerts.set(String(alerts.flat.length), "Error resolving openapi $ref", error); } -function resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions: OpenApiRoutes.RefsOptions, { alerts, logger }: OpenApiRoutes.Options): Promise { +function resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions: OpenApiDefinition.Refs, { alerts, logger }: OpenApiDefinition.Options): Promise { return resolveRefs(document, refsOptions).then((res) => { if (logger) { logger.silly(`Document with resolved refs: '${JSON.stringify(res)}'`); @@ -235,7 +235,7 @@ function resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions: OpenApiR }); } -async function resolveOpenApiDocumentRefs(documentMock: OpenApiRoutes.Document, { defaultLocation, alerts, logger }: OpenApiRoutes.Options = {}): Promise { +async function resolveOpenApiDocumentRefs(documentMock: OpenApiDefinition.Definition, { defaultLocation, alerts, logger }: OpenApiDefinition.Options = {}): Promise { const document = await resolveDocumentRefs(documentMock.document, {location: defaultLocation, ...documentMock.refs}, { alerts, logger }); if(document) { return { @@ -246,18 +246,10 @@ async function resolveOpenApiDocumentRefs(documentMock: OpenApiRoutes.Document, return null; } -export async function openApiRoutes(openApiMockDocument: OpenApiRoutes.Document, advancedOptions?: OpenApiRoutes.Options): Promise { +export async function openApiRoutes(openApiMockDocument: OpenApiDefinition.Definition, advancedOptions?: OpenApiDefinition.Options): Promise { const openApiDocument = await resolveOpenApiDocumentRefs(openApiMockDocument, advancedOptions); if(!openApiDocument) { return []; } return openApiDocumentToRoutes(openApiDocument); } - -export function openApisRoutes(openApiMockDocuments: OpenApiRoutes.Document[], advancedOptions?: OpenApiRoutes.Options): Promise { - return Promise.all(openApiMockDocuments.map((openApiMockDocument) => { - return openApiRoutes(openApiMockDocument, advancedOptions); - })).then((allRoutes) => { - return allRoutes.flat(); - }); -} diff --git a/packages/plugin-openapi/src/types.ts b/packages/plugin-openapi/src/types.ts index df5f43e92..42a1b35fa 100644 --- a/packages/plugin-openapi/src/types.ts +++ b/packages/plugin-openapi/src/types.ts @@ -26,7 +26,7 @@ export namespace OpenAPIV3 { } // eslint-disable-next-line @typescript-eslint/no-namespace -export namespace OpenApiRoutes { +export namespace OpenApiDefinition { export interface Options { defaultLocation?: string, // TODO, add alerts type when exported by core @@ -37,14 +37,20 @@ export namespace OpenApiRoutes { logger?: any } - export interface RefsOptions { + export interface Collection { + id: string, + from: string, + } + + export interface Refs { location?: string, subDocPath?: string, } - export interface Document { + export interface Definition { basePath: string, - refs?: RefsOptions, + refs?: Refs, + collection?: Collection, document: OpenAPIV3.Document } } diff --git a/packages/plugin-openapi/test/collections.spec.js b/packages/plugin-openapi/test/collections.spec.js new file mode 100644 index 000000000..966d266ca --- /dev/null +++ b/packages/plugin-openapi/test/collections.spec.js @@ -0,0 +1,108 @@ +import { startServer, fetchJson, waitForServer } from "./support/helpers"; + +describe("generated collections", () => { + let server; + + describe("when collection id is provided in definition", () => { + beforeAll(async () => { + server = await startServer("api-users-collection", { + mock: { + collections: { + selected: "users", + }, + }, + }); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + describe("routes", () => { + it("should have created routes from openapi definition", async () => { + expect(server.mock.collections.plain).toEqual([ + { + id: "users", + from: null, + definedRoutes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + routes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + }, + ]); + }); + }); + + describe("get-users route", () => { + it("should have 200-json-one-user variant available in users collection", async () => { + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ + { + id: 1, + name: "John Doe", + }, + ]); + expect(response.status).toEqual(200); + }); + }); + + describe("post-users route", () => { + it("should have 201-status variant available in users collection", async () => { + const response = await fetchJson("/api/users", { + method: "POST", + }); + expect(response.body).toBe(undefined); + expect(response.status).toEqual(201); + }); + }); + + describe("get-users-id route", () => { + it("should have 200-json-success variant available in users collection", async () => { + const response = await fetchJson("/api/users/2"); + expect(response.body).toEqual({ + id: 1, + name: "John Doe", + }); + expect(response.status).toEqual(200); + }); + }); + }); + + describe("when route has no variants", () => { + beforeAll(async () => { + server = await startServer("api-users-collection-no-variants", { + log: "debug", + mock: { + collections: { + selected: "users", + }, + }, + }); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + describe("routes", () => { + it("should have omitted routes without any variant", async () => { + expect(server.mock.collections.plain).toEqual([ + { + id: "users", + from: null, + definedRoutes: ["post-users:201-status", "get-users-id:200-json-success"], + routes: ["post-users:201-status", "get-users-id:200-json-success"], + }, + ]); + }); + }); + }); +}); diff --git a/packages/plugin-openapi/test/fixtures/api-users-collection-no-variants/collections.js b/packages/plugin-openapi/test/fixtures/api-users-collection-no-variants/collections.js new file mode 100644 index 000000000..e0a30c5df --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/api-users-collection-no-variants/collections.js @@ -0,0 +1 @@ +module.exports = []; diff --git a/packages/plugin-openapi/test/fixtures/api-users-collection-no-variants/openapi/api.js b/packages/plugin-openapi/test/fixtures/api-users-collection-no-variants/openapi/api.js new file mode 100644 index 000000000..d85ce3307 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/api-users-collection-no-variants/openapi/api.js @@ -0,0 +1,29 @@ +const deepMerge = require("deepmerge"); + +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + basePath: "/api", + collection: { + id: "users", + }, + document: deepMerge(openApiDocument, { + paths: { + "/users": { + get: { + responses: { + "200": { + content: { + "application/json": { + examples: undefined, + }, + }, + }, + }, + }, + }, + }, + }), + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/api-users-collection/collections.js b/packages/plugin-openapi/test/fixtures/api-users-collection/collections.js new file mode 100644 index 000000000..e0a30c5df --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/api-users-collection/collections.js @@ -0,0 +1 @@ +module.exports = []; diff --git a/packages/plugin-openapi/test/fixtures/api-users-collection/openapi/api.js b/packages/plugin-openapi/test/fixtures/api-users-collection/openapi/api.js new file mode 100644 index 000000000..24b90e20c --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/api-users-collection/openapi/api.js @@ -0,0 +1,11 @@ +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + basePath: "/api", + collection: { + id: "users", + }, + document: openApiDocument, + }, +]; From 76271b716fe207e3a66e1458c15969b6e5cf4a8f Mon Sep 17 00:00:00 2001 From: javierbrea Date: Wed, 24 Aug 2022 15:02:26 +0200 Subject: [PATCH 15/21] feat(#384): Create default collection with routes from all OpenAPI definitions --- packages/core/CHANGELOG.md | 2 +- packages/core/src/mock/validations.js | 6 +-- packages/plugin-openapi/src/Plugin.ts | 41 ++++++++++++++++--- .../plugin-openapi/src/mocks-server-core.d.ts | 3 +- packages/plugin-openapi/src/openapi.ts | 20 ++++----- packages/plugin-openapi/src/types.ts | 2 +- .../plugin-openapi/test/collections.spec.js | 20 +++++++++ 7 files changed, 71 insertions(+), 23 deletions(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index ccade5a1a..c8006ea4c 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -14,11 +14,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - feat: Support asynchronies in files. Files now can export a function. In that case, the loader will receive the result of the function. If function returns a promise, it will receive the result of the promise once it is resolved (rejected promises are treated as file load errors). +- Added: Support null value in "from" property in collections ### Fixed - fix: Collections and routes validation was throwing when undefined was passed as value - ## [3.10.0] - 2022-08-11 ### Added diff --git a/packages/core/src/mock/validations.js b/packages/core/src/mock/validations.js index d8ef8a6e2..0badca761 100644 --- a/packages/core/src/mock/validations.js +++ b/packages/core/src/mock/validations.js @@ -54,7 +54,7 @@ const collectionsSchema = { type: "string", }, from: { - type: "string", + type: ["string", "null"], }, routesVariants: { type: "array", @@ -74,7 +74,7 @@ const collectionsSchema = { type: "string", }, from: { - type: "string", + type: ["string", "null"], }, routeVariants: { type: "array", @@ -94,7 +94,7 @@ const collectionsSchema = { type: "string", }, from: { - type: "string", + type: ["string", "null"], }, routes: { type: "array", diff --git a/packages/plugin-openapi/src/Plugin.ts b/packages/plugin-openapi/src/Plugin.ts index 7514942b4..6fc7ea88c 100644 --- a/packages/plugin-openapi/src/Plugin.ts +++ b/packages/plugin-openapi/src/Plugin.ts @@ -36,7 +36,7 @@ function getRoutesCollection(routes: Routes, collectionOptions?: OpenApiDefiniti collection.routes.push(`${route.id}:${route.variants[0].id}`) } return collection; - }, { id: collectionOptions.id, from: collectionOptions.from, routes: [] } as Collection); + }, { id: collectionOptions.id, from: collectionOptions.from || null, routes: [] } as Collection); } class Plugin { @@ -51,7 +51,7 @@ class Plugin { private _loadRoutes: MockLoaders["loadRoutes"] private _loadCollections: MockLoaders["loadCollections"] private _documentsAlerts: Core["alerts"] - private _collectionNameOption: ConfigOption + private _collectionIdOption: ConfigOption private _collectionFromOption: ConfigOption constructor({ logger, alerts, mock, files, config }: Core) { @@ -61,7 +61,7 @@ class Plugin { this._files = files; const configCollection = this._config.addNamespace(COLLECTION_NAMESPACE); - [this._collectionNameOption, this._collectionFromOption] = configCollection.addOptions(COLLECTION_OPTIONS); + [this._collectionIdOption, this._collectionFromOption] = configCollection.addOptions(COLLECTION_OPTIONS); this._documentsAlerts = this._alerts.collection("documents"); @@ -104,16 +104,45 @@ class Plugin { }, { routes: [], collections: []}); } + private get _defaultCollectionOptions(): OpenApiDefinition.Collection | null { + if(!this._collectionIdOption.value) { + return null; + } + const options = { + id: this._collectionIdOption.value as string, + } as OpenApiDefinition.Collection; + + if(this._collectionFromOption.value) { + options.from = this._collectionFromOption.value as string + } + return options; + } + async _onLoadFiles(filesContents: FilesContents) { + let collectionsToLoad; this._documentsAlerts.clean(); const { routes, collections } = await this._getRoutesAndCollectionsFromFilesContents(filesContents); const folderTrace = `from OpenAPI definitions found in folder '${this._files.path}/${DEFAULT_FOLDER}'`; + this._logger.debug(`Routes to load from openApi definitions: '${JSON.stringify(routes)}'`); this._logger.verbose(`Loading ${routes.length} routes ${folderTrace}`); + this._loadRoutes(routes); - this._logger.debug(`Collections to load from OpenAPI definitions: '${JSON.stringify(collections)}'`); - this._logger.verbose(`Loading ${collections.length} collections ${folderTrace}`); - this._loadCollections(collections); + + this._logger.debug(`Collections created from OpenAPI definitions: '${JSON.stringify(collections)}'`); + + if (this._defaultCollectionOptions) { + const defaultCollection = getRoutesCollection(routes, this._defaultCollectionOptions); + this._logger.debug(`Collection created from all OpenAPI definitions: '${JSON.stringify(defaultCollection)}'`); + collectionsToLoad = collections.concat([defaultCollection as Collection]); + } else { + collectionsToLoad = collections; + } + + this._logger.verbose(`Loading ${collectionsToLoad.length} collections ${folderTrace}`); + + this._loadCollections(collectionsToLoad); + } } diff --git a/packages/plugin-openapi/src/mocks-server-core.d.ts b/packages/plugin-openapi/src/mocks-server-core.d.ts index 355ac014c..18a8fa14b 100644 --- a/packages/plugin-openapi/src/mocks-server-core.d.ts +++ b/packages/plugin-openapi/src/mocks-server-core.d.ts @@ -93,8 +93,7 @@ declare module "@mocks-server/core" { } interface ConfigOption { - addNamespace(): Config - addOptions(): Config + value: unknown } interface Config { diff --git a/packages/plugin-openapi/src/openapi.ts b/packages/plugin-openapi/src/openapi.ts index b788c3f91..610152784 100644 --- a/packages/plugin-openapi/src/openapi.ts +++ b/packages/plugin-openapi/src/openapi.ts @@ -184,9 +184,9 @@ function openApiPathToRoutes(path: string, basePath = "", openApiPathObject?: Op }).filter(notEmpty); } -function openApiDocumentToRoutes(openApiMockDocument: OpenApiDefinition.Definition): Routes { - const openApiDocument = openApiMockDocument.document; - const basePath = openApiMockDocument.basePath; +function openApiDefinitionToRoutes(openApiDefinition: OpenApiDefinition.Definition): Routes { + const openApiDocument = openApiDefinition.document; + const basePath = openApiDefinition.basePath; const paths = openApiDocument.paths || {}; return Object.keys(paths).map((path: string) => { @@ -235,21 +235,21 @@ function resolveDocumentRefs(document: OpenAPIV3.Document, refsOptions: OpenApiD }); } -async function resolveOpenApiDocumentRefs(documentMock: OpenApiDefinition.Definition, { defaultLocation, alerts, logger }: OpenApiDefinition.Options = {}): Promise { - const document = await resolveDocumentRefs(documentMock.document, {location: defaultLocation, ...documentMock.refs}, { alerts, logger }); +async function resolveOpenApiDocumentRefs(openApiDefinition: OpenApiDefinition.Definition, { defaultLocation, alerts, logger }: OpenApiDefinition.Options = {}): Promise { + const document = await resolveDocumentRefs(openApiDefinition.document, {location: defaultLocation, ...openApiDefinition.refs}, { alerts, logger }); if(document) { return { - ...documentMock, + ...openApiDefinition, document, } } return null; } -export async function openApiRoutes(openApiMockDocument: OpenApiDefinition.Definition, advancedOptions?: OpenApiDefinition.Options): Promise { - const openApiDocument = await resolveOpenApiDocumentRefs(openApiMockDocument, advancedOptions); - if(!openApiDocument) { +export async function openApiRoutes(openApiDefinition: OpenApiDefinition.Definition, advancedOptions?: OpenApiDefinition.Options): Promise { + const resolvedOpenApiDefinition = await resolveOpenApiDocumentRefs(openApiDefinition, advancedOptions); + if(!resolvedOpenApiDefinition) { return []; } - return openApiDocumentToRoutes(openApiDocument); + return openApiDefinitionToRoutes(resolvedOpenApiDefinition); } diff --git a/packages/plugin-openapi/src/types.ts b/packages/plugin-openapi/src/types.ts index 42a1b35fa..c5d5d5c7f 100644 --- a/packages/plugin-openapi/src/types.ts +++ b/packages/plugin-openapi/src/types.ts @@ -39,7 +39,7 @@ export namespace OpenApiDefinition { export interface Collection { id: string, - from: string, + from?: string, } export interface Refs { diff --git a/packages/plugin-openapi/test/collections.spec.js b/packages/plugin-openapi/test/collections.spec.js index 966d266ca..c8f251f7b 100644 --- a/packages/plugin-openapi/test/collections.spec.js +++ b/packages/plugin-openapi/test/collections.spec.js @@ -36,6 +36,20 @@ describe("generated collections", () => { "get-users-id:200-json-success", ], }, + { + id: "openapi", + from: null, + definedRoutes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + routes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + }, ]); }); }); @@ -101,6 +115,12 @@ describe("generated collections", () => { definedRoutes: ["post-users:201-status", "get-users-id:200-json-success"], routes: ["post-users:201-status", "get-users-id:200-json-success"], }, + { + id: "openapi", + from: null, + definedRoutes: ["post-users:201-status", "get-users-id:200-json-success"], + routes: ["post-users:201-status", "get-users-id:200-json-success"], + }, ]); }); }); From 2a4d3ccec9b6d4725fd98cc898dde1acb59e7a4e Mon Sep 17 00:00:00 2001 From: javierbrea Date: Thu, 25 Aug 2022 09:20:24 +0200 Subject: [PATCH 16/21] feat(#384): Support setting the collection id to null in plugin options --- packages/config/CHANGELOG.md | 5 + packages/config/README.md | 19 ++ packages/config/jest.config.js | 2 +- packages/config/src/Option.js | 10 +- packages/config/src/types.js | 1 + packages/config/src/validation.js | 51 ++-- packages/config/test/src/validate.spec.js | 115 ++++++-- packages/plugin-openapi/src/Plugin.ts | 1 + .../plugin-openapi/src/mocks-server-core.d.ts | 1 + .../plugin-openapi/test/collections.spec.js | 258 +++++++++++++++++- .../api-users-collection-from/collections.js | 6 + .../api-users-collection-from/openapi/api.js | 12 + .../routes/routes.js | 22 ++ .../multiple-definitions/collections.js | 1 + .../multiple-definitions/openapi/api.js | 13 + 15 files changed, 461 insertions(+), 56 deletions(-) create mode 100644 packages/plugin-openapi/test/fixtures/api-users-collection-from/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/api-users-collection-from/openapi/api.js create mode 100644 packages/plugin-openapi/test/fixtures/api-users-collection-from/routes/routes.js create mode 100644 packages/plugin-openapi/test/fixtures/multiple-definitions/collections.js create mode 100644 packages/plugin-openapi/test/fixtures/multiple-definitions/openapi/api.js diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index ea96532ab..1de78e179 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Removed +## [unreleased] + +### Added +- feat: Add 'nullable' property to option. Nullable types are 'string', 'number' and 'boolean' + ## [1.2.1] - 2022-08-11 ### Fixed diff --git a/packages/config/README.md b/packages/config/README.md index 031ec8621..9bef60f06 100644 --- a/packages/config/README.md +++ b/packages/config/README.md @@ -86,6 +86,8 @@ Options can be added to a namespace, or to the root config object. Both `config` Options can be of one of next types: `string`, `boolean`, `number`, `object` or `array`. This library automatically converts the values from command line arguments and environment variables to the expected type when possible. If the conversion is not possible or the validation fails an error is thrown. Validation errors provide enough context to users to let them know the option that failed. This library uses [`ajv`](https://github.com/ajv-validator) and [`better-ajv-errors`](https://github.com/atlassian/better-ajv-errors) for validations. +Types `string`, `boolean`, `number` can be nullable using the option `nullable` property. + Here is an example of how to add an option to the root config object, and then you have information about how the option would be set from different sources: ```js @@ -279,6 +281,21 @@ Examples about how to define options of type `object` from sources: __The contents of the array are also converted to its correspondent type when the `itemsType` option is provided.__ +### __Nullable types__ + +An option can be null when it is set as `nullable`. Nullable types are `string`, `boolean` and `number`. Types `object` and `array` can't be nullable, their value should be set to empty array or empty object instead. + +```js +const config = new Config({ moduleName: "moduleName" }); +const option = config.addOption({ + name: "optionA", + type: "string", + nullable: true, + default: null, +}); +await config.load(); +``` + ## Built-in options The library registers some options that can be used to determine the behavior of the library itself. As the rest of the configuration created by the library, these options can be set using configuration file, environment variables, command line arguments, etc. But there are some of them that can be defined only in some specific sources because they affect to reading that sources or not. @@ -356,6 +373,7 @@ const namespace = config.addNamespace("name"); * __`name`__ _(String)_: Name for the option. * __`description`__ _(String)_: _Optional_. Used in help, traces, etc. * __`type`__ _(String)_. One of _`string`_, _`boolean`_, _`number`_, _`array`_ or _`object`_. Used to apply type validation when loading configuration and in `option.value` setter. + * __`nullable`__ _(Boolean)_. _Optional_. Default is `false`. When `true`, the option value can be set to `null`. It is only supported in types `string`, `number` and `boolean`. * __`itemsType`__ _(String)_. Can be defined only when `type` is `array`. It must be one of _`string`_, _`boolean`_, _`number`_ or _`object`_. * __`default`__ - _Optional_. Default value. Its type depends on the `type` option. * __`extraData`__ - _(Object)_. _Optional_. Useful to store any extra data you want in the option. For example, Mocks Server uses it to define whether an option must be written when creating the configuration scaffold or not. @@ -384,6 +402,7 @@ const rootOption = config.addOption("name2"); * `callback(value)` _(Function)_: Callback to be executed whenever the option value changes. It receives the new value as first argument. * __`name`__: Getter returning the option name. * __`type`__: Getter returning the option type. +* __`nullable`__: Getter returning whether the option is nullable or not. * __`description`__: Getter returning the option description. * __`extraData`__: Getter returning the option extra data. * __`default`__: Getter returning the option default value. diff --git a/packages/config/jest.config.js b/packages/config/jest.config.js index f8bf2543a..6725e8c5b 100644 --- a/packages/config/jest.config.js +++ b/packages/config/jest.config.js @@ -26,7 +26,7 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: ["/test/**/*.spec.js"], - // testMatch: ["/test/src/getValidationSchema.spec.js"], + // testMatch: ["/test/src/validate.spec.js"], // The test environment that will be used for testing testEnvironment: "node", diff --git a/packages/config/src/Option.js b/packages/config/src/Option.js index b0b79cad3..0c8455e80 100644 --- a/packages/config/src/Option.js +++ b/packages/config/src/Option.js @@ -10,8 +10,8 @@ const { types, avoidArraysMerge } = require("./types"); class Option { constructor(properties) { this._eventEmitter = new EventEmitter(); - validateOptionAndThrow(properties); this._name = properties.name; + this._nullable = Boolean(properties.nullable); this._extraData = properties.extraData; this._type = properties.type; this._description = properties.description; @@ -20,6 +20,8 @@ class Option { this._value = this._default; this._eventsStarted = false; this._hasBeenSet = false; + + validateOptionAndThrow({ ...properties, nullable: this._nullable }); } get extraData() { @@ -38,6 +40,10 @@ class Option { return this._type; } + get nullable() { + return this._nullable; + } + get itemsType() { return this._itemsType; } @@ -64,7 +70,7 @@ class Option { } _validateAndThrow(value) { - validateValueTypeAndThrow(value, this._type, this._itemsType); + validateValueTypeAndThrow(value, this._type, this._nullable, this._itemsType); } _emitChange(previousValue, value) { diff --git a/packages/config/src/types.js b/packages/config/src/types.js index 2e6c85e6d..872b9c0f7 100644 --- a/packages/config/src/types.js +++ b/packages/config/src/types.js @@ -4,6 +4,7 @@ const types = { BOOLEAN: "boolean", OBJECT: "object", ARRAY: "array", + NULL: "null", }; const FALSY_VALUES = ["false", "0", 0]; diff --git a/packages/config/src/validation.js b/packages/config/src/validation.js index 936214f41..88538c34d 100644 --- a/packages/config/src/validation.js +++ b/packages/config/src/validation.js @@ -6,11 +6,12 @@ const ajv = new Ajv({ allErrors: true }); const { types } = require("./types"); -function enforceDefaultTypeSchema(type, itemsType) { +function enforceDefaultTypeSchema({ type, itemsType, nullable }) { const schema = { properties: { name: { type: types.STRING }, type: { enum: [type] }, + nullable: { enum: [false] }, description: { type: types.STRING }, default: { type, @@ -21,15 +22,20 @@ function enforceDefaultTypeSchema(type, itemsType) { }, }, additionalProperties: false, - required: ["name", "type"], + required: ["name", "type", "nullable"], }; + if (nullable) { + schema.properties.default.type = [type, types.NULL]; + schema.properties.nullable = { enum: [true] }; + } + if (itemsType) { schema.properties.itemsType = { enum: [itemsType] }; schema.properties.default.items = { type: itemsType, }; - schema.required = ["name", "type", "itemsType"]; + schema.required = ["name", "type", "nullable", "itemsType"]; } return schema; @@ -38,15 +44,18 @@ function enforceDefaultTypeSchema(type, itemsType) { const optionSchema = { type: types.OBJECT, oneOf: [ - enforceDefaultTypeSchema(types.NUMBER), - enforceDefaultTypeSchema(types.STRING), - enforceDefaultTypeSchema(types.BOOLEAN), - enforceDefaultTypeSchema(types.OBJECT), - enforceDefaultTypeSchema(types.ARRAY), - enforceDefaultTypeSchema(types.ARRAY, types.NUMBER), - enforceDefaultTypeSchema(types.ARRAY, types.STRING), - enforceDefaultTypeSchema(types.ARRAY, types.BOOLEAN), - enforceDefaultTypeSchema(types.ARRAY, types.OBJECT), + enforceDefaultTypeSchema({ type: types.NUMBER }), + enforceDefaultTypeSchema({ type: types.NUMBER, nullable: true }), + enforceDefaultTypeSchema({ type: types.STRING }), + enforceDefaultTypeSchema({ type: types.STRING, nullable: true }), + enforceDefaultTypeSchema({ type: types.BOOLEAN }), + enforceDefaultTypeSchema({ type: types.BOOLEAN, nullable: true }), + enforceDefaultTypeSchema({ type: types.OBJECT }), + enforceDefaultTypeSchema({ type: types.ARRAY }), + enforceDefaultTypeSchema({ type: types.ARRAY, itemsType: types.NUMBER }), + enforceDefaultTypeSchema({ type: types.ARRAY, itemsType: types.STRING }), + enforceDefaultTypeSchema({ type: types.ARRAY, itemsType: types.BOOLEAN }), + enforceDefaultTypeSchema({ type: types.ARRAY, itemsType: types.OBJECT }), ], }; @@ -133,9 +142,16 @@ function validateSchemaAndThrow(config, schema, validator) { function addNamespaceSchema(namespace, { rootSchema, allowAdditionalProperties }) { const initialSchema = rootSchema || emptySchema({ allowAdditionalProperties }); const schema = namespace.options.reduce((currentSchema, option) => { - currentSchema.properties[option.name] = { - type: option.type, - }; + if (option.nullable) { + currentSchema.properties[option.name] = { + type: [option.type, types.NULL], + }; + } else { + currentSchema.properties[option.name] = { + type: option.type, + }; + } + if (option.itemsType) { currentSchema.properties[option.name].items = { type: option.itemsType, @@ -192,7 +208,10 @@ function validateOptionAndThrow(properties) { validateSchemaAndThrow(properties, optionSchema, optionValidator); } -function validateValueTypeAndThrow(value, type, itemsType) { +function validateValueTypeAndThrow(value, type, nullable, itemsType) { + if (nullable && value === null) { + return; + } typeAndThrowValidators[type](value, itemsType); } diff --git a/packages/config/test/src/validate.spec.js b/packages/config/test/src/validate.spec.js index de425a454..5d65c1d27 100644 --- a/packages/config/test/src/validate.spec.js +++ b/packages/config/test/src/validate.spec.js @@ -14,32 +14,101 @@ describe("validate method", () => { }); describe("when option is created in root", () => { - it("should not pass validation when type is string and value does not match type", async () => { - config = new Config(); - config.addOption({ - name: "fooOption", - type: "string", - default: "default-str", - }); - const validation = config.validate({ - fooOption: 2, + function testTypeValidation({ type, validValue, invalidValue, default: defaultValue }) { + const name = "fooOption"; + + describe(`${type} type`, () => { + it("should not pass validation when value does not match type", async () => { + config = new Config(); + config.addOption({ + name, + type, + default: defaultValue, + }); + const validation = config.validate({ + [name]: invalidValue, + }); + expect(validation.valid).toEqual(false); + expect(validation.errors.length).toEqual(1); + }); + + it("should pass validation when is nullable and value is null", async () => { + config = new Config(); + config.addOption({ + name, + type, + nullable: true, + default: defaultValue, + }); + const validation = config.validate({ + [name]: null, + }); + expect(validation.valid).toEqual(true); + expect(validation.errors).toEqual(null); + }); + + it("should not throw when is nullable and value is set to null", async () => { + config = new Config(); + config.addOption({ + name, + type, + nullable: true, + default: defaultValue, + }); + config.option(name).value = null; + expect(config.option(name).value).toEqual(null); + }); + + it("should pass validation when is nullable string and default value is null", async () => { + config = new Config(); + config.addOption({ + name, + type, + default: null, + nullable: true, + }); + const validation = config.validate({ + [name]: validValue, + }); + expect(validation.valid).toEqual(true); + expect(validation.errors).toEqual(null); + }); + + it("should pass validation when value matches type", async () => { + config = new Config(); + config.addOption({ + name, + type, + default: defaultValue, + }); + const validation = config.validate({ + [name]: validValue, + }); + expect(validation.valid).toEqual(true); + expect(validation.errors).toEqual(null); + }); }); - expect(validation.valid).toEqual(false); - expect(validation.errors.length).toEqual(1); + } + + testTypeValidation({ + type: "string", + invalidValue: 2, + validValue: "2", + default: "default-str", }); - it("should pass validation when type is string and value does not match type", async () => { - config = new Config(); - config.addOption({ - name: "fooOption", - type: "string", - default: "default-str", - }); - const validation = config.validate({ - fooOption: "2", - }); - expect(validation.valid).toEqual(true); - expect(validation.errors).toEqual(null); + testTypeValidation({ + type: "number", + invalidValue: "2", + validValue: 2, + default: 3, + }); + + testTypeValidation({ + type: "boolean", + invalidValue: "2", + validValue: true, + default: false, }); }); diff --git a/packages/plugin-openapi/src/Plugin.ts b/packages/plugin-openapi/src/Plugin.ts index 6fc7ea88c..922ca3986 100644 --- a/packages/plugin-openapi/src/Plugin.ts +++ b/packages/plugin-openapi/src/Plugin.ts @@ -13,6 +13,7 @@ const COLLECTION_OPTIONS = [ description: "Name for the collection created from OpenAPI definitions", name: "id", type: "string", + nullable: true, default: "openapi", }, { diff --git a/packages/plugin-openapi/src/mocks-server-core.d.ts b/packages/plugin-openapi/src/mocks-server-core.d.ts index 18a8fa14b..77fa4577e 100644 --- a/packages/plugin-openapi/src/mocks-server-core.d.ts +++ b/packages/plugin-openapi/src/mocks-server-core.d.ts @@ -90,6 +90,7 @@ declare module "@mocks-server/core" { name: string, type: string, default?: unknown, + nullable?: boolean, } interface ConfigOption { diff --git a/packages/plugin-openapi/test/collections.spec.js b/packages/plugin-openapi/test/collections.spec.js index c8f251f7b..08d7c7853 100644 --- a/packages/plugin-openapi/test/collections.spec.js +++ b/packages/plugin-openapi/test/collections.spec.js @@ -19,8 +19,8 @@ describe("generated collections", () => { await server.stop(); }); - describe("routes", () => { - it("should have created routes from openapi definition", async () => { + describe("collections", () => { + it("should have created collections with routes from openapi definition", async () => { expect(server.mock.collections.plain).toEqual([ { id: "users", @@ -92,7 +92,6 @@ describe("generated collections", () => { describe("when route has no variants", () => { beforeAll(async () => { server = await startServer("api-users-collection-no-variants", { - log: "debug", mock: { collections: { selected: "users", @@ -106,22 +105,253 @@ describe("generated collections", () => { await server.stop(); }); - describe("routes", () => { - it("should have omitted routes without any variant", async () => { - expect(server.mock.collections.plain).toEqual([ + it("should have omitted routes without any variant in collecetions", async () => { + expect(server.mock.collections.plain).toEqual([ + { + id: "users", + from: null, + definedRoutes: ["post-users:201-status", "get-users-id:200-json-success"], + routes: ["post-users:201-status", "get-users-id:200-json-success"], + }, + { + id: "openapi", + from: null, + definedRoutes: ["post-users:201-status", "get-users-id:200-json-success"], + routes: ["post-users:201-status", "get-users-id:200-json-success"], + }, + ]); + }); + }); + + describe("default collection", () => { + beforeAll(async () => { + server = await startServer("multiple-definitions", { + mock: { + collections: { + selected: "openapi", + }, + }, + }); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + it("should include routes from all OpenAPI definitions", async () => { + expect(server.mock.collections.plain).toEqual([ + { + id: "openapi", + from: null, + definedRoutes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + "read-users:one-user", + "create-user:success", + "read-user:success", + ], + routes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + "read-users:one-user", + "create-user:success", + "read-user:success", + ], + }, + ]); + }); + }); + + describe("when default collection ID is set to null", () => { + beforeAll(async () => { + server = await startServer("api-users-collection", { + mock: { + collections: { + selected: "users", + }, + }, + plugins: { + openapi: { + collection: { + id: null, + }, + }, + }, + }); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + it("should have not created default collection", async () => { + expect(server.mock.collections.plain).toEqual([ + { + id: "users", + from: null, + definedRoutes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + routes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + }, + ]); + }); + }); + + describe("when default collection ID is set in config", () => { + beforeAll(async () => { + server = await startServer("api-users-collection", { + mock: { + collections: { + selected: "users", + }, + }, + plugins: { + openapi: { + collection: { + id: "custom-collection", + }, + }, + }, + }); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + it("should have set collection id", async () => { + expect(server.mock.collections.plain).toEqual([ + { + id: "users", + from: null, + definedRoutes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + routes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + }, + { + id: "custom-collection", + from: null, + definedRoutes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + routes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + }, + ]); + }); + }); + + describe("when default collection extends from another collection", () => { + beforeAll(async () => { + server = await startServer("api-users-collection-from", { + mock: { + collections: { + selected: "openapi", + }, + }, + plugins: { + openapi: { + collection: { + from: "users", + }, + }, + }, + }); + await waitForServer(); + }); + + afterAll(async () => { + await server.stop(); + }); + + it("should include routes from the other collection and have the from property", async () => { + expect(server.mock.collections.plain).toEqual([ + { + id: "base", + from: null, + definedRoutes: ["get-books:success"], + routes: ["get-books:success"], + }, + { + id: "users", + from: "base", + definedRoutes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + routes: [ + "get-books:success", + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + }, + { + id: "openapi", + from: "users", + definedRoutes: [ + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + routes: [ + "get-books:success", + "get-users:200-json-one-user", + "post-users:201-status", + "get-users-id:200-json-success", + ], + }, + ]); + }); + + describe("get-users route", () => { + it("should have 200-json-one-user variant available in users collection", async () => { + const response = await fetchJson("/api/users"); + expect(response.body).toEqual([ { - id: "users", - from: null, - definedRoutes: ["post-users:201-status", "get-users-id:200-json-success"], - routes: ["post-users:201-status", "get-users-id:200-json-success"], + id: 1, + name: "John Doe", }, + ]); + expect(response.status).toEqual(200); + }); + }); + + describe("get-books route", () => { + it("should be also available", async () => { + const response = await fetchJson("/api/books"); + expect(response.body).toEqual([ { - id: "openapi", - from: null, - definedRoutes: ["post-users:201-status", "get-users-id:200-json-success"], - routes: ["post-users:201-status", "get-users-id:200-json-success"], + title: "1984", + author: "George Orwell", }, ]); + expect(response.status).toEqual(200); }); }); }); diff --git a/packages/plugin-openapi/test/fixtures/api-users-collection-from/collections.js b/packages/plugin-openapi/test/fixtures/api-users-collection-from/collections.js new file mode 100644 index 000000000..19788a99e --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/api-users-collection-from/collections.js @@ -0,0 +1,6 @@ +module.exports = [ + { + id: "base", + routes: ["get-books:success"], + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/api-users-collection-from/openapi/api.js b/packages/plugin-openapi/test/fixtures/api-users-collection-from/openapi/api.js new file mode 100644 index 000000000..98e3253cf --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/api-users-collection-from/openapi/api.js @@ -0,0 +1,12 @@ +const openApiDocument = require("../../../openapi/users"); + +module.exports = [ + { + basePath: "/api", + collection: { + id: "users", + from: "base", + }, + document: openApiDocument, + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/api-users-collection-from/routes/routes.js b/packages/plugin-openapi/test/fixtures/api-users-collection-from/routes/routes.js new file mode 100644 index 000000000..6dd28e4c9 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/api-users-collection-from/routes/routes.js @@ -0,0 +1,22 @@ +module.exports = [ + { + id: "get-books", + url: "/api/books", + method: "get", + variants: [ + { + id: "success", + type: "json", + options: { + status: 200, + body: [ + { + title: "1984", + author: "George Orwell", + }, + ], + }, + }, + ], + }, +]; diff --git a/packages/plugin-openapi/test/fixtures/multiple-definitions/collections.js b/packages/plugin-openapi/test/fixtures/multiple-definitions/collections.js new file mode 100644 index 000000000..e0a30c5df --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/multiple-definitions/collections.js @@ -0,0 +1 @@ +module.exports = []; diff --git a/packages/plugin-openapi/test/fixtures/multiple-definitions/openapi/api.js b/packages/plugin-openapi/test/fixtures/multiple-definitions/openapi/api.js new file mode 100644 index 000000000..53eba05d6 --- /dev/null +++ b/packages/plugin-openapi/test/fixtures/multiple-definitions/openapi/api.js @@ -0,0 +1,13 @@ +const openApiDocument = require("../../../openapi/users"); +const openApiDocumentCustomIds = require("../../custom-ids/openapi/api")[0]; + +module.exports = [ + { + basePath: "/api", + document: openApiDocument, + }, + { + ...openApiDocumentCustomIds, + basePath: "/api-2", + }, +]; From da74f5d1ce816349dc97e3c9e65f1a5ab6d313f0 Mon Sep 17 00:00:00 2001 From: javierbrea Date: Thu, 25 Aug 2022 09:25:46 +0200 Subject: [PATCH 17/21] docs: Add options to readme --- packages/plugin-openapi/README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/plugin-openapi/README.md b/packages/plugin-openapi/README.md index a35320e5c..2fd5fd405 100644 --- a/packages/plugin-openapi/README.md +++ b/packages/plugin-openapi/README.md @@ -13,7 +13,18 @@ # Mocks Server Plugin OpenApi -[Mocks Server][website-url] plugin allowing to create routes and collections from OpenApi definitions. +[Mocks Server][website-url] plugin enabling to create routes and collections from OpenApi definitions. + +## Usage + +This plugin is included in the main distribution of the Mocks Server project, so you can refer to the [official documentation website][website-url]. + +## Options + +* __`plugins.openapi.collection.id`__ _(String | Null)_: Id for the collection to be created with __all routes from all OpenAPI documents__. Default is "openapi". When it is set to `null`, no collection will be created. +* __`plugins.openapi.collection.from`__ _(String)_: When provided, the created collection will extend from this one. + +Read more about [how to set options in Mocks Server here](https://www.mocks-server.org/docs/configuration/how-to-change-settings). ## Contributing From 205343f7b02d16851647e815e3ba7b8276a879df Mon Sep 17 00:00:00 2001 From: javierbrea Date: Thu, 25 Aug 2022 09:41:37 +0200 Subject: [PATCH 18/21] test: Fix unit test --- packages/core/test/mock/validations.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/mock/validations.spec.js b/packages/core/test/mock/validations.spec.js index b82fbe5c2..b297b60ba 100644 --- a/packages/core/test/mock/validations.spec.js +++ b/packages/core/test/mock/validations.spec.js @@ -459,7 +459,7 @@ describe("mocks validations", () => { foo: "foo", }); expect(errors.message).toEqual( - "Collection is invalid: must have required property 'routes'. /from: type must be string. /from: type must be string. /from: type must be string" + "Collection is invalid: must have required property 'routes'. /from: type must be string,null. /from: type must be string,null. /from: type must be string,null" ); }); }); From 312bf72d33b215efd927e937461603d6754ac751 Mon Sep 17 00:00:00 2001 From: javierbrea Date: Thu, 25 Aug 2022 09:42:10 +0200 Subject: [PATCH 19/21] feat(#384): Do not create default collection if there are no openapi files --- packages/plugin-openapi/src/Plugin.ts | 39 ++++++++++++++------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/plugin-openapi/src/Plugin.ts b/packages/plugin-openapi/src/Plugin.ts index 922ca3986..35bf78fdb 100644 --- a/packages/plugin-openapi/src/Plugin.ts +++ b/packages/plugin-openapi/src/Plugin.ts @@ -120,30 +120,31 @@ class Plugin { } async _onLoadFiles(filesContents: FilesContents) { - let collectionsToLoad; - this._documentsAlerts.clean(); - const { routes, collections } = await this._getRoutesAndCollectionsFromFilesContents(filesContents); - const folderTrace = `from OpenAPI definitions found in folder '${this._files.path}/${DEFAULT_FOLDER}'`; + if (filesContents.length) { + let collectionsToLoad; + this._documentsAlerts.clean(); + const { routes, collections } = await this._getRoutesAndCollectionsFromFilesContents(filesContents); + const folderTrace = `from OpenAPI definitions found in folder '${this._files.path}/${DEFAULT_FOLDER}'`; - this._logger.debug(`Routes to load from openApi definitions: '${JSON.stringify(routes)}'`); - this._logger.verbose(`Loading ${routes.length} routes ${folderTrace}`); + this._logger.debug(`Routes to load from openApi definitions: '${JSON.stringify(routes)}'`); + this._logger.verbose(`Loading ${routes.length} routes ${folderTrace}`); - this._loadRoutes(routes); + this._loadRoutes(routes); - this._logger.debug(`Collections created from OpenAPI definitions: '${JSON.stringify(collections)}'`); + this._logger.debug(`Collections created from OpenAPI definitions: '${JSON.stringify(collections)}'`); - if (this._defaultCollectionOptions) { - const defaultCollection = getRoutesCollection(routes, this._defaultCollectionOptions); - this._logger.debug(`Collection created from all OpenAPI definitions: '${JSON.stringify(defaultCollection)}'`); - collectionsToLoad = collections.concat([defaultCollection as Collection]); - } else { - collectionsToLoad = collections; - } - - this._logger.verbose(`Loading ${collectionsToLoad.length} collections ${folderTrace}`); - - this._loadCollections(collectionsToLoad); + if (this._defaultCollectionOptions) { + const defaultCollection = getRoutesCollection(routes, this._defaultCollectionOptions); + this._logger.debug(`Collection created from all OpenAPI definitions: '${JSON.stringify(defaultCollection)}'`); + collectionsToLoad = collections.concat([defaultCollection as Collection]); + } else { + collectionsToLoad = collections; + } + + this._logger.verbose(`Loading ${collectionsToLoad.length} collections ${folderTrace}`); + this._loadCollections(collectionsToLoad); + } } } From d6596983c3bcd0e5d184b79487a3241699ecb8ef Mon Sep 17 00:00:00 2001 From: javierbrea Date: Thu, 25 Aug 2022 11:07:26 +0200 Subject: [PATCH 20/21] refactor: Fix Sonar smell --- packages/core/CHANGELOG.md | 4 ++++ packages/core/src/mock/validations.js | 4 ++-- packages/main/CHANGELOG.md | 5 ++++- packages/plugin-openapi/CHANGELOG.md | 3 +++ packages/plugin-openapi/src/openapi.ts | 3 --- packages/plugin-openapi/src/types.ts | 4 ++-- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index c8006ea4c..d00c81fe0 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - feat: Support asynchronies in files. Files now can export a function. In that case, the loader will receive the result of the function. If function returns a promise, it will receive the result of the promise once it is resolved (rejected promises are treated as file load errors). - Added: Support null value in "from" property in collections +### Changed + +- chore(deps): Update @mocks-server/config to 1.3.0 + ### Fixed - fix: Collections and routes validation was throwing when undefined was passed as value diff --git a/packages/core/src/mock/validations.js b/packages/core/src/mock/validations.js index 0badca761..9d8896b9c 100644 --- a/packages/core/src/mock/validations.js +++ b/packages/core/src/mock/validations.js @@ -245,8 +245,8 @@ function customValidationSingleMessage(errors) { .join(". "); } -function validationSingleMessage(schema, data = {}, errors) { - const formattedJson = betterAjvErrors(schema, data, errors, { +function validationSingleMessage(schema, data, errors) { + const formattedJson = betterAjvErrors(schema, data || {}, errors, { format: "js", }); return formattedJson.map((result) => result.error).join(". "); diff --git a/packages/main/CHANGELOG.md b/packages/main/CHANGELOG.md index 34fb7c861..48b386ae2 100644 --- a/packages/main/CHANGELOG.md +++ b/packages/main/CHANGELOG.md @@ -17,7 +17,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] ### Added -- feat(#384): Add plugin-openapi to preinstalled plugins +- feat(#384): Add `@mocks-server/plugin-openapi` to preinstalled plugins + +### Changed +- chore(deps): Update `@mocks-server/core` dependency to 3.11.0 ## [3.10.0] - 2022-08-11 diff --git a/packages/plugin-openapi/CHANGELOG.md b/packages/plugin-openapi/CHANGELOG.md index 37c14bdac..010aec3c9 100644 --- a/packages/plugin-openapi/CHANGELOG.md +++ b/packages/plugin-openapi/CHANGELOG.md @@ -12,3 +12,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### BREAKING CHANGE ## [unreleased] + +### Added +- feat: First release diff --git a/packages/plugin-openapi/src/openapi.ts b/packages/plugin-openapi/src/openapi.ts index 610152784..8636ba121 100644 --- a/packages/plugin-openapi/src/openapi.ts +++ b/packages/plugin-openapi/src/openapi.ts @@ -81,7 +81,6 @@ function getStatusCode(code: string, codes: string[]): number { return Number(replaceCodeWildcards(code)); } -// TODO, support also ReferenceObject in examples function openApiResponseExampleToVariant(exampleId: string, code: number, variantType: RouteVariantTypes, mediaType: string, openApiResponseExample: OpenAPIV3.ExampleObject, openApiResponseHeaders?: OpenAPIV3.ResponseHeaders): RouteVariant | null { if(!notEmpty(openApiResponseExample) || !notEmpty(openApiResponseExample.value)) { return null; @@ -107,7 +106,6 @@ function openApiResponseNoContentToVariant(code: number, openApiResponse: OpenAP return { ...baseVariant, options: { - // TODO, convert possible ref headers: openApiResponse.headers as HTTPHeaders, status: code, } @@ -120,7 +118,6 @@ function openApiResponseExamplesToVariants(code: number, variantType: RouteVaria return null; } return Object.keys(examples).map((exampleId: string) => { - // TODO, support also ReferenceObject in examples return openApiResponseExampleToVariant(exampleId, code, variantType, mediaType, examples[exampleId] as OpenAPIV3.ExampleObject , openApiResponseHeaders); }).filter(notEmpty); } diff --git a/packages/plugin-openapi/src/types.ts b/packages/plugin-openapi/src/types.ts index c5d5d5c7f..2b051901f 100644 --- a/packages/plugin-openapi/src/types.ts +++ b/packages/plugin-openapi/src/types.ts @@ -29,10 +29,10 @@ export namespace OpenAPIV3 { export namespace OpenApiDefinition { export interface Options { defaultLocation?: string, - // TODO, add alerts type when exported by core + // Add alerts type when exported by core // eslint-disable-next-line @typescript-eslint/no-explicit-any alerts?: any, - // TODO, add alerts type when exported by core + // Add alerts type when exported by core // eslint-disable-next-line @typescript-eslint/no-explicit-any logger?: any } From 2c7352668ff34869c78d7aa38c1a2c6f78af31b9 Mon Sep 17 00:00:00 2001 From: javierbrea Date: Thu, 25 Aug 2022 13:20:10 +0200 Subject: [PATCH 21/21] chore: upgrade versions --- packages/config/CHANGELOG.md | 2 +- packages/config/package.json | 2 +- packages/config/sonar-project.properties | 2 +- packages/core/CHANGELOG.md | 2 +- packages/core/package.json | 2 +- packages/core/sonar-project.properties | 2 +- packages/main/CHANGELOG.md | 2 +- packages/main/package.json | 2 +- packages/main/sonar-project.properties | 2 +- packages/plugin-openapi/CHANGELOG.md | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 1de78e179..d60382331 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Removed -## [unreleased] +## [1.3.0] - 2022-08-25 ### Added - feat: Add 'nullable' property to option. Nullable types are 'string', 'number' and 'boolean' diff --git a/packages/config/package.json b/packages/config/package.json index 037bf534a..af743c6fb 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@mocks-server/config", - "version": "1.2.1", + "version": "1.3.0", "description": "Modular configuration provider. Read it from file, environment and arguments", "keywords": [ "configuration", diff --git a/packages/config/sonar-project.properties b/packages/config/sonar-project.properties index f37ba261f..d0f17a39d 100644 --- a/packages/config/sonar-project.properties +++ b/packages/config/sonar-project.properties @@ -1,7 +1,7 @@ sonar.organization=mocks-server sonar.projectKey=mocks-server_main_config sonar.projectName=config -sonar.projectVersion=1.2.1 +sonar.projectVersion=1.3.0 sonar.javascript.file.suffixes=.js sonar.sourceEncoding=UTF-8 diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index d00c81fe0..f3d9f432b 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed ### Removed -## [unreleased] +## [3.11.0] - 2022-08-25 ### Added - feat: Support asynchronies in files. Files now can export a function. In that case, the loader will receive the result of the function. If function returns a promise, it will receive the result of the promise once it is resolved (rejected promises are treated as file load errors). diff --git a/packages/core/package.json b/packages/core/package.json index f6cb5d9e3..fc3a17f7a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@mocks-server/core", - "version": "3.10.0", + "version": "3.11.0", "description": "Pluggable mock server supporting multiple route variants and mocks", "keywords": [ "mocks", diff --git a/packages/core/sonar-project.properties b/packages/core/sonar-project.properties index 292e226f9..f67bfc890 100644 --- a/packages/core/sonar-project.properties +++ b/packages/core/sonar-project.properties @@ -1,7 +1,7 @@ sonar.organization=mocks-server sonar.projectKey=mocks-server_main_core sonar.projectName=core -sonar.projectVersion=3.10.0 +sonar.projectVersion=3.11.0 sonar.javascript.file.suffixes=.js sonar.sourceEncoding=UTF-8 diff --git a/packages/main/CHANGELOG.md b/packages/main/CHANGELOG.md index 48b386ae2..5141a479a 100644 --- a/packages/main/CHANGELOG.md +++ b/packages/main/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Removed ### Breaking change -## [unreleased] +## [3.11.0] - 2022-08-25 ### Added - feat(#384): Add `@mocks-server/plugin-openapi` to preinstalled plugins diff --git a/packages/main/package.json b/packages/main/package.json index 9428ceefb..8575d150b 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -1,6 +1,6 @@ { "name": "@mocks-server/main", - "version": "3.10.0", + "version": "3.11.0", "description": "Mock Server supporting multiple route variants and mocks", "keywords": [ "mock", diff --git a/packages/main/sonar-project.properties b/packages/main/sonar-project.properties index 962693add..78dc5870d 100644 --- a/packages/main/sonar-project.properties +++ b/packages/main/sonar-project.properties @@ -1,7 +1,7 @@ sonar.organization=mocks-server sonar.projectKey=mocks-server_main sonar.projectName=main -sonar.projectVersion=3.10.0 +sonar.projectVersion=3.11.0 sonar.javascript.file.suffixes=.js sonar.sourceEncoding=UTF-8 diff --git a/packages/plugin-openapi/CHANGELOG.md b/packages/plugin-openapi/CHANGELOG.md index 010aec3c9..209f5ebe4 100644 --- a/packages/plugin-openapi/CHANGELOG.md +++ b/packages/plugin-openapi/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Removed ### BREAKING CHANGE -## [unreleased] +## [1.0.0] - 2022-08-25 ### Added - feat: First release