From 81fc23ed5cf4ad620f6bab11efa23fd9e52aeb76 Mon Sep 17 00:00:00 2001 From: Nicolas Froidure Date: Fri, 15 Feb 2019 16:15:55 +0100 Subject: [PATCH] feat(Authentication): Add authentication wrapper concerns #18 --- package-lock.json | 47 +- packages/create-whook/package.json | 3 + .../src/__snapshots__/index.test.js.snap | 454 ++++++++++++++++++ .../create-whook/src/config/common/config.js | 1 + .../src/handlers/getDiagnostic.js | 23 +- packages/create-whook/src/handlers/getPing.js | 2 +- packages/create-whook/src/handlers/putEcho.js | 2 +- packages/create-whook/src/index.test.js | 143 ++++++ packages/create-whook/src/services/API.js | 62 ++- .../create-whook/src/services/API.test.js | 27 +- .../create-whook/src/services/MECHANISMS.js | 41 ++ .../src/services/MECHANISMS.test.js | 38 ++ .../create-whook/src/services/WRAPPERS.js | 5 +- .../services/__snapshots__/API.test.js.snap | 58 ++- .../__snapshots__/MECHANISMS.test.js.snap | 27 ++ .../__snapshots__/authentication.test.js.snap | 37 ++ .../src/services/authentication.js | 27 ++ .../src/services/authentication.test.js | 63 +++ packages/whook-authorization/API.md | 55 +++ packages/whook-authorization/LICENSE | 20 + packages/whook-authorization/README.md | 82 ++++ packages/whook-authorization/package.json | 140 ++++++ .../src/__snapshots__/index.test.js.snap | 273 +++++++++++ packages/whook-authorization/src/index.js | 168 +++++++ .../whook-authorization/src/index.test.js | 421 ++++++++++++++++ packages/whook/src/index.test.js | 1 - 26 files changed, 2151 insertions(+), 69 deletions(-) create mode 100644 packages/create-whook/src/__snapshots__/index.test.js.snap create mode 100644 packages/create-whook/src/index.test.js create mode 100644 packages/create-whook/src/services/MECHANISMS.js create mode 100644 packages/create-whook/src/services/MECHANISMS.test.js create mode 100644 packages/create-whook/src/services/__snapshots__/MECHANISMS.test.js.snap create mode 100644 packages/create-whook/src/services/__snapshots__/authentication.test.js.snap create mode 100644 packages/create-whook/src/services/authentication.js create mode 100644 packages/create-whook/src/services/authentication.test.js create mode 100644 packages/whook-authorization/API.md create mode 100644 packages/whook-authorization/LICENSE create mode 100644 packages/whook-authorization/README.md create mode 100644 packages/whook-authorization/package.json create mode 100644 packages/whook-authorization/src/__snapshots__/index.test.js.snap create mode 100644 packages/whook-authorization/src/index.js create mode 100644 packages/whook-authorization/src/index.test.js diff --git a/package-lock.json b/package-lock.json index b8610de9..dd81f3d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1915,15 +1915,6 @@ "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", "dev": true }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, "debuglog": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", @@ -2358,6 +2349,15 @@ "to-regex": "^3.0.1" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", @@ -3480,6 +3480,17 @@ "requires": { "pkg-dir": "^2.0.0", "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + } } }, "imurmurhash": { @@ -5213,15 +5224,6 @@ "pinkie": "^2.0.0" } }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -5805,6 +5807,15 @@ "use": "^3.1.0" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", diff --git a/packages/create-whook/package.json b/packages/create-whook/package.json index 1fd336b4..3ef224da 100644 --- a/packages/create-whook/package.json +++ b/packages/create-whook/package.json @@ -57,12 +57,15 @@ "ecstatic": "^3.3.0", "knifecycle": "^5.2.0", "swagger-http-router": "^4.2.1", + "http-auth-utils": "^2.1.0", "whook": "^3.1.3", "whook-cli": "^0.0.0", "whook-swagger-ui": "^0.0.0", + "whook-authorization": "^0.0.0", "whook-cors": "^0.0.0" }, "devDependencies": { + "axios": "^0.18.0", "@babel/cli": "^7.2.3", "@babel/core": "^7.2.2", "@babel/node": "^7.2.2", diff --git a/packages/create-whook/src/__snapshots__/index.test.js.snap b/packages/create-whook/src/__snapshots__/index.test.js.snap new file mode 100644 index 00000000..e8aa751b --- /dev/null +++ b/packages/create-whook/src/__snapshots__/index.test.js.snap @@ -0,0 +1,454 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`runServer should authenticate users 1`] = ` +Object { + "data": Object { + "transactions": Object { + "2": Object { + "errored": false, + "id": "2", + "ip": "127.0.0.1", + "method": "GET", + "protocol": "http", + "reqHeaders": Object { + "accept": "application/json, text/plain, */*", + "accept-encoding": "gzip, deflate", + "accept-language": "en", + "authorization": "Fake 1-admin", + "connection": "close", + "host": "localhost:9999", + "origin": "http://localhost", + "referer": "http://localhost/", + "user-agent": "Mozilla/5.0 (linux) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/11.12.0", + }, + "startInBytes": 331, + "startOutBytes": 0, + "startTime": 1390694400000, + "url": "/v1/diag", + }, + }, + }, + "debugCalls": Array [ + Array [ + "Created a delay:", + 30000, + ], + Array [ + "Cleared a delay", + ], + Array [ + "Created a delay:", + 30000, + ], + Array [ + "Cleared a delay", + ], + ], + "headers": Object { + "content-type": "application/json", + "date": undefined, + }, + "logErrorCalls": Array [], + "logInfoCalls": Array [ + Array [ + Object { + "endInBytes": 284, + "endOutBytes": 377, + "endTime": 1390694400000, + "errored": false, + "id": "1", + "ip": "127.0.0.1", + "method": "OPTIONS", + "protocol": "http", + "reqHeaders": Object { + "access-control-request-headers": "authorization", + "access-control-request-method": "GET", + "connection": "close", + "content-length": "0", + "host": "localhost:9999", + "origin": "http://localhost", + "user-agent": "Mozilla/5.0 (linux) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/11.12.0", + }, + "resHeaders": Object { + "Access-Control-Allow-Headers": "Accept,Accept-Encoding,Accept-Language,Referrer,Content-Type,Content-Encoding,Authorization,Keep-Alive,User-Agent", + "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", + "Access-Control-Allow-Origin": "*", + "Vary": "Origin", + }, + "startInBytes": 284, + "startOutBytes": 0, + "startTime": 1390694400000, + "statusCode": 200, + "url": "/v1/diag", + }, + ], + Array [ + Object { + "endInBytes": 331, + "endOutBytes": 1001, + "endTime": 1390694400000, + "errored": false, + "id": "2", + "ip": "127.0.0.1", + "method": "GET", + "protocol": "http", + "reqHeaders": Object { + "accept": "application/json, text/plain, */*", + "accept-encoding": "gzip, deflate", + "accept-language": "en", + "authorization": "Fake 1-admin", + "connection": "close", + "host": "localhost:9999", + "origin": "http://localhost", + "referer": "http://localhost/", + "user-agent": "Mozilla/5.0 (linux) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/11.12.0", + }, + "resHeaders": Object { + "Access-Control-Allow-Headers": "Accept,Accept-Encoding,Accept-Language,Referrer,Content-Type,Content-Encoding,Authorization,Keep-Alive,User-Agent", + "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", + "Access-Control-Allow-Origin": "*", + "Vary": "Origin", + "X-Authenticated": "{\\"hash\\":\\"1-admin\\",\\"userId\\":1,\\"scopes\\":[\\"admin\\"]}", + "content-type": "application/json", + }, + "startInBytes": 331, + "startOutBytes": 0, + "startTime": 1390694400000, + "statusCode": 200, + "url": "/v1/diag", + }, + ], + ], + "status": 200, +} +`; + +exports[`runServer should fail with bad fake tokens 1`] = ` +Object { + "debugCalls": Array [ + Array [ + "Created a delay:", + 30000, + ], + Array [ + "Cleared a delay", + ], + Array [ + "Created a delay:", + 30000, + ], + Array [ + "Cleared a delay", + ], + ], + "errorCode": undefined, + "errorParams": undefined, + "httpCode": undefined, + "logErrorCalls": Array [ + Array [ + "An error occured", + Object { + "code": "E_INVALID_FAKE_TOKEN", + "details": Array [], + "guruMeditation": "4", + "request": "http://localhost:9999/v1/diag", + "stack": "YHTTPError[400]: E_INVALID_FAKE_TOKEN () + at Object.parseAuthorizationRest (/home/nfroidure/nfroidure/whook/packages/create-whook/src/services/MECHANISMS.js:18:13) + at /home/nfroidure/nfroidure/whook/node_modules/http-auth-utils/dist/index.js:134:29 + at Array.some () + at parseAuthorizationHeader (/home/nfroidure/nfroidure/whook/node_modules/http-auth-utils/dist/index.js:130:18) + at handleWithAuthorization (/home/nfroidure/nfroidure/whook/packages/whook-authorization/dist/index.js:82:29) + at handler (/home/nfroidure/nfroidure/whook/packages/whook-cors/dist/index.js:37:28) + at executeHandler (/home/nfroidure/nfroidure/whook/node_modules/swagger-http-router/dist/lib.js:87:27) + at + at process._tickCallback (internal/process/next_tick.js:189:7)", + "status": 400, + "verb": "GET", + }, + ], + Array [ + "Undocumented response:", + 400, + Object { + "consumes": Array [], + "method": "get", + "operationId": "getDiagnostic", + "parameters": Array [ + Object { + "in": "header", + "name": "authorization", + "type": "string", + }, + Object { + "in": "query", + "name": "access_token", + "type": "string", + }, + ], + "path": "/diag", + "produces": Array [ + "application/json", + ], + "responses": Object { + "200": Object { + "description": "Diagnostic", + "schema": Object { + "additionalProperties": true, + "type": "object", + }, + }, + }, + "security": Object { + "fakeAuth": Array [ + "admin", + ], + }, + "summary": "Checks API's health.", + "tags": Array [ + "system", + ], + }, + ], + ], + "logInfoCalls": Array [ + Array [ + Object { + "endInBytes": 284, + "endOutBytes": 377, + "endTime": 1390694400000, + "errored": false, + "id": "3", + "ip": "127.0.0.1", + "method": "OPTIONS", + "protocol": "http", + "reqHeaders": Object { + "access-control-request-headers": "authorization", + "access-control-request-method": "GET", + "connection": "close", + "content-length": "0", + "host": "localhost:9999", + "origin": "http://localhost", + "user-agent": "Mozilla/5.0 (linux) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/11.12.0", + }, + "resHeaders": Object { + "Access-Control-Allow-Headers": "Accept,Accept-Encoding,Accept-Language,Referrer,Content-Type,Content-Encoding,Authorization,Keep-Alive,User-Agent", + "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", + "Access-Control-Allow-Origin": "*", + "Vary": "Origin", + }, + "startInBytes": 284, + "startOutBytes": 0, + "startTime": 1390694400000, + "statusCode": 200, + "url": "/v1/diag", + }, + ], + Array [ + Object { + "endInBytes": 331, + "endOutBytes": 510, + "endTime": 1390694400000, + "errored": true, + "id": "4", + "ip": "127.0.0.1", + "method": "GET", + "protocol": "http", + "reqHeaders": Object { + "accept": "application/json, text/plain, */*", + "accept-encoding": "gzip, deflate", + "accept-language": "en", + "authorization": "Fake e-admin", + "connection": "close", + "host": "localhost:9999", + "origin": "http://localhost", + "referer": "http://localhost/", + "user-agent": "Mozilla/5.0 (linux) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/11.12.0", + }, + "resHeaders": Object { + "Access-Control-Allow-Headers": "Accept,Accept-Encoding,Accept-Language,Referrer,Content-Type,Content-Encoding,Authorization,Keep-Alive,User-Agent", + "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", + "Access-Control-Allow-Origin": "*", + "Vary": "Origin", + "cache-control": "private", + "content-type": "application/json", + }, + "startInBytes": 331, + "startOutBytes": 0, + "startTime": 1390694400000, + "statusCode": 400, + "url": "/v1/diag", + }, + ], + ], +} +`; + +exports[`runServer should ping 1`] = ` +Object { + "data": Object { + "pong": "pong", + }, + "debugCalls": Array [ + Array [ + "Created a delay:", + 30000, + ], + Array [ + "Cleared a delay", + ], + ], + "headers": Object { + "content-type": "application/json", + "date": undefined, + }, + "logErrorCalls": Array [], + "logInfoCalls": Array [ + Array [ + Object { + "endInBytes": 302, + "endOutBytes": 447, + "endTime": 1390694400000, + "errored": false, + "id": "0", + "ip": "127.0.0.1", + "method": "GET", + "protocol": "http", + "reqHeaders": Object { + "accept": "application/json, text/plain, */*", + "accept-encoding": "gzip, deflate", + "accept-language": "en", + "connection": "close", + "host": "localhost:9999", + "origin": "http://localhost", + "referer": "http://localhost/", + "user-agent": "Mozilla/5.0 (linux) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/11.12.0", + }, + "resHeaders": Object { + "Access-Control-Allow-Headers": "Accept,Accept-Encoding,Accept-Language,Referrer,Content-Type,Content-Encoding,Authorization,Keep-Alive,User-Agent", + "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", + "Access-Control-Allow-Origin": "*", + "Vary": "Origin", + "X-Node-ENV": "test", + "content-type": "application/json", + }, + "startInBytes": 302, + "startOutBytes": 0, + "startTime": 1390694400000, + "statusCode": 200, + "url": "/v1/ping", + }, + ], + ], + "status": 200, +} +`; + +exports[`runServer should work 1`] = ` +Object { + "debugCalls": Array [ + Array [ + "Logging service initialized.", + ], + Array [ + "Delay service initialized.", + ], + Array [ + "πŸ€– - Initializing the \`$autoload\` service.", + ], + Array [ + "🏭 - Initializing the CONFIGS service.", + ], + Array [ + "πŸ’Ώ - Loading API initializer from /home/whoiam/projects/whook/packages/create-whook/src/services/API.", + ], + Array [ + "Running in \\"test\\" environment.", + ], + Array [ + "Process service initialized.", + ], + Array [ + "HTTP Transaction initialized.", + ], + Array [ + "πŸ¦„ - Initializing the API service!", + ], + Array [ + "πŸ’Ώ - Loading getOpenAPIWrapped initializer from /home/whoiam/projects/whook/packages/create-whook/src/handlers/getOpenAPI.", + ], + Array [ + "πŸ’Ώ - Loading optionsWithCORSWrapped initializer from /home/whoiam/projects/whook/packages/create-whook/src/handlers/optionsWithCORS.", + ], + Array [ + "πŸ’Ώ - Loading getPingWrapped initializer from /home/whoiam/projects/whook/packages/create-whook/src/handlers/getPing.", + ], + Array [ + "πŸ’Ώ - Loading getDelayWrapped initializer from /home/whoiam/projects/whook/packages/create-whook/src/handlers/getDelay.", + ], + Array [ + "πŸ’Ώ - Loading getDiagnosticWrapped initializer from /home/whoiam/projects/whook/packages/create-whook/src/handlers/getDiagnostic.", + ], + Array [ + "πŸ’Ώ - Loading getTimeWrapped initializer from /home/whoiam/projects/whook/packages/create-whook/src/handlers/getTime.", + ], + Array [ + "πŸ’Ώ - Loading putEchoWrapped initializer from /home/whoiam/projects/whook/packages/create-whook/src/handlers/putEcho.", + ], + Array [ + "πŸ’Ώ - Loading WRAPPERS initializer from /home/whoiam/projects/whook/packages/create-whook/src/services/WRAPPERS.", + ], + Array [ + "πŸ’Ώ - Loading MECHANISMS initializer from /home/whoiam/projects/whook/packages/create-whook/src/services/MECHANISMS.", + ], + Array [ + "πŸ’Ώ - Loading authentication initializer from /home/whoiam/projects/whook/packages/create-whook/src/services/authentication.", + ], + Array [ + "πŸ”§ - Initializing auth mechanisms", + ], + Array [ + "πŸ” - Initializing the authentication wrapper.", + ], + Array [ + "πŸ” - Initializing the authentication wrapper.", + ], + Array [ + "πŸ” - Initializing the authentication wrapper.", + ], + Array [ + "πŸ” - Initializing the authentication wrapper.", + ], + Array [ + "πŸ” - Initializing the authentication wrapper.", + ], + Array [ + "πŸ” - Initializing the authentication wrapper.", + ], + Array [ + "πŸ” - Initializing the authentication wrapper.", + ], + Array [ + "HTTP Router initialized.", + ], + ], + "logErrorCalls": Array [ + Array [ + "⚑ - Loading configurations from /home/whoiam/projects/whook/packages/create-whook/src/config/test/config.", + ], + Array [ + "⚠️ - Using fake auth mechanism!", + ], + Array [ + "On air πŸš€πŸŒ•", + ], + ], + "logInfoCalls": Array [ + Array [ + "πŸ’ - Serving the API docs: http://localhost:9999/docs?url=http%3A%2F%2Flocalhost%3A9999%2Fv1%2FopenAPI", + ], + Array [ + "HTTP Server listening at \\"http://localhost:9999\\".", + ], + ], +} +`; diff --git a/packages/create-whook/src/config/common/config.js b/packages/create-whook/src/config/common/config.js index 71902f24..eb5cdc12 100644 --- a/packages/create-whook/src/config/common/config.js +++ b/packages/create-whook/src/config/common/config.js @@ -14,6 +14,7 @@ const CONFIG = { }, NODE_ENVS, DEBUG_NODE_ENVS: process.env.DEBUG ? NODE_ENVS : DEBUG_NODE_ENVS, + TOKEN: 'oudelali', CORS: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS', diff --git a/packages/create-whook/src/handlers/getDiagnostic.js b/packages/create-whook/src/handlers/getDiagnostic.js index 6494c8a4..3ae621d7 100644 --- a/packages/create-whook/src/handlers/getDiagnostic.js +++ b/packages/create-whook/src/handlers/getDiagnostic.js @@ -6,21 +6,30 @@ export const definition = { operation: { operationId: 'getDiagnostic', summary: "Checks API's health.", + security: { + bearerAuth: ['admin'], + }, tags: ['system'], consumes: [], produces: ['application/json'], + parameters: [ + { + in: 'header', + name: 'authorization', + type: 'string', + }, + { + in: 'query', + name: 'access_token', + type: 'string', + }, + ], responses: { 200: { description: 'Diagnostic', schema: { type: 'object', - properties: { - additionalProperties: false, - pong: { - type: 'string', - enum: ['pong'], - }, - }, + additionalProperties: true, }, }, }, diff --git a/packages/create-whook/src/handlers/getPing.js b/packages/create-whook/src/handlers/getPing.js index 5d4af680..6d4f8752 100644 --- a/packages/create-whook/src/handlers/getPing.js +++ b/packages/create-whook/src/handlers/getPing.js @@ -14,8 +14,8 @@ export const definition = { description: 'Pong', schema: { type: 'object', + additionalProperties: false, properties: { - additionalProperties: false, pong: { type: 'string', enum: ['pong'], diff --git a/packages/create-whook/src/handlers/putEcho.js b/packages/create-whook/src/handlers/putEcho.js index 13832ae1..6af50f30 100644 --- a/packages/create-whook/src/handlers/putEcho.js +++ b/packages/create-whook/src/handlers/putEcho.js @@ -3,8 +3,8 @@ import { autoHandler } from 'knifecycle'; const echoSchema = { type: 'object', required: ['echo'], + additionalProperties: false, properties: { - additionalProperties: false, echo: { type: 'string', }, diff --git a/packages/create-whook/src/index.test.js b/packages/create-whook/src/index.test.js new file mode 100644 index 00000000..8b43066b --- /dev/null +++ b/packages/create-whook/src/index.test.js @@ -0,0 +1,143 @@ +import { constant } from 'knifecycle'; +import { + runServer, + prepareServer, + prepareEnvironment as basePrepareEnvironment, +} from './index'; +import axios from 'axios'; +import YError from 'yerror'; + +describe('runServer', () => { + const logger = { + info: jest.fn(), + error: jest.fn(), + }; + const debug = jest.fn(); + const time = jest.fn(); + const PORT = 9999; + const HOST = 'localhost'; + + async function prepareEnvironment() { + const $ = await basePrepareEnvironment(); + + $.register(constant('ENV', {})); + $.register(constant('PORT', PORT)); + $.register(constant('HOST', HOST)); + $.register(constant('NODE_ENV', 'test')); + $.register(constant('DEBUG_NODE_ENVS', ['test'])); + $.register(constant('NODE_ENVS', ['test'])); + $.register(constant('time', time)); + $.register(constant('logger', logger)); + $.register(constant('debug', debug)); + + return $; + } + process.env.ISOLATED_ENV = 1; + + let $destroy; + + beforeAll(async () => { + const { $destroy: _destroy } = await runServer( + prepareEnvironment, + prepareServer, + ['$destroy', 'httpServer', 'process'], + ); + + $destroy = _destroy; + }, 15000); + + afterAll(async () => { + await $destroy(); + }, 15000); + + afterEach(() => { + time.mockReset(); + debug.mockReset(); + logger.info.mockReset(); + logger.error.mockReset(); + }); + + it('should work', async () => { + expect({ + debugCalls: debug.mock.calls.map(filterPaths), + logInfoCalls: logger.info.mock.calls.map(filterPaths), + logErrorCalls: logger.error.mock.calls.map(filterPaths), + }).toMatchSnapshot(); + }); + + it('should ping', async () => { + time.mockReturnValue(new Date('2014-01-26T00:00:00.000Z').getTime()); + const { status, headers, data } = await axios({ + method: 'get', + url: `http://${HOST}:${PORT}/v1/ping`, + }); + + expect({ + status, + headers: { + ...headers, + // Erasing the Date header that may be added by Axios :/ + date: {}.undef, + }, + data, + debugCalls: debug.mock.calls.map(filterPaths), + logInfoCalls: logger.info.mock.calls.map(filterPaths), + logErrorCalls: logger.error.mock.calls.map(filterPaths), + }).toMatchSnapshot(); + }); + + it('should authenticate users', async () => { + time.mockReturnValue(new Date('2014-01-26T00:00:00.000Z').getTime()); + const { status, headers, data } = await axios({ + method: 'get', + url: `http://${HOST}:${PORT}/v1/diag`, + headers: { + authorization: `Fake 1-admin`, + }, + }); + + expect({ + status, + headers: { + ...headers, + // Erasing the Date header that may be added by Axios :/ + date: {}.undef, + }, + data, + debugCalls: debug.mock.calls.map(filterPaths), + logInfoCalls: logger.info.mock.calls.map(filterPaths), + logErrorCalls: logger.error.mock.calls.map(filterPaths), + }).toMatchSnapshot(); + }); + + it('should fail with bad fake tokens', async () => { + time.mockReturnValue(new Date('2014-01-26T00:00:00.000Z').getTime()); + try { + await axios({ + method: 'get', + url: `http://${HOST}:${PORT}/v1/diag`, + headers: { + authorization: `Fake e-admin`, + }, + }); + throw new YError('E_UNEXPECTED_SUCCESS'); + } catch (err) { + expect({ + httpCode: err.httpCode, + errorCode: err.code, + errorParams: err.params, + debugCalls: debug.mock.calls.map(filterPaths), + logInfoCalls: logger.info.mock.calls.map(filterPaths), + logErrorCalls: logger.error.mock.calls.map(filterPaths), + }).toMatchSnapshot(); + } + }); +}); + +function filterPaths(strs) { + return strs.map(str => + 'string' !== typeof str + ? str + : str.replace(/ (\/[^/]+){1,}\/whook/g, ' /home/whoiam/projects/whook'), + ); +} diff --git a/packages/create-whook/src/services/API.js b/packages/create-whook/src/services/API.js index 8715d09a..c3a6d0c3 100644 --- a/packages/create-whook/src/services/API.js +++ b/packages/create-whook/src/services/API.js @@ -12,16 +12,20 @@ export default name('API', autoService(initAPI)); // The API service is where you put your handlers // altogether to form the final API -async function initAPI({ CONFIG, log }) { +async function initAPI({ DEBUG_NODE_ENVS, NODE_ENV, CONFIG, log }) { log('debug', 'πŸ¦„ - Initializing the API service!'); + const debugging = DEBUG_NODE_ENVS.includes(NODE_ENV); + const API = { host: CONFIG.host, basePath: CONFIG.basePath, schemes: CONFIG.schemes, securityDefinitions: { - basicAuth: { - type: 'basic', + bearerAuth: { + type: 'apiKey', + in: 'query', + name: 'access_token', }, }, swagger: '2.0', @@ -30,26 +34,38 @@ async function initAPI({ CONFIG, log }) { title: CONFIG.name, description: CONFIG.description, }, - paths: { - [getOpenAPIDefinition.path]: { - [getOpenAPIDefinition.method]: getOpenAPIDefinition.operation, - }, - [getPingDefinition.path]: { - [getPingDefinition.method]: getPingDefinition.operation, - }, - [getDelayDefinition.path]: { - [getDelayDefinition.method]: getDelayDefinition.operation, - }, - [getDiagnosticDefinition.path]: { - [getDiagnosticDefinition.method]: getDiagnosticDefinition.operation, - }, - [getTimeDefinition.path]: { - [getTimeDefinition.method]: getTimeDefinition.operation, - }, - [putEchoDefinition.path]: { - [putEchoDefinition.method]: putEchoDefinition.operation, - }, - }, + paths: [ + getOpenAPIDefinition, + getPingDefinition, + getDelayDefinition, + getDiagnosticDefinition, + getTimeDefinition, + putEchoDefinition, + ] + .map(definition => + debugging && definition.operation.security + ? { + ...definition, + operation: { + ...definition.operation, + security: { + ...definition.security, + fakeAuth: ['admin'], + }, + }, + } + : definition, + ) + .reduce( + (paths, definition) => ({ + ...paths, + [definition.path]: { + ...(paths[definition.path] || {}), + [definition.method]: definition.operation, + }, + }), + {}, + ), }; // You can apply transformations to your API like diff --git a/packages/create-whook/src/services/API.test.js b/packages/create-whook/src/services/API.test.js index e5fbf6d5..141ca93c 100644 --- a/packages/create-whook/src/services/API.test.js +++ b/packages/create-whook/src/services/API.test.js @@ -1,8 +1,12 @@ import initAPI from './API'; import FULL_CONFIG from '../config/test/config'; +import { + getSwaggerOperations, + flattenSwagger, +} from 'swagger-http-router/dist/utils'; describe('API', () => { - const { CONFIG } = FULL_CONFIG; + const { CONFIG, NODE_ENV, DEBUG_NODE_ENVS } = FULL_CONFIG; const log = jest.fn(); beforeEach(() => { @@ -13,6 +17,8 @@ describe('API', () => { const API = await initAPI({ log, CONFIG, + NODE_ENV, + DEBUG_NODE_ENVS, }); expect({ @@ -20,4 +26,23 @@ describe('API', () => { logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), }).toMatchSnapshot(); }); + + it('should always have the same amount of public endpoints', async () => { + const API = await initAPI({ + log, + CONFIG, + NODE_ENV, + DEBUG_NODE_ENVS, + }); + const operations = await getSwaggerOperations(await flattenSwagger(API)); + + expect( + operations + .filter( + operation => !operation['x-whook'] || !operation['x-whook'].private, + ) + .map(({ method, path }) => `${method} ${path}`) + .sort(), + ).toMatchSnapshot(); + }); }); diff --git a/packages/create-whook/src/services/MECHANISMS.js b/packages/create-whook/src/services/MECHANISMS.js new file mode 100644 index 00000000..07e882ed --- /dev/null +++ b/packages/create-whook/src/services/MECHANISMS.js @@ -0,0 +1,41 @@ +import HTTPError from 'yhttperror'; +import { name, autoService } from 'knifecycle'; +import { BEARER as BEARER_MECHANISM } from 'http-auth-utils'; + +export const FAKE_MECHANISM = { + type: 'Fake', + parseAuthorizationRest: rest => { + let userId; + let scopes; + + rest.replace(/^(\d+)-((\w+,)*(\w+){1})$/, (_, rawUserId, rawScopes) => { + userId = parseInt(rawUserId); + scopes = rawScopes.split(); + return ''; + }); + + if ('undefined' === typeof userId || 'undefined' === typeof scopes) { + throw new HTTPError(400, 'E_INVALID_FAKE_TOKEN'); + } + + return { + hash: rest, + userId, + scopes, + }; + }, +}; + +export default name('MECHANISMS', autoService(initMechanisms)); + +async function initMechanisms({ DEBUG_NODE_ENVS, NODE_ENV, log }) { + log('debug', 'πŸ”§ - Initializing auth mechanisms'); + + const debugging = DEBUG_NODE_ENVS.includes(NODE_ENV); + const MECHANISMS = [BEARER_MECHANISM, ...(debugging ? [FAKE_MECHANISM] : [])]; + + if (debugging) { + log('warning', '⚠️ - Using fake auth mechanism!'); + } + return MECHANISMS; +} diff --git a/packages/create-whook/src/services/MECHANISMS.test.js b/packages/create-whook/src/services/MECHANISMS.test.js new file mode 100644 index 00000000..15c15633 --- /dev/null +++ b/packages/create-whook/src/services/MECHANISMS.test.js @@ -0,0 +1,38 @@ +import initMechanisms, { FAKE_MECHANISM } from './MECHANISMS'; +import { BEARER as BEARER_MECHANISM } from 'http-auth-utils'; + +describe('NECHANISMS', () => { + const log = jest.fn(); + + beforeEach(() => { + log.mockReset(); + }); + + it('should only include bearer', async () => { + const MECHANISMS = await initMechanisms({ + NODE_ENV: 'production', + DEBUG_NODE_ENVS: ['test', 'development'], + log, + }); + + expect(MECHANISMS).toEqual([BEARER_MECHANISM]); + + expect({ + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + + it('should also include fake on debugging', async () => { + const MECHANISMS = await initMechanisms({ + NODE_ENV: 'development', + DEBUG_NODE_ENVS: ['test', 'development'], + log, + }); + + expect(MECHANISMS).toEqual([BEARER_MECHANISM, FAKE_MECHANISM]); + + expect({ + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); +}); diff --git a/packages/create-whook/src/services/WRAPPERS.js b/packages/create-whook/src/services/WRAPPERS.js index 5c414a5f..56a3aa42 100644 --- a/packages/create-whook/src/services/WRAPPERS.js +++ b/packages/create-whook/src/services/WRAPPERS.js @@ -1,13 +1,14 @@ import { service } from 'knifecycle'; import { wrapHandlerWithCORS } from 'whook-cors'; +import { wrapHandlerWithAuthorization } from 'whook-authorization'; export default service(initWrappers, 'WRAPPERS'); // Wrappers are allowing you to override every // handlers of your API with specific behaviors, -// here we add CORS support +// here we add CORS and HTTP authorization support async function initWrappers() { - const WRAPPERS = [wrapHandlerWithCORS]; + const WRAPPERS = [wrapHandlerWithCORS, wrapHandlerWithAuthorization]; return WRAPPERS; } diff --git a/packages/create-whook/src/services/__snapshots__/API.test.js.snap b/packages/create-whook/src/services/__snapshots__/API.test.js.snap index 712a0120..4efecdd6 100644 --- a/packages/create-whook/src/services/__snapshots__/API.test.js.snap +++ b/packages/create-whook/src/services/__snapshots__/API.test.js.snap @@ -1,5 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`API should always have the same amount of public endpoints 1`] = ` +Array [ + "get /delay", + "get /diag", + "get /openAPI", + "get /ping", + "get /time", + "put /echo", +] +`; + exports[`API should work 1`] = ` Object { "API": Object { @@ -71,6 +82,18 @@ Object { "get": Object { "consumes": Array [], "operationId": "getDiagnostic", + "parameters": Array [ + Object { + "in": "header", + "name": "authorization", + "type": "string", + }, + Object { + "in": "query", + "name": "access_token", + "type": "string", + }, + ], "produces": Array [ "application/json", ], @@ -78,19 +101,16 @@ Object { "200": Object { "description": "Diagnostic", "schema": Object { - "properties": Object { - "additionalProperties": false, - "pong": Object { - "enum": Array [ - "pong", - ], - "type": "string", - }, - }, + "additionalProperties": true, "type": "object", }, }, }, + "security": Object { + "bearerAuth": Array [ + "admin", + ], + }, "summary": "Checks API's health.", "tags": Array [ "system", @@ -99,7 +119,13 @@ Object { "options": Object { "consumes": Array [], "operationId": "optionsWithCORS", - "parameters": Array [], + "parameters": Array [ + Object { + "in": "query", + "name": "access_token", + "type": "string", + }, + ], "produces": Array [], "responses": Object { "200": Object { @@ -154,8 +180,8 @@ Object { "in": "body", "name": "body", "schema": Object { + "additionalProperties": false, "properties": Object { - "additionalProperties": false, "echo": Object { "type": "string", }, @@ -174,8 +200,8 @@ Object { "200": Object { "description": "The actual echo", "schema": Object { + "additionalProperties": false, "properties": Object { - "additionalProperties": false, "echo": Object { "type": "string", }, @@ -249,8 +275,8 @@ Object { "200": Object { "description": "Pong", "schema": Object { + "additionalProperties": false, "properties": Object { - "additionalProperties": false, "pong": Object { "enum": Array [ "pong", @@ -343,8 +369,10 @@ Object { "http", ], "securityDefinitions": Object { - "basicAuth": Object { - "type": "basic", + "bearerAuth": Object { + "in": "query", + "name": "access_token", + "type": "apiKey", }, }, "swagger": "2.0", diff --git a/packages/create-whook/src/services/__snapshots__/MECHANISMS.test.js.snap b/packages/create-whook/src/services/__snapshots__/MECHANISMS.test.js.snap new file mode 100644 index 00000000..def53523 --- /dev/null +++ b/packages/create-whook/src/services/__snapshots__/MECHANISMS.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NECHANISMS should also include fake on debugging 1`] = ` +Object { + "logCalls": Array [ + Array [ + "debug", + "πŸ”§ - Initializing auth mechanisms", + ], + Array [ + "warning", + "⚠️ - Using fake auth mechanism!", + ], + ], +} +`; + +exports[`NECHANISMS should only include bearer 1`] = ` +Object { + "logCalls": Array [ + Array [ + "debug", + "πŸ”§ - Initializing auth mechanisms", + ], + ], +} +`; diff --git a/packages/create-whook/src/services/__snapshots__/authentication.test.js.snap b/packages/create-whook/src/services/__snapshots__/authentication.test.js.snap new file mode 100644 index 00000000..f2351591 --- /dev/null +++ b/packages/create-whook/src/services/__snapshots__/authentication.test.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`authentication .check() with a bad auth type should fail 1`] = ` +Object { + "errorCode": "E_UNEXPECTED_SUCCESS", + "errorParams": Array [], +} +`; + +exports[`authentication .check() with bearer type should fail with a bad token 1`] = ` +Object { + "errorCode": "E_UNEXPECTED_SUCCESS", + "errorParams": Array [], +} +`; + +exports[`authentication .check() with bearer type should work with a good token 1`] = ` +Object { + "result": Object { + "scopes": Array [ + "admin", + ], + "userId": 1, + }, +} +`; + +exports[`authentication .check() with fake type should work with fakedata 1`] = ` +Object { + "result": Object { + "scopes": Array [ + "user", + ], + "userId": 1, + }, +} +`; diff --git a/packages/create-whook/src/services/authentication.js b/packages/create-whook/src/services/authentication.js new file mode 100644 index 00000000..34d9890d --- /dev/null +++ b/packages/create-whook/src/services/authentication.js @@ -0,0 +1,27 @@ +import { autoService } from 'knifecycle'; +import YError from 'yerror'; + +export default autoService(initAuthentication); + +// A fake authentication service +async function initAuthentication({ TOKEN }) { + const authentication = { + check: async (type, data) => { + if (type === 'fake') { + return data; + } + if (type === 'bearer') { + if (data.hash === TOKEN) { + return { + userId: 1, + scopes: ['admin'], + }; + } + throw new YError('E_BAD_BEARER_TOKEN', type, data.hash); + } + throw new YError('E_UNEXPECTED_AUTH_TYPE', type); + }, + }; + + return authentication; +} diff --git a/packages/create-whook/src/services/authentication.test.js b/packages/create-whook/src/services/authentication.test.js new file mode 100644 index 00000000..52933ab7 --- /dev/null +++ b/packages/create-whook/src/services/authentication.test.js @@ -0,0 +1,63 @@ +import initAuthentication from './authentication'; +import YError from 'yerror'; + +describe('authentication', () => { + const TOKEN = 'my_secret'; + + describe('.check()', () => { + describe('with bearer type', () => { + it('should work with a good token', async () => { + const authentication = await initAuthentication({ TOKEN }); + const result = await authentication.check('bearer', { hash: TOKEN }); + + expect({ + result, + }).toMatchSnapshot(); + }); + + it('should fail with a bad token', async () => { + const authentication = await initAuthentication({ TOKEN }); + + try { + authentication.check('bearer', { hash: 'lol' }); + throw new YError('E_UNEXPECTED_SUCCESS'); + } catch (err) { + expect({ + errorCode: err.code, + errorParams: err.params, + }).toMatchSnapshot(); + } + }); + }); + + describe('with fake type', () => { + it('should work with fakedata', async () => { + const authentication = await initAuthentication({ TOKEN }); + const result = await authentication.check('fake', { + userId: 1, + scopes: ['user'], + }); + + expect({ + result, + }).toMatchSnapshot(); + }); + }); + + describe('with a bad auth type', () => { + it('should fail', async () => { + const authentication = await initAuthentication({ TOKEN }); + + try { + authentication.check('yolo', { hash: 'lol' }); + throw new YError('E_UNEXPECTED_SUCCESS'); + } catch (err) { + expect({ + errorCode: err.code, + errorParams: err.params, + }).toMatchSnapshot(); + } + }); + }); + }); +}); diff --git a/packages/whook-authorization/API.md b/packages/whook-authorization/API.md new file mode 100644 index 00000000..63b7d57e --- /dev/null +++ b/packages/whook-authorization/API.md @@ -0,0 +1,55 @@ +# API +## Constants + +
+
optionsWithCORS β‡’ Promise.<Object>
+

A simple Whook handler that just returns a 200 OK + HTTP response

+
+
+ +## Functions + +
+
wrapHandlerWithCORS(initHandler) β‡’ function
+

Wrap an handler initializer to append CORS to response.

+
+
augmentAPIWithCORS(API) β‡’ Promise.<Object>
+

Augment an OpenAPI to also serve OPTIONS methods with + the CORS added.

+
+
+ + + +## optionsWithCORS β‡’ Promise.<Object> +A simple Whook handler that just returns a 200 OK + HTTP response + +**Kind**: global constant +**Returns**: Promise.<Object> - The HTTP response object + + +## wrapHandlerWithCORS(initHandler) β‡’ function +Wrap an handler initializer to append CORS to response. + +**Kind**: global function +**Returns**: function - The handler initializer wrapped + +| Param | Type | Description | +| --- | --- | --- | +| initHandler | function | The handler initializer | + + + +## augmentAPIWithCORS(API) β‡’ Promise.<Object> +Augment an OpenAPI to also serve OPTIONS methods with + the CORS added. + +**Kind**: global function +**Returns**: Promise.<Object> - The augmented OpenAPI object + +| Param | Type | Description | +| --- | --- | --- | +| API | Object | The OpenAPI object | + diff --git a/packages/whook-authorization/LICENSE b/packages/whook-authorization/LICENSE new file mode 100644 index 00000000..df9966e5 --- /dev/null +++ b/packages/whook-authorization/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright Β© 2017 Nicolas Froidure + +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/whook-authorization/README.md b/packages/whook-authorization/README.md new file mode 100644 index 00000000..0e470576 --- /dev/null +++ b/packages/whook-authorization/README.md @@ -0,0 +1,82 @@ +[//]: # ( ) +[//]: # (This file is automatically generated by a `metapak`) +[//]: # (module. Do not change it except between the) +[//]: # (`content:start/end` flags, your changes would) +[//]: # (be overridden.) +[//]: # ( ) +# whook-authorization +> A wrapper to provide authorization support to a Whook server + +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nfroidure/whook-authorization/blob/master/LICENSE) +[![NPM version](https://badge.fury.io/js/whook-authorization.svg)](https://npmjs.org/package/whook-authorization) + + +[//]: # (::contents:start) + +To see how to add authorization support to your application, have a look + at the `create-whook` project, it will be well documented here + as soon as possible. + +[//]: # (::contents:end) + +# API +## Constants + +
+
optionsWithCORS β‡’ Promise.<Object>
+

A simple Whook handler that just returns a 200 OK + HTTP response

+
+
+ +## Functions + +
+
wrapHandlerWithCORS(initHandler) β‡’ function
+

Wrap an handler initializer to append CORS to response.

+
+
augmentAPIWithCORS(API) β‡’ Promise.<Object>
+

Augment an OpenAPI to also serve OPTIONS methods with + the CORS added.

+
+
+ + + +## optionsWithCORS β‡’ Promise.<Object> +A simple Whook handler that just returns a 200 OK + HTTP response + +**Kind**: global constant +**Returns**: Promise.<Object> - The HTTP response object + + +## wrapHandlerWithCORS(initHandler) β‡’ function +Wrap an handler initializer to append CORS to response. + +**Kind**: global function +**Returns**: function - The handler initializer wrapped + +| Param | Type | Description | +| --- | --- | --- | +| initHandler | function | The handler initializer | + + + +## augmentAPIWithCORS(API) β‡’ Promise.<Object> +Augment an OpenAPI to also serve OPTIONS methods with + the CORS added. + +**Kind**: global function +**Returns**: Promise.<Object> - The augmented OpenAPI object + +| Param | Type | Description | +| --- | --- | --- | +| API | Object | The OpenAPI object | + + +# Authors +- [Nicolas Froidure](http://insertafter.com/en/index.html) + +# License +[MIT](https://github.com/nfroidure/whook-authorization/blob/master/LICENSE) diff --git a/packages/whook-authorization/package.json b/packages/whook-authorization/package.json new file mode 100644 index 00000000..0f93f2b0 --- /dev/null +++ b/packages/whook-authorization/package.json @@ -0,0 +1,140 @@ +{ + "name": "whook-authorization", + "version": "0.0.0", + "description": "A wrapper to provide authorization support to a Whook server", + "main": "dist/index.js", + "metapak": { + "configs": [ + "main", + "readme", + "eslint", + "babel", + "jest", + "jsdocs" + ], + "data": { + "childPackage": true, + "files": "'src/**/*.js'", + "ignore": [ + "dist" + ], + "bundleFiles": [ + "dist/**/*.js" + ] + } + }, + "author": { + "name": "Nicolas Froidure", + "email": "nicolas.froidure@insertafter.com", + "url": "http://insertafter.com/en/index.html" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/nfroidure/whook.git" + }, + "bugs": { + "url": "https://github.com/nfroidure/whook/issues" + }, + "homepage": "https://github.com/nfroidure/whook", + "dependencies": { + "knifecycle": "^5.2.0", + "http-auth-utils": "^2.1.0" + }, + "peerDependencies": { + "whook": "^3.1.3" + }, + "devDependencies": { + "@babel/cli": "^7.2.3", + "@babel/core": "^7.2.2", + "@babel/plugin-proposal-object-rest-spread": "^7.3.1", + "@babel/preset-env": "^7.3.1", + "@babel/register": "^7.0.0", + "babel-eslint": "^10.0.1", + "babel-plugin-knifecycle": "^1.0.3", + "eslint": "^5.13.0", + "eslint-plugin-prettier": "^3.0.1", + "jest": "^24.0.0", + "jsdoc-to-markdown": "^4.0.1", + "metapak": "^3.1.5", + "metapak-nfroidure": "9.5.1", + "prettier": "^1.16.3" + }, + "contributors": [], + "engines": { + "node": ">=8.12.0" + }, + "files": [ + "dist/**/*.js", + "LICENSE", + "README.md", + "CHANGELOG.md" + ], + "eslintConfig": { + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "modules": true + }, + "env": { + "es6": true, + "node": true, + "jest": true, + "mocha": true + }, + "plugins": [ + "prettier" + ], + "rules": { + "prettier/prettier": "error" + } + }, + "prettier": { + "semi": true, + "printWidth": 80, + "singleQuote": true, + "trailingComma": "all", + "proseWrap": "always" + }, + "babel": { + "presets": [ + [ + "@babel/env", + { + "targets": { + "node": "8.12.0" + } + } + ] + ], + "plugins": [ + "@babel/plugin-proposal-object-rest-spread", + "babel-plugin-knifecycle" + ] + }, + "jest": { + "coverageReporters": [ + "lcov", + "html" + ], + "testPathIgnorePatterns": [ + "/node_modules/" + ], + "roots": [ + "/src" + ] + }, + "scripts": { + "cli": "env NODE_ENV=${NODE_ENV:-cli}", + "compile": "babel src --out-dir=dist", + "cover": "npm run jest -- --coverage", + "doc": "echo \"# API\" > API.md; jsdoc2md 'src/**/*.js' >> API.md && git add API.md", + "jest": "NODE_ENV=test jest", + "lint": "eslint 'src/**/*.js'", + "metapak": "metapak", + "prettier": "prettier --write 'src/**/*.js'", + "preversion": " && npm run compile", + "test": "npm run jest" + } +} diff --git a/packages/whook-authorization/src/__snapshots__/index.test.js.snap b/packages/whook-authorization/src/__snapshots__/index.test.js.snap new file mode 100644 index 00000000..8735811b --- /dev/null +++ b/packages/whook-authorization/src/__snapshots__/index.test.js.snap @@ -0,0 +1,273 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`wrapHandlerWithAuthorization should fail with a mismatch between user and authenticated one 1`] = ` +Object { + "authenticationChecks": Array [ + Array [ + "bearer", + Object { + "hash": "yolo", + }, + ], + ], + "errorCode": "E_UNAUTHORIZED", + "errorParams": Array [ + 1, + 3, + ], + "httpCode": 403, + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], +} +`; + +exports[`wrapHandlerWithAuthorization should fail with bad operation definition provided 1`] = ` +Object { + "authenticationChecks": Array [], + "errorCode": "E_MISCONFIGURATION", + "errorParams": Array [ + "Bearer", + Array [], + ], + "httpCode": 500, + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], +} +`; + +exports[`wrapHandlerWithAuthorization should fail with no authorization at all for secured endpoints 1`] = ` +Object { + "authenticationChecks": Array [], + "errorCode": "E_UNAUTHORIZED", + "errorParams": Array [], + "httpCode": 401, + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], +} +`; + +exports[`wrapHandlerWithAuthorization should fail with no operation definition provided 1`] = ` +Object { + "authenticationChecks": Array [], + "errorCode": "E_OPERATION_REQUIRED", + "errorParams": Array [], + "httpCode": 500, + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], +} +`; + +exports[`wrapHandlerWithAuthorization should fail with not supported auth 1`] = ` +Object { + "authenticationChecks": Array [], + "errorCode": "E_UNKNOWN_AUTH_MECHANISM", + "errorParams": Array [ + "Whatever yolo", + ], + "httpCode": 400, + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], +} +`; + +exports[`wrapHandlerWithAuthorization should fail with unallowed mechanisms 1`] = ` +Object { + "authenticationChecks": Array [], + "errorCode": "E_UNALLOWED_AUTH_MECHANISM", + "errorParams": Array [ + "Basic yolo", + ], + "httpCode": 400, + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], +} +`; + +exports[`wrapHandlerWithAuthorization should fail without right scopes 1`] = ` +Object { + "authenticationChecks": Array [ + Array [ + "bearer", + Object { + "hash": "yolo", + }, + ], + ], + "errorCode": "E_UNAUTHORIZED", + "errorParams": Array [ + Array [], + Array [ + "user", + "admin", + ], + ], + "httpCode": 403, + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], +} +`; + +exports[`wrapHandlerWithAuthorization should proxy authentication errors 1`] = ` +Object { + "authenticationChecks": Array [ + Array [ + "bearer", + Object { + "hash": "yolo", + }, + ], + ], + "errorCode": "E_UNAUTHORIZED", + "errorParams": Array [], + "httpCode": 401, + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], +} +`; + +exports[`wrapHandlerWithAuthorization with authenticated and restricted endpoints should work with access tokens and good authentication check 1`] = ` +Object { + "authenticationChecks": Array [ + Array [ + "bearer", + Object { + "hash": "yolo", + }, + ], + ], + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], + "response": Object { + "headers": Object { + "X-Authenticated": "{\\"userId\\":1,\\"scopes\\":[\\"user\\",\\"admin\\"]}", + }, + "status": 200, + }, +} +`; + +exports[`wrapHandlerWithAuthorization with authenticated and restricted endpoints should work with bearer tokens and good authentication check 1`] = ` +Object { + "authenticationChecks": Array [ + Array [ + "bearer", + Object { + "hash": "yolo", + }, + ], + ], + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], + "response": Object { + "headers": Object { + "X-Authenticated": "{\\"userId\\":1,\\"scopes\\":[\\"user\\",\\"admin\\"]}", + }, + "status": 200, + }, +} +`; + +exports[`wrapHandlerWithAuthorization with authenticated but not restricted endpoints should work with access tokens and good authentication check 1`] = ` +Object { + "authenticationChecks": Array [ + Array [ + "bearer", + Object { + "hash": "yolo", + }, + ], + ], + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], + "response": Object { + "headers": Object { + "X-Authenticated": "{\\"userId\\":1,\\"scopes\\":[\\"user\\",\\"admin\\"]}", + }, + "status": 200, + }, +} +`; + +exports[`wrapHandlerWithAuthorization with authenticated but not restricted endpoints should work with bearer tokens and good authentication check 1`] = ` +Object { + "authenticationChecks": Array [ + Array [ + "bearer", + Object { + "hash": "yolo", + }, + ], + ], + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], + "response": Object { + "headers": Object { + "X-Authenticated": "{\\"userId\\":1,\\"scopes\\":[\\"user\\",\\"admin\\"]}", + }, + "status": 200, + }, +} +`; + +exports[`wrapHandlerWithAuthorization with unauthenticated endpoints should work 1`] = ` +Object { + "authenticationChecks": Array [], + "logCalls": Array [ + Array [ + "debug", + "πŸ” - Initializing the authentication wrapper.", + ], + ], + "response": Object { + "status": 200, + }, +} +`; diff --git a/packages/whook-authorization/src/index.js b/packages/whook-authorization/src/index.js new file mode 100644 index 00000000..1fd4968a --- /dev/null +++ b/packages/whook-authorization/src/index.js @@ -0,0 +1,168 @@ +import { reuseSpecialProps, alsoInject } from 'knifecycle'; +import HTTPError from 'yhttperror'; +import { + parseAuthorizationHeader, + BEARER as BEARER_MECHANISM, +} from 'http-auth-utils'; + +/** + * Wrap an handler initializer to check client's authorizations. + * @param {Function} initHandler The handler initializer + * @returns {Function} The handler initializer wrapped + */ +export function wrapHandlerWithAuthorization(initHandler) { + return alsoInject( + ['?MECHANISMS', '?DEFAULT_MECHANISM', 'authentication', 'log'], + reuseSpecialProps( + initHandler, + initHandlerWithAuthorization.bind(null, initHandler), + ), + ); +} + +async function initHandlerWithAuthorization( + initHandler, + { + MECHANISMS = [BEARER_MECHANISM], + DEFAULT_MECHANISM = BEARER_MECHANISM.type, + authentication, + log, + ...otherServices + }, +) { + log('debug', 'πŸ” - Initializing the authentication wrapper.'); + + const services = { + MECHANISMS, + DEFAULT_MECHANISM, + authentication, + log, + ...otherServices, + }; + const handler = await initHandler(services); + + return handleWithAuthorization.bind(null, services, handler); +} + +async function handleWithAuthorization( + { MECHANISMS, DEFAULT_MECHANISM, authentication }, + handler, + parameters, + operation, +) { + let response; + + // Since the operation embed the security rules + // we need ensure we got it here since, if for + // any reason, the operation is not transmitted + // then security will not be checked + // and the API will have a big security hole. + // TL;DR: DO NOT remove this line! + if (!operation) { + throw new HTTPError(500, 'E_OPERATION_REQUIRED'); + } + + if ('undefined' === typeof operation.security) { + response = await handler(parameters, operation); + } else { + const authorization = parameters.access_token + ? `${DEFAULT_MECHANISM} ${parameters.access_token}` + : parameters.authorization; + let parsedAuthorization; + + if (!authorization) { + throw new HTTPError(401, 'E_UNAUTHORIZED'); + } + + try { + parsedAuthorization = parseAuthorizationHeader( + authorization, + MECHANISMS.filter( + mechanism => + operation.security[`${mechanism.type.toLowerCase()}Auth`], + ), + ); + } catch (err) { + // This code should be simplified by solving this issue + // https://github.com/nfroidure/http-auth-utils/issues/2 + if ( + err.code === 'E_UNKNOWN_AUTH_MECHANISM' && + MECHANISMS.some( + mechanism => + authorization.substr(0, mechanism.type.length) === mechanism.type, + ) + ) { + throw HTTPError.wrap(err, 400, 'E_UNALLOWED_AUTH_MECHANISM'); + } + throw HTTPError.cast(err, 400); + } + + const requiredScopes = + operation.security[`${parsedAuthorization.type.toLowerCase()}Auth`]; + + // If security exists, we need at least one scope + if (!(requiredScopes && requiredScopes.length)) { + throw new HTTPError( + 500, + 'E_MISCONFIGURATION', + parsedAuthorization.type, + requiredScopes, + ); + } + + let authorizationContent; + + try { + authorizationContent = await authentication.check( + parsedAuthorization.type.toLowerCase(), + parsedAuthorization.data, + ); + } catch (err) { + throw HTTPError.cast(err, 401); + } + + // Check user id if present in parameters + if ( + 'undefined' !== typeof parameters.userId && + authorizationContent.userId !== parameters.userId + ) { + throw new HTTPError( + 403, + 'E_UNAUTHORIZED', + authorizationContent.userId, + parameters.userId, + ); + } + + // Check scopes + if ( + !requiredScopes.some(requiredScope => + authorizationContent.scopes.includes(requiredScope), + ) + ) { + throw new HTTPError( + 403, + 'E_UNAUTHORIZED', + authorizationContent.scopes, + requiredScopes, + ); + } + + response = await handler( + { + ...parameters, + ...authorizationContent, + authenticated: true, + }, + operation, + ); + response = { + ...response, + headers: { + ...(response.headers || {}), + 'X-Authenticated': JSON.stringify(authorizationContent), + }, + }; + } + return response; +} diff --git a/packages/whook-authorization/src/index.test.js b/packages/whook-authorization/src/index.test.js new file mode 100644 index 00000000..03c6789a --- /dev/null +++ b/packages/whook-authorization/src/index.test.js @@ -0,0 +1,421 @@ +import { wrapHandlerWithAuthorization } from '.'; +import { handler } from 'knifecycle'; +import YError from 'yerror'; +import { + BEARER as BEARER_MECHANISM, + BASIC as BASIC_MECHANISM, +} from 'http-auth-utils'; + +describe('wrapHandlerWithAuthorization', () => { + const log = jest.fn(); + const authentication = { + check: jest.fn(), + }; + const NOOP_OPERATION = { + operationId: 'noopHandler', + summary: 'Does nothing.', + tags: ['system'], + consumes: [], + produces: [], + responses: { + 200: { + description: 'Sucessfully did nothing!', + }, + }, + }; + const NOOP_AUTHENTICATED_OPERATION = { + ...NOOP_OPERATION, + security: { + bearerAuth: ['user', 'admin'], + }, + }; + const NOOP_RESTRICTED_OPERATION = { + ...NOOP_OPERATION, + security: { + bearerAuth: ['user', 'admin'], + }, + }; + const BAD_OPERATION = { + ...NOOP_OPERATION, + security: { + bearerAuth: [], + }, + }; + + beforeEach(() => { + log.mockReset(); + authentication.check.mockReset(); + }); + + describe('with unauthenticated endpoints', () => { + it('should work', async () => { + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + authentication, + log, + }); + const response = await wrappedHandler({}, NOOP_OPERATION); + + expect({ + response, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + }); + + describe('with authenticated but not restricted endpoints', () => { + it('should work with bearer tokens and good authentication check', async () => { + authentication.check.mockResolvedValue({ + userId: 1, + scopes: ['user', 'admin'], + }); + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + authentication, + log, + }); + const response = await wrappedHandler( + { + authorization: 'Bearer yolo', + }, + NOOP_AUTHENTICATED_OPERATION, + ); + + expect({ + response, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + + it('should work with access tokens and good authentication check', async () => { + authentication.check.mockResolvedValue({ + userId: 1, + scopes: ['user', 'admin'], + }); + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + authentication, + log, + }); + const response = await wrappedHandler( + { + access_token: 'yolo', + }, + NOOP_AUTHENTICATED_OPERATION, + ); + + expect({ + response, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + }); + + describe('with authenticated and restricted endpoints', () => { + it('should work with bearer tokens and good authentication check', async () => { + authentication.check.mockResolvedValue({ + userId: 1, + scopes: ['user', 'admin'], + }); + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + authentication, + log, + }); + const response = await wrappedHandler( + { + authorization: 'Bearer yolo', + }, + NOOP_RESTRICTED_OPERATION, + ); + + expect({ + response, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + + it('should work with access tokens and good authentication check', async () => { + authentication.check.mockResolvedValue({ + userId: 1, + scopes: ['user', 'admin'], + }); + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + authentication, + log, + }); + const response = await wrappedHandler( + { + access_token: 'yolo', + }, + NOOP_RESTRICTED_OPERATION, + ); + + expect({ + response, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + }); + + it('should fail with no operation definition provided', async () => { + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + authentication, + log, + }); + + try { + await wrappedHandler({ + access_token: 'yolo', + }); + throw new Error('E_UNEXPECTED_SUCCESS'); + } catch (err) { + expect({ + httpCode: err.httpCode, + errorCode: err.code, + errorParams: err.params, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + } + }); + + it('should fail with a mismatch between user and authenticated one', async () => { + authentication.check.mockResolvedValue({ + userId: 1, + scopes: ['user', 'admin'], + }); + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + authentication, + log, + }); + + try { + await wrappedHandler( + { + access_token: 'yolo', + userId: 3, + }, + NOOP_RESTRICTED_OPERATION, + ); + throw new Error('E_UNEXPECTED_SUCCESS'); + } catch (err) { + expect({ + httpCode: err.httpCode, + errorCode: err.code, + errorParams: err.params, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + } + }); + + it('should fail with bad operation definition provided', async () => { + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + authentication, + log, + }); + + try { + await wrappedHandler( + { + access_token: 'yolo', + }, + BAD_OPERATION, + ); + throw new Error('E_UNEXPECTED_SUCCESS'); + } catch (err) { + expect({ + httpCode: err.httpCode, + errorCode: err.code, + errorParams: err.params, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + } + }); + + it('should fail without right scopes', async () => { + authentication.check.mockResolvedValue({ + userId: 1, + scopes: [], + }); + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + authentication, + log, + }); + + try { + await wrappedHandler( + { + authorization: 'Bearer yolo', + }, + NOOP_RESTRICTED_OPERATION, + ); + throw new Error('E_UNEXPECTED_SUCCESS'); + } catch (err) { + expect({ + httpCode: err.httpCode, + errorCode: err.code, + errorParams: err.params, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + } + }); + + it('should fail with unallowed mechanisms', async () => { + authentication.check.mockResolvedValue({ + userId: 1, + scopes: [], + }); + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + MECHANISMS: [BASIC_MECHANISM, BEARER_MECHANISM], + authentication, + log, + }); + + try { + await wrappedHandler( + { + authorization: 'Basic yolo', + }, + NOOP_RESTRICTED_OPERATION, + ); + throw new Error('E_UNEXPECTED_SUCCESS'); + } catch (err) { + expect({ + httpCode: err.httpCode, + errorCode: err.code, + errorParams: err.params, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + } + }); + + it('should fail with not supported auth', async () => { + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + authentication, + log, + }); + + try { + await wrappedHandler( + { + authorization: 'Whatever yolo', + }, + NOOP_RESTRICTED_OPERATION, + ); + throw new Error('E_UNEXPECTED_SUCCESS'); + } catch (err) { + expect({ + httpCode: err.httpCode, + errorCode: err.code, + errorParams: err.params, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + } + }); + + it('should fail with no authorization at all for secured endpoints', async () => { + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + authentication, + log, + }); + + try { + await wrappedHandler({}, NOOP_RESTRICTED_OPERATION); + throw new Error('E_UNEXPECTED_SUCCESS'); + } catch (err) { + expect({ + httpCode: err.httpCode, + errorCode: err.code, + errorParams: err.params, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + } + }); + + it('should proxy authentication errors', async () => { + authentication.check.mockRejectedValue(new YError('E_UNAUTHORIZED')); + + const noopHandler = handler(() => ({ status: 200 }), 'getNoop'); + const wrappedNoodHandlerWithAuthorization = wrapHandlerWithAuthorization( + noopHandler, + ); + const wrappedHandler = await wrappedNoodHandlerWithAuthorization({ + authentication, + log, + }); + + try { + await wrappedHandler( + { + authorization: 'Bearer yolo', + }, + NOOP_RESTRICTED_OPERATION, + ); + throw new Error('E_UNEXPECTED_SUCCESS'); + } catch (err) { + expect({ + httpCode: err.httpCode, + errorCode: err.code, + errorParams: err.params, + authenticationChecks: authentication.check.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + } + }); +}); diff --git a/packages/whook/src/index.test.js b/packages/whook/src/index.test.js index 4bce5df6..d2d00276 100644 --- a/packages/whook/src/index.test.js +++ b/packages/whook/src/index.test.js @@ -58,7 +58,6 @@ describe('runServer', () => { $.register(constant('PORT', 8888)); $.register(constant('HOST', 'localhost')); $.register(constant('WRAPPERS', [])); - $.register(constant('NODE_ENV', 'test')); $.register(constant('DEBUG_NODE_ENVS', [])); $.register(constant('NODE_ENVS', ['test'])); $.register(