diff --git a/.eslintrc.json b/.eslintrc.json index 858d392..183550d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -6,7 +6,7 @@ "parserOptions": { "ecmaVersion": "2018" }, - "plugins": ["prettier"], + "plugins": ["prettier", "no-only-tests"], "rules": { "prettier/prettier": [ "error", @@ -16,7 +16,8 @@ } ], "no-undef": "error", - "no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }] + "no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }], + "no-only-tests/no-only-tests": [2] }, "extends": ["prettier"], "root": true diff --git a/CHANGELOG.md b/CHANGELOG.md index 115957f..fe50dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] ### Added +- feat(#78): Add alerts router +- chore(lint): Add eslint plugin to avoid only in tests ### Changed +- chore(deps): Update dependencies +- test(e2e): Move utils to a support folder +- feat: Add/remove routers on start/stop plugin methods, not in init method. ### Fixed ### Removed diff --git a/jest.config.js b/jest.config.js index 52c9c81..6ab54f4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -25,7 +25,8 @@ module.exports = { }, // The glob patterns Jest uses to detect test files - testMatch: ["**/test/unit/**/?(*.)+(spec|test).js?(x)"], + testMatch: ["/test/unit/**/*.spec.js"], + // testMatch: ["/test/unit/**/Plugin.spec.js"], // The test environment that will be used for testing testEnvironment: "node", diff --git a/jest.e2e.config.js b/jest.e2e.config.js index 52a2db0..165c323 100644 --- a/jest.e2e.config.js +++ b/jest.e2e.config.js @@ -5,7 +5,8 @@ module.exports = { // Automatically clear mock calls and instances between every test clearMocks: true, - testMatch: ["**/test/e2e/**/?(*.)+(spec|test).js?(x)"], + testMatch: ["/test/e2e/**/*.spec.js"], + // testMatch: ["/test/e2e/**/stop-plugin.spec.js"], // Indicates whether the coverage information should be collected while executing the test collectCoverage: false, diff --git a/package-lock.json b/package-lock.json index 3aad602..1a99062 100644 --- a/package-lock.json +++ b/package-lock.json @@ -700,14 +700,14 @@ } }, "@mocks-server/admin-api-paths": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@mocks-server/admin-api-paths/-/admin-api-paths-1.0.8.tgz", - "integrity": "sha512-UH7PqoUWhUjUexP5IeS08frHKsKa5mzLi/HosbWLxo0rkk6SqzOmxg+2mtBPPx+y/Pw8RygfiwvRdUJGvK1byA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mocks-server/admin-api-paths/-/admin-api-paths-1.1.0.tgz", + "integrity": "sha512-qFcIsRtfD9uXj3Js0qKyKZKUeoCgzcjxZWPw3Xe64bPByE5lrPYlMzVMbmENVLIkQS5aOlVttxOzpG8jHO/DxA==" }, "@mocks-server/core": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@mocks-server/core/-/core-1.5.1.tgz", - "integrity": "sha512-RIJlVpzi0MIDt163B0Bi6gA5DbrJwm1IZgwJhHCprPXoD24x7KbA3q5b12LdJdayRx/BXvtmn1zX+piZqCRhmQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@mocks-server/core/-/core-1.6.0.tgz", + "integrity": "sha512-Gh2C7RGXvs09x7qDArby2XN1G11iOyLY5AcZzSaJXHM59F59NV9E17rNKf5i8S2XJVv1aNA4WO/AqsIfMFDcXQ==", "dev": true, "requires": { "@hapi/boom": "9.1.0", @@ -724,14 +724,6 @@ "require-all": "3.0.0", "route-parser": "0.0.5", "winston": "3.3.3" - }, - "dependencies": { - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true - } } }, "@sinonjs/commons": { @@ -2195,6 +2187,12 @@ "get-stdin": "^6.0.0" } }, + "eslint-plugin-no-only-tests": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-2.4.0.tgz", + "integrity": "sha512-azP9PwQYfGtXJjW273nIxQH9Ygr+5/UyeW2wEjYoDtVYPI+WPKwbj0+qcAKYUXFZLRumq4HKkFaoDBAwBoXImQ==", + "dev": true + }, "eslint-plugin-prettier": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", diff --git a/package.json b/package.json index de98776..94e6f33 100644 --- a/package.json +++ b/package.json @@ -34,13 +34,14 @@ "@mocks-server/core": ">=1.3.0" }, "dependencies": { - "@mocks-server/admin-api-paths": "1.0.8", + "@mocks-server/admin-api-paths": "1.1.0", "@hapi/boom": "9.1.0", "express": "4.17.1" }, "devDependencies": { - "@mocks-server/core": "1.5.1", + "@mocks-server/core": "1.6.0", "eslint": "7.12.1", + "eslint-plugin-no-only-tests": "2.4.0", "eslint-config-prettier": "6.15.0", "eslint-plugin-prettier": "3.1.4", "fs-extra": "9.0.1", diff --git a/src/Alerts.js b/src/Alerts.js new file mode 100644 index 0000000..3e309d1 --- /dev/null +++ b/src/Alerts.js @@ -0,0 +1,70 @@ +/* +Copyright 2020 Javier Brea +Copyright 2019 XbyOrange + +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. +*/ + +"use strict"; + +const express = require("express"); +const Boom = require("@hapi/boom"); + +const { PLUGIN_NAME } = require("./constants"); + +class AlertsApi { + constructor(core) { + this._core = core; + this._tracer = core.tracer; + this._router = express.Router(); + this._router.get("/", this.getCollection.bind(this)); + this._router.get("/:id", this.getModel.bind(this)); + } + + _parseModel(alert) { + return { + id: alert.context, + context: alert.context, + message: alert.message, + error: alert.error + ? { + name: alert.error.name, + message: alert.error.message, + stack: alert.error.stack, + } + : null, + }; + } + + _parseCollection() { + return this._core.alerts.map(this._parseModel); + } + + getCollection(req, res) { + this._tracer.verbose(`${PLUGIN_NAME}: Sending alerts | ${req.id}`); + res.status(200); + res.send(this._parseCollection()); + } + + getModel(req, res, next) { + const id = req.params.id; + this._tracer.verbose(`${PLUGIN_NAME}: Sending alert ${id} | ${req.id}`); + const foundAlert = this._core.alerts.find((alert) => alert.context === id); + if (foundAlert) { + res.status(200); + res.send(this._parseModel(foundAlert)); + } else { + next(Boom.notFound(`Alert with id "${id}" was not found`)); + } + } + + get router() { + return this._router; + } +} + +module.exports = AlertsApi; diff --git a/src/Plugin.js b/src/Plugin.js index c5c9fbb..c7657b0 100644 --- a/src/Plugin.js +++ b/src/Plugin.js @@ -16,6 +16,7 @@ const { BEHAVIORS, ABOUT, FIXTURES, + ALERTS, } = require("@mocks-server/admin-api-paths"); const packageInfo = require("../package.json"); @@ -23,6 +24,7 @@ const DeprecatedApi = require("./deprecated/Api"); const Settings = require("./Settings"); const Behaviors = require("./Behaviors"); +const Alerts = require("./Alerts"); const Fixtures = require("./Fixtures"); const About = require("./About"); @@ -41,6 +43,7 @@ class Plugin { this._deprecatedApi = new DeprecatedApi(core); this._settingsApi = new Settings(this._core); this._behaviorsApi = new Behaviors(this._core); + this._alertsApi = new Alerts(this._core); this._aboutApi = new About(this._core); this._fixturesApi = new Fixtures(this._core); core.addSetting({ @@ -66,28 +69,34 @@ class Plugin { async init() { await this._deprecatedApi.init(); - this._core.onChangeSettings(this._onChangeSettings); this._initRouter(); + } + + start() { + this._stopListeningOnChangeSettings = this._core.onChangeSettings(this._onChangeSettings); this._addDeprecatedRouter(); this._addRouter(); } + stop() { + if (this._stopListeningOnChangeSettings) { + this._stopListeningOnChangeSettings(); + } + this._removeDeprecatedRouter(); + this._removeRouter(); + } + _initRouter() { this._router = express.Router(); this._router.use(SETTINGS, this._settingsApi.router); this._router.use(BEHAVIORS, this._behaviorsApi.router); this._router.use(ABOUT, this._aboutApi.router); this._router.use(FIXTURES, this._fixturesApi.router); + this._router.use(ALERTS, this._alertsApi.router); } _addDeprecatedRouter() { - if ( - this._settings.get(ADMIN_API_DEPRECATED_PATHS_OPTION) === false && - this._addedDeprecatedRouter - ) { - this._core.removeRouter(DEPRECATED_API_PATH, this._deprecatedApi.router); - this._addedDeprecatedRouter = false; - } + this._removeDeprecatedRouter(); if ( this._settings.get(ADMIN_API_DEPRECATED_PATHS_OPTION) === true && !this._addedDeprecatedRouter @@ -97,12 +106,24 @@ class Plugin { } } + _removeDeprecatedRouter() { + if (this._addedDeprecatedRouter) { + this._core.removeRouter(DEPRECATED_API_PATH, this._deprecatedApi.router); + this._addedDeprecatedRouter = false; + } + } + _addRouter() { - if (this._previousRoutersPath) { - this._core.removeRouter(this._previousRoutersPath, this._router); + this._removeRouter(); + this._routersPath = this._settings.get(ADMIN_API_PATH_OPTION); + this._core.addRouter(this._routersPath, this._router); + } + + _removeRouter() { + if (this._routersPath) { + this._core.removeRouter(this._routersPath, this._router); + this._routersPath = null; } - this._previousRoutersPath = this._settings.get(ADMIN_API_PATH_OPTION); - this._core.addRouter(this._previousRoutersPath, this._router); } _onChangeSettings(newSettings) { diff --git a/test/e2e/about-api.spec.js b/test/e2e/about-api.spec.js index 293211c..7e06abb 100644 --- a/test/e2e/about-api.spec.js +++ b/test/e2e/about-api.spec.js @@ -8,7 +8,7 @@ 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 { startServer, stopServer, request } = require("./utils"); +const { startServer, stopServer, request } = require("./support/utils"); const { version } = require("../../package.json"); describe("about api", () => { @@ -17,8 +17,8 @@ describe("about api", () => { server = await startServer("web-tutorial"); }); - afterAll(() => { - stopServer(server); + afterAll(async () => { + await stopServer(server); }); describe("get /", () => { diff --git a/test/e2e/alerts-api.spec.js b/test/e2e/alerts-api.spec.js new file mode 100644 index 0000000..c09093e --- /dev/null +++ b/test/e2e/alerts-api.spec.js @@ -0,0 +1,116 @@ +/* +Copyright 2020 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 path = require("path"); +const fsExtra = require("fs-extra"); + +const { CliRunner, request, fixturesFolder, wait } = require("./support/utils"); + +describe("alerts api", () => { + let cli; + beforeAll(async () => { + fsExtra.removeSync(fixturesFolder("files-watch")); + fsExtra.copySync(fixturesFolder("web-tutorial"), fixturesFolder("files-watch")); + cli = new CliRunner(["node", "start.js", "--path=files-watch", "--behavior=foo"], { + cwd: path.resolve(__dirname, "fixtures"), + }); + await wait(1000); + }); + + afterAll(async () => { + await cli.kill(); + }); + + describe("when started", () => { + it("should return behavior not found alert", async () => { + const response = await request("/admin/alerts"); + expect(response.length).toEqual(1); + }); + + it("should return specific alert when requested by id", async () => { + const response = await request("/admin/alerts/mocks%3Abehaviors%3Acurrent"); + expect(response).toEqual({ + id: "mocks:behaviors:current", + context: "mocks:behaviors:current", + message: 'Defined behavior "foo" was not found. The first one found was used instead', + error: null, + }); + }); + + it("should serve users collection mock under the /api/users path", async () => { + const users = await request("/api/users"); + expect(users).toEqual([ + { id: 1, name: "John Doe" }, + { id: 2, name: "Jane Doe" }, + ]); + }); + }); + + describe("when behavior is modified", () => { + beforeAll(async () => { + await request("/admin/settings", { + method: "PATCH", + body: { + behavior: "dynamic", + }, + }); + await wait(); + }, 10000); + + it("should return no alerts", async () => { + const response = await request("/admin/alerts"); + expect(response.length).toEqual(0); + }); + }); + + describe("when files contain an error", () => { + beforeAll(async () => { + fsExtra.copySync(fixturesFolder("files-error"), fixturesFolder("files-watch")); + await wait(6000); + }, 10000); + + it("should return one alert", async () => { + const response = await request("/admin/alerts"); + expect(response.length).toEqual(1); + }); + + it("should return specific alert when requested by id", async () => { + const response = await request( + "/admin/alerts/plugins%3A%40mocks-server%2Fcore%2Fplugin-files-loader%3Aload" + ); + expect(response.id).toEqual("plugins:@mocks-server/core/plugin-files-loader:load"); + expect(response.message).toEqual(expect.stringContaining("test/e2e/fixtures/files-watch")); + expect(response.error.name).toEqual("ReferenceError"); + expect(response.error.message).toEqual("FOO is not defined"); + expect(response.error.stack).toEqual( + expect.stringContaining("test/e2e/fixtures/files-watch/fixtures/users.js:2:18") + ); + }); + }); + + describe("when files error is fixed", () => { + beforeAll(async () => { + fsExtra.copySync(fixturesFolder("web-tutorial"), fixturesFolder("files-watch")); + await wait(6000); + }, 10000); + + it("should return no alerts", async () => { + const response = await request("/admin/alerts"); + expect(response.length).toEqual(0); + }); + + it("should serve users collection mock under the /api/users path", async () => { + const users = await request("/api/users"); + expect(users).toEqual([ + { id: 1, name: "John Doe" }, + { id: 2, name: "Jane Doe" }, + ]); + }); + }); +}); diff --git a/test/e2e/behaviors-api.spec.js b/test/e2e/behaviors-api.spec.js index 480b1de..eb0894b 100644 --- a/test/e2e/behaviors-api.spec.js +++ b/test/e2e/behaviors-api.spec.js @@ -8,7 +8,7 @@ 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 { startServer, stopServer, request } = require("./utils"); +const { startServer, stopServer, request } = require("./support/utils"); describe("behaviors api", () => { let server; @@ -16,8 +16,8 @@ describe("behaviors api", () => { server = await startServer("web-tutorial"); }); - afterAll(() => { - stopServer(server); + afterAll(async () => { + await stopServer(server); }); describe("get /", () => { diff --git a/test/e2e/change-behavior.spec.js b/test/e2e/change-behavior.spec.js index 93dab96..d00f117 100644 --- a/test/e2e/change-behavior.spec.js +++ b/test/e2e/change-behavior.spec.js @@ -8,7 +8,7 @@ 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 { startServer, stopServer, request } = require("./utils"); +const { startServer, stopServer, request } = require("./support/utils"); describe("API for changing current behavior", () => { let server; @@ -17,8 +17,8 @@ describe("API for changing current behavior", () => { server = await startServer("web-tutorial-json"); }); - afterAll(() => { - stopServer(server); + afterAll(async () => { + await stopServer(server); }); describe("When started", () => { diff --git a/test/e2e/deprecated-change-behavior.spec.js b/test/e2e/deprecated-change-behavior.spec.js index 3167402..36f3632 100644 --- a/test/e2e/deprecated-change-behavior.spec.js +++ b/test/e2e/deprecated-change-behavior.spec.js @@ -14,7 +14,7 @@ const { request, deprecatedChangeBehavior, deprecatedGetBehaviors, -} = require("./utils"); +} = require("./support/utils"); describe("deprecated API for changing current behavior", () => { let server; @@ -23,8 +23,8 @@ describe("deprecated API for changing current behavior", () => { server = await startServer(); }); - afterAll(() => { - stopServer(server); + afterAll(async () => { + await stopServer(server); }); describe("When started", () => { diff --git a/test/e2e/deprecated-core-events.spec.js b/test/e2e/deprecated-core-events.spec.js index 831d61e..5e90603 100644 --- a/test/e2e/deprecated-core-events.spec.js +++ b/test/e2e/deprecated-core-events.spec.js @@ -17,7 +17,7 @@ const { fixturesFolder, wait, CliRunner, -} = require("./utils"); +} = require("./support/utils"); describe("Plugin listening to core events", () => { let cli; diff --git a/test/e2e/fixtures-api.spec.js b/test/e2e/fixtures-api.spec.js index e477ef0..21ae4cf 100644 --- a/test/e2e/fixtures-api.spec.js +++ b/test/e2e/fixtures-api.spec.js @@ -8,16 +8,16 @@ 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 { startServer, stopServer, request } = require("./utils"); +const { startServer, stopServer, request } = require("./support/utils"); -describe("fixtures api", () => { +describe("alerts api", () => { let server; beforeAll(async () => { server = await startServer("web-tutorial"); }); - afterAll(() => { - stopServer(server); + afterAll(async () => { + await stopServer(server); }); describe("get /", () => { diff --git a/test/e2e/fixtures/files-error/fixtures/users.js b/test/e2e/fixtures/files-error/fixtures/users.js new file mode 100644 index 0000000..3cb2db1 --- /dev/null +++ b/test/e2e/fixtures/files-error/fixtures/users.js @@ -0,0 +1,2 @@ +//eslint-disable-next-line +module.exports = FOO; diff --git a/test/e2e/fixtures/start-files-watch.js b/test/e2e/fixtures/start-files-watch.js index 9b83d5a..4a3cc65 100644 --- a/test/e2e/fixtures/start-files-watch.js +++ b/test/e2e/fixtures/start-files-watch.js @@ -1,5 +1,5 @@ const path = require("path"); -const { startServer } = require("../utils"); +const { startServer } = require("../support/utils"); startServer(path.resolve(__dirname, "files-watch"), { watch: true, diff --git a/test/e2e/plugin-options.spec.js b/test/e2e/plugin-options.spec.js index 525dcbb..7ee8a32 100644 --- a/test/e2e/plugin-options.spec.js +++ b/test/e2e/plugin-options.spec.js @@ -8,7 +8,14 @@ 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 path = require("path"); -const { startServer, stopServer, request, CliRunner, wait, fixturesFolder } = require("./utils"); +const { + startServer, + stopServer, + request, + CliRunner, + wait, + fixturesFolder, +} = require("./support/utils"); describe("plugin options", () => { let server; @@ -21,8 +28,8 @@ describe("plugin options", () => { }); }); - afterAll(() => { - stopServer(server); + afterAll(async () => { + await stopServer(server); }); it("should disable deprecated behaviors api path", async () => { diff --git a/test/e2e/settings-api.spec.js b/test/e2e/settings-api.spec.js index 4d40b11..f405cb3 100644 --- a/test/e2e/settings-api.spec.js +++ b/test/e2e/settings-api.spec.js @@ -8,7 +8,14 @@ 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 { startServer, stopServer, request, fixturesFolder, TimeCounter, wait } = require("./utils"); +const { + startServer, + stopServer, + request, + fixturesFolder, + TimeCounter, + wait, +} = require("./support/utils"); describe("settings api", () => { let server; @@ -16,8 +23,8 @@ describe("settings api", () => { server = await startServer("web-tutorial"); }); - afterAll(() => { - stopServer(server); + afterAll(async () => { + await stopServer(server); }); describe("get", () => { diff --git a/test/e2e/settings-logs-api.spec.js b/test/e2e/settings-logs-api.spec.js index e790a59..0d34b01 100644 --- a/test/e2e/settings-logs-api.spec.js +++ b/test/e2e/settings-logs-api.spec.js @@ -10,7 +10,7 @@ Unless required by applicable law or agreed to in writing, software distributed const path = require("path"); const fsExtra = require("fs-extra"); -const { CliRunner, request, fixturesFolder, wait } = require("./utils"); +const { CliRunner, request, fixturesFolder, wait } = require("./support/utils"); describe("log option modified through api", () => { let cli; diff --git a/test/e2e/settings-watch-api.spec.js b/test/e2e/settings-watch-api.spec.js index f54922c..284d79a 100644 --- a/test/e2e/settings-watch-api.spec.js +++ b/test/e2e/settings-watch-api.spec.js @@ -10,7 +10,7 @@ Unless required by applicable law or agreed to in writing, software distributed const path = require("path"); const fsExtra = require("fs-extra"); -const { CliRunner, request, fixturesFolder, wait } = require("./utils"); +const { CliRunner, request, fixturesFolder, wait } = require("./support/utils"); describe("watch option modified through api", () => { let cli; diff --git a/test/e2e/stop-plugin.spec.js b/test/e2e/stop-plugin.spec.js new file mode 100644 index 0000000..0de4243 --- /dev/null +++ b/test/e2e/stop-plugin.spec.js @@ -0,0 +1,112 @@ +/* +Copyright 2020 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 { startServer, stopServer, request, fixturesFolder, wait } = require("./support/utils"); + +describe("stop plugin", () => { + let server; + beforeAll(async () => { + server = await startServer("web-tutorial"); + }); + + afterAll(async () => { + await stopServer(server); + }); + + describe("when started", () => { + it("should return current settings", async () => { + const response = await request("/admin/settings"); + expect(response).toEqual({ + behavior: "standard", + path: fixturesFolder("web-tutorial"), + delay: 0, + host: "0.0.0.0", + port: 3100, + watch: false, + log: "silly", + adminApiPath: "/admin", + adminApiDeprecatedPaths: true, + }); + }); + }); + + describe("when stopped", () => { + it("should respond not found when requesting setting", async () => { + await server._stopPlugins(); + const response = await request("/admin/settings", { + resolveWithFullResponse: true, + simple: false, + }); + expect(response.statusCode).toEqual(404); + }); + + it("should respond to mocks requests", async () => { + await server._stopPlugins(); + const response = await request("/api/users"); + expect(response).toEqual([ + { + id: 1, + name: "John Doe", + }, + { + id: 2, + name: "Jane Doe", + }, + ]); + }); + }); + + describe("when behavior is changed", () => { + it("should respond with new behavior", async () => { + server.settings.set("behavior", "user2"); + await wait(1000); + const response = await request("/api/users/2"); + expect(response).toEqual({ + id: 2, + name: "Jane Doe", + }); + }); + + it("should have not started the plugin", async () => { + await server._stopPlugins(); + const response = await request("/admin/settings", { + resolveWithFullResponse: true, + simple: false, + }); + expect(response.statusCode).toEqual(404); + }); + }); + + describe("when plugins are started", () => { + it("should respond with same behavior", async () => { + await server._startPlugins(); + const response = await request("/api/users/2"); + expect(response).toEqual({ + id: 2, + name: "Jane Doe", + }); + }); + + it("should have started the plugin", async () => { + const response = await request("/admin/settings"); + expect(response).toEqual({ + behavior: "user2", + path: fixturesFolder("web-tutorial"), + delay: 0, + host: "0.0.0.0", + port: 3100, + watch: false, + log: "silly", + adminApiPath: "/admin", + adminApiDeprecatedPaths: true, + }); + }); + }); +}); diff --git a/test/e2e/CliRunner.js b/test/e2e/support/CliRunner.js similarity index 100% rename from test/e2e/CliRunner.js rename to test/e2e/support/CliRunner.js diff --git a/test/e2e/utils.js b/test/e2e/support/utils.js similarity index 95% rename from test/e2e/utils.js rename to test/e2e/support/utils.js index 20ff1ab..e6ed274 100644 --- a/test/e2e/utils.js +++ b/test/e2e/support/utils.js @@ -14,7 +14,7 @@ const { Core } = require("@mocks-server/core"); const requestPromise = require("request-promise"); const CliRunner = require("./CliRunner"); -const PluginAdminApi = require("../../index"); +const PluginAdminApi = require("../../../index"); const SERVER_PORT = 3100; @@ -29,7 +29,7 @@ const defaultRequestOptions = { }; const fixturesFolder = (folderName) => { - return path.resolve(__dirname, "fixtures", folderName); + return path.resolve(__dirname, "..", "fixtures", folderName); }; const startServer = (mocksPath, opts = {}) => { diff --git a/test/unit/Core.mocks.js b/test/unit/Core.mocks.js index dfc7ee7..ce4a2a0 100644 --- a/test/unit/Core.mocks.js +++ b/test/unit/Core.mocks.js @@ -46,9 +46,13 @@ class CoreMock { addRouter: this._sandbox.stub(), removeRouter: this._sandbox.stub(), behaviors: { - currentFromCollection: "foo-current", + count: 0, collection: "foo-behaviors-collection", + ids: [], + current: {}, + currentId: "foo", }, + alerts: [], serverError: null, _eventEmitter: { on: this._sandbox.stub(), diff --git a/test/unit/src/Alerts.mocks.js b/test/unit/src/Alerts.mocks.js new file mode 100644 index 0000000..1016f97 --- /dev/null +++ b/test/unit/src/Alerts.mocks.js @@ -0,0 +1,41 @@ +/* +Copyright 2020 Javier Brea +Copyright 2019 XbyOrange + +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 sinon = require("sinon"); + +jest.mock("../../../src/Alerts"); + +const Alerts = require("../../../src/Alerts"); + +const Mock = class Mock { + constructor() { + this._sandbox = sinon.createSandbox(); + + this._stubs = { + init: this._sandbox.stub(), + }; + + Alerts.mockImplementation(() => this._stubs); + } + + get stubs() { + return { + Constructor: Alerts, + instance: this._stubs, + }; + } + + restore() { + this._sandbox.restore(); + } +}; + +module.exports = Mock; diff --git a/test/unit/src/Alerts.spec.js b/test/unit/src/Alerts.spec.js new file mode 100644 index 0000000..e74ff25 --- /dev/null +++ b/test/unit/src/Alerts.spec.js @@ -0,0 +1,152 @@ +/* +Copyright 2020 Javier Brea +Copyright 2019 XbyOrange + +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 express = require("express"); +const sinon = require("sinon"); +const Boom = require("@hapi/boom"); + +const LibMocks = require("../Libs.mocks"); +const CoreMocks = require("../Core.mocks"); + +const Alerts = require("../../../src/Alerts"); + +describe("Alerts", () => { + let sandbox; + let libMocks; + let coreMock; + let coreInstance; + let resMock; + let alerts; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + resMock = { + status: sandbox.stub(), + send: sandbox.stub(), + }; + libMocks = new LibMocks(); + coreMock = new CoreMocks(); + coreInstance = coreMock.stubs.instance; + alerts = new Alerts(coreInstance); + expect.assertions(1); + }); + + afterEach(() => { + sandbox.restore(); + libMocks.restore(); + coreMock.restore(); + }); + + describe("when created", () => { + it("should create an express Router", async () => { + expect(express.Router.calledOnce).toEqual(true); + }); + + it("should have added get router at /", async () => { + expect(libMocks.stubs.express.get.getCall(0).args[0]).toEqual("/"); + }); + + it("should register a get router for alert ids", async () => { + expect(libMocks.stubs.express.get.getCall(1).args[0]).toEqual("/:id"); + }); + }); + + describe("getCollection router", () => { + it("should return the alerts parsed", () => { + expect.assertions(6); + coreInstance.alerts = [ + { + context: "plugins:foo:start", + message: "Foo warning starting plugin", + }, + { + context: "plugins:foo2:stop", + message: "Foo error stopping plugin 2", + error: new Error("Foo error message"), + }, + ]; + alerts = new Alerts(coreInstance); + alerts.getCollection({}, resMock); + expect(resMock.send.getCall(0).args[0][0]).toEqual({ + id: "plugins:foo:start", + context: "plugins:foo:start", + message: "Foo warning starting plugin", + error: null, + }); + expect(resMock.send.getCall(0).args[0][1].id).toEqual("plugins:foo2:stop"); + expect(resMock.send.getCall(0).args[0][1].context).toEqual("plugins:foo2:stop"); + expect(resMock.send.getCall(0).args[0][1].message).toEqual("Foo error stopping plugin 2"); + expect(resMock.send.getCall(0).args[0][1].error.message).toEqual("Foo error message"); + expect(resMock.send.getCall(0).args[0][1].error.stack).toEqual( + expect.stringContaining("test/unit/src/Alerts.spec.js:73:18") + ); + }); + }); + + describe("getModel router", () => { + it("should return the requested alert model parsed", () => { + coreInstance.alerts = [ + { + context: "plugins:foo:start", + message: "Foo warning starting plugin", + }, + { + context: "plugins:foo2:stop", + message: "Foo error stopping plugin 2", + error: new Error("Foo error message"), + }, + ]; + alerts = new Alerts(coreInstance); + alerts.getModel( + { + params: { + id: "plugins:foo:start", + }, + }, + resMock + ); + expect(resMock.send.getCall(0).args[0]).toEqual({ + id: "plugins:foo:start", + context: "plugins:foo:start", + message: "Foo warning starting plugin", + error: null, + }); + }); + + it("should return a not found error if alert is not found", () => { + const nextStub = sandbox.stub(); + sandbox.stub(Boom, "notFound").returns("foo-error"); + coreInstance.alerts = [ + { + context: "plugins:foo:start", + message: "Foo warning starting plugin", + }, + ]; + alerts = new Alerts(coreInstance); + alerts.getModel( + { + params: { + id: "foo", + }, + }, + resMock, + nextStub + ); + expect(nextStub.getCall(0).args[0]).toEqual("foo-error"); + }); + }); + + describe("router getter", () => { + it("should return express created router", async () => { + expect(alerts.router).toEqual(libMocks.stubs.express); + }); + }); +}); diff --git a/test/unit/src/Plugin.spec.js b/test/unit/src/Plugin.spec.js index f227e7f..442e410 100644 --- a/test/unit/src/Plugin.spec.js +++ b/test/unit/src/Plugin.spec.js @@ -17,6 +17,7 @@ const CoreMocks = require("../Core.mocks"); const DeprecatedApiMocks = require("./deprecated/Api.mocks"); const SettingsMocks = require("./Settings.mocks"); const BehaviorsMocks = require("./Behaviors.mocks"); +const AlertsMocks = require("./Alerts.mocks"); const FixturesMock = require("./Fixtures.mocks"); const AboutMock = require("./About.mocks"); @@ -27,6 +28,7 @@ describe("Plugin", () => { let libMocks; let settingsMocks; let behaviorsMocks; + let alertsMocks; let fixturesMock; let deprecatedApiMock; let deprecatedApiInstance; @@ -40,6 +42,7 @@ describe("Plugin", () => { libMocks = new LibMocks(); settingsMocks = new SettingsMocks(); behaviorsMocks = new BehaviorsMocks(); + alertsMocks = new AlertsMocks(); aboutMock = new AboutMock(); fixturesMock = new FixturesMock(); deprecatedApiMock = new DeprecatedApiMocks(); @@ -55,6 +58,7 @@ describe("Plugin", () => { libMocks.restore(); settingsMocks.restore(); behaviorsMocks.restore(); + alertsMocks.restore(); fixturesMock.restore(); coreMock.restore(); aboutMock.restore(); @@ -87,39 +91,80 @@ describe("Plugin", () => { await plugin.init(); expect(deprecatedApiInstance.init.callCount).toEqual(1); }); + }); + describe("when started", () => { it("should add deprecated router if adminApiDeprecatedPaths setting returns true", async () => { coreInstance.settings.get.withArgs("adminApiDeprecatedPaths").returns(true); await plugin.init(); + plugin.start(); expect(coreInstance.addRouter.callCount).toEqual(2); }); it("should not add deprecated router if adminApiDeprecatedPaths setting returns false", async () => { coreInstance.settings.get.withArgs("adminApiDeprecatedPaths").returns(false); await plugin.init(); + plugin.start(); expect(coreInstance.addRouter.callCount).toEqual(1); }); it("should register Express router into the core under the path returned by settings", async () => { coreInstance.settings.get.withArgs("adminApiPath").returns("/foo"); await plugin.init(); + plugin.start(); expect(coreInstance.addRouter.getCall(0).args).toEqual(["/foo", plugin._router]); }); }); + describe("when stopped", () => { + it("should remove deprecated router if it was started", async () => { + expect.assertions(2); + coreInstance.settings.get.withArgs("adminApiDeprecatedPaths").returns(true); + await plugin.init(); + plugin.start(); + plugin.stop(); + expect(coreInstance.removeRouter.callCount).toEqual(1); + expect(coreInstance.removeRouter.getCall(0).args[0]).toEqual("/mocks"); + }); + + it("should remove router if it was started", async () => { + expect.assertions(2); + coreInstance.settings.get.withArgs("adminApiPath").returns("/admin"); + await plugin.init(); + plugin.start(); + plugin.stop(); + expect(coreInstance.removeRouter.callCount).toEqual(1); + expect(coreInstance.removeRouter.getCall(0).args[0]).toEqual("/admin"); + }); + + it("should stop listening to onChangeSettings events", async () => { + const removeListenerSpy = sandbox.spy(); + coreInstance.onChangeSettings.returns(removeListenerSpy); + coreInstance.settings.get.withArgs("adminApiPath").returns("/admin"); + await plugin.init(); + plugin.start(); + plugin.stop(); + expect(removeListenerSpy.callCount).toEqual(1); + }); + }); + describe("when settings change", () => { - it("should not register deprecated router again if it is enabled and was already registered", async () => { + it("should remove deprecated router and add it again if it is enabled and was already registered", async () => { + expect.assertions(2); coreInstance.settings.get.withArgs("adminApiDeprecatedPaths").returns(true); await plugin.init(); + plugin.start(); coreInstance.onChangeSettings.getCall(0).args[0]({ adminApiDeprecatedPaths: true, }); - expect(coreInstance.addRouter.callCount).toEqual(2); + expect(coreInstance.removeRouter.callCount).toEqual(1); + expect(coreInstance.addRouter.callCount).toEqual(3); }); it("should remove deprecated router if it is disabled and was already enabled", async () => { coreInstance.settings.get.withArgs("adminApiDeprecatedPaths").returns(true); await plugin.init(); + plugin.start(); coreInstance.settings.get.withArgs("adminApiDeprecatedPaths").returns(false); coreInstance.onChangeSettings.getCall(0).args[0]({ adminApiDeprecatedPaths: false, @@ -130,6 +175,7 @@ describe("Plugin", () => { it("should not remove deprecated router if it was not added", async () => { coreInstance.settings.get.withArgs("adminApiDeprecatedPaths").returns(false); await plugin.init(); + plugin.start(); coreInstance.settings.get.withArgs("adminApiDeprecatedPaths").returns(false); coreInstance.onChangeSettings.getCall(0).args[0]({ adminApiDeprecatedPaths: false, @@ -141,6 +187,7 @@ describe("Plugin", () => { expect.assertions(3); coreInstance.settings.get.withArgs("adminApiPath").returns("/foo"); await plugin.init(); + plugin.start(); coreInstance.settings.get.withArgs("adminApiPath").returns("/foo2"); coreInstance.onChangeSettings.getCall(0).args[0]({ adminApiPath: "/foo2",