From 612e4348c2e39d731b981268ff557f7fa1998233 Mon Sep 17 00:00:00 2001 From: Maximilian Beck Date: Sun, 3 Jan 2021 18:05:18 +0100 Subject: [PATCH 1/3] #20: Rename ApiModule --- src/ApplicationModule.ts | 4 ++-- src/api/ApiModule.ts | 31 ------------------------------- src/api/http/HttpApiModule.ts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 33 deletions(-) delete mode 100644 src/api/ApiModule.ts create mode 100644 src/api/http/HttpApiModule.ts diff --git a/src/ApplicationModule.ts b/src/ApplicationModule.ts index cff8999..580e25e 100644 --- a/src/ApplicationModule.ts +++ b/src/ApplicationModule.ts @@ -2,7 +2,7 @@ import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConnectionOptions } from 'typeorm'; import { JwtDecodeMiddleware } from './infrastructure/security/jwt/JwtDecodeMiddleware'; -import { ApiModule } from './api/ApiModule'; +import { HttpApiModule } from './api/http/HttpApiModule'; import { VotingDomainModule } from './domain/VotingDomainModule'; import { InfrastructureModule } from './infrastructure/InfrastructureModule'; // eslint-disable-line import/order @@ -20,7 +20,7 @@ const votingDomain = VotingDomainModule.forRoot([InfrastructureModule]); }; }, }), - ApiModule.forRoot([votingDomain]), + HttpApiModule.forRoot([votingDomain]), ], }) export class ApplicationModule { diff --git a/src/api/ApiModule.ts b/src/api/ApiModule.ts deleted file mode 100644 index 8f3b92b..0000000 --- a/src/api/ApiModule.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { DynamicModule, Module } from '@nestjs/common'; -import { APP_FILTER } from '@nestjs/core'; -import { ImportModule } from '../util/ImportModule'; -import { ApiExceptionFilter } from './http/rest/ApiExceptionFilter'; -import { HealthCheckController } from './http/rest/healthcheck/controller/HealthCheckController'; -import { SessionController } from './http/rest/voting/session/controller/SessionController'; -import { CreateSessionRequestResponseFactory } from './http/rest/voting/session/factory/CreateSessionRequestResponseFactory'; -import { CreateTopicRequestResponseFactory } from './http/rest/voting/session/factory/CreateTopicRequestResponseFactory'; -import { CreateParticipantRequestResponseFactory } from './http/rest/voting/session/factory/CreateParticipantRequestResponseFactory'; -import { ExternalIdComposer } from './http/rest/voting/session/factory/ExternalIdComposer'; -import { TopicController } from './http/rest/voting/session/controller/TopicController'; -import { ParticipantController } from './http/rest/voting/session/controller/ParticipantController'; - -@Module({ - controllers: [HealthCheckController, SessionController, TopicController, ParticipantController], - providers: [ - { provide: APP_FILTER, useClass: ApiExceptionFilter }, - CreateSessionRequestResponseFactory, - CreateTopicRequestResponseFactory, - CreateParticipantRequestResponseFactory, - ExternalIdComposer, - ], -}) -export class ApiModule { - public static forRoot(imports: ImportModule[]): DynamicModule { - return { - module: ApiModule, - imports, - }; - } -} diff --git a/src/api/http/HttpApiModule.ts b/src/api/http/HttpApiModule.ts new file mode 100644 index 0000000..00f3e0d --- /dev/null +++ b/src/api/http/HttpApiModule.ts @@ -0,0 +1,31 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { APP_FILTER } from '@nestjs/core'; +import { ImportModule } from '../../util/ImportModule'; +import { ApiExceptionFilter } from './rest/ApiExceptionFilter'; +import { HealthCheckController } from './rest/healthcheck/controller/HealthCheckController'; +import { SessionController } from './rest/voting/session/controller/SessionController'; +import { CreateSessionRequestResponseFactory } from './rest/voting/session/factory/CreateSessionRequestResponseFactory'; +import { CreateTopicRequestResponseFactory } from './rest/voting/session/factory/CreateTopicRequestResponseFactory'; +import { CreateParticipantRequestResponseFactory } from './rest/voting/session/factory/CreateParticipantRequestResponseFactory'; +import { ExternalIdComposer } from './rest/voting/session/factory/ExternalIdComposer'; +import { TopicController } from './rest/voting/session/controller/TopicController'; +import { ParticipantController } from './rest/voting/session/controller/ParticipantController'; + +@Module({ + controllers: [HealthCheckController, SessionController, TopicController, ParticipantController], + providers: [ + { provide: APP_FILTER, useClass: ApiExceptionFilter }, + CreateSessionRequestResponseFactory, + CreateTopicRequestResponseFactory, + CreateParticipantRequestResponseFactory, + ExternalIdComposer, + ], +}) +export class HttpApiModule { + public static forRoot(imports: ImportModule[]): DynamicModule { + return { + module: HttpApiModule, + imports, + }; + } +} From 442c3639061e8d4597bc081e7332d2c7212c8af1 Mon Sep 17 00:00:00 2001 From: Maximilian Beck Date: Mon, 4 Jan 2021 00:07:10 +0100 Subject: [PATCH 2/3] #20: Add websocket gateways and handle connections --- .env.local | 1 + docker-compose.yml | 1 + package-lock.json | 787 +++++++++++++++++- package.json | 8 +- setupTests.ts | 3 + src/ApplicationModule.ts | 7 +- src/api/AbstractExceptionFilter.ts | 36 + src/api/http/rest/ApiExceptionFilter.spec.ts | 14 +- src/api/http/rest/ApiExceptionFilter.ts | 113 +-- src/api/ws/WebSocketExceptionFilter.spec.ts | 90 ++ src/api/ws/WebSocketExceptionFilter.ts | 15 + src/api/ws/voting/AuthorizedSocket.ts | 6 + .../WebSocketDispatcherApiModule.ts | 9 + .../dispatcher/gateway/SessionGateway.spec.ts | 13 + .../dispatcher/gateway/SessionGateway.ts | 8 + .../ClientNotAuthorizedForSessionException.ts | 7 + ...icipantNotAuthorizedForSessionException.ts | 7 + .../receiver/WebSocketReceiverApiModule.ts | 15 + .../receiver/gateway/VotingGateway.spec.ts | 108 +++ .../voting/receiver/gateway/VotingGateway.ts | 64 ++ src/domain/index.ts | 6 + .../AbstractSessionMessagingGateway.ts | 1 + src/domain/model/Session.spec.ts | 8 + src/domain/model/Session.ts | 4 + src/domain/service/SessionService.spec.ts | 11 + src/domain/service/SessionService.ts | 4 + .../clientToken/ClientTokenPayload.ts | 6 + .../clientToken/decodeClientPayload.spec.ts | 129 +++ .../clientToken/decodeClientPayload.ts | 37 + test/connect-to-session.e2e-spec.ts | 65 ++ test/jest.setup.e2e.after-env.ts | 3 + 31 files changed, 1504 insertions(+), 82 deletions(-) create mode 100644 src/api/AbstractExceptionFilter.ts create mode 100644 src/api/ws/WebSocketExceptionFilter.spec.ts create mode 100644 src/api/ws/WebSocketExceptionFilter.ts create mode 100644 src/api/ws/voting/AuthorizedSocket.ts create mode 100644 src/api/ws/voting/dispatcher/WebSocketDispatcherApiModule.ts create mode 100644 src/api/ws/voting/dispatcher/gateway/SessionGateway.spec.ts create mode 100644 src/api/ws/voting/dispatcher/gateway/SessionGateway.ts create mode 100644 src/api/ws/voting/exception/ClientNotAuthorizedForSessionException.ts create mode 100644 src/api/ws/voting/exception/ParticipantNotAuthorizedForSessionException.ts create mode 100644 src/api/ws/voting/receiver/WebSocketReceiverApiModule.ts create mode 100644 src/api/ws/voting/receiver/gateway/VotingGateway.spec.ts create mode 100644 src/api/ws/voting/receiver/gateway/VotingGateway.ts create mode 100644 src/domain/messaging/AbstractSessionMessagingGateway.ts create mode 100644 src/infrastructure/security/clientToken/ClientTokenPayload.ts create mode 100644 src/infrastructure/security/clientToken/decodeClientPayload.spec.ts create mode 100644 src/infrastructure/security/clientToken/decodeClientPayload.ts create mode 100644 test/connect-to-session.e2e-spec.ts diff --git a/.env.local b/.env.local index f92feac..4bd15a4 100644 --- a/.env.local +++ b/.env.local @@ -7,3 +7,4 @@ DB_SCHEMA=public DB_SSL= REDIS_HOST=localhost REDIS_PORT=6379 +JWT_CLIENT_TOKEN_SECRET=SuperSecret1 diff --git a/docker-compose.yml b/docker-compose.yml index 26e0287..c1c9ed8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,7 @@ services: REDIS_HOST: redis REDIS_PORT: 6379 SSL_REDIRECTION_ENABLED: 0 + JWT_CLIENT_TOKEN_SECRET: SuperSecret1 volumes: - .:/opt/app ports: diff --git a/package-lock.json b/package-lock.json index 3c2036d..b768425 100644 --- a/package-lock.json +++ b/package-lock.json @@ -897,6 +897,230 @@ "tslib": "2.0.3" } }, + "@nestjs/platform-socket.io": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-7.6.5.tgz", + "integrity": "sha512-KX3j7yFFUKZVrYZQCG6vuLGW24/ozegmZEb9hbwTNlXNgLUaqqvOwMH596g1DlOS2Ju1XjazTwP/VlbckS05HA==", + "requires": { + "socket.io": "2.3.0", + "tslib": "2.0.3" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "engine.io": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz", + "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==", + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "0.3.1", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "ws": "^7.1.2" + } + }, + "engine.io-client": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz", + "integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==", + "requires": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", + "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.4", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "socket.io": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", + "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", + "requires": { + "debug": "~4.1.0", + "engine.io": "~3.4.0", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.3.0", + "socket.io-parser": "~3.4.0" + }, + "dependencies": { + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "socket.io-client": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "socket.io-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", + "integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==", + "requires": { + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + } + } + } + } + }, + "socket.io-adapter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", + "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" + }, + "socket.io-parser": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", + "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", + "requires": { + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "isarray": "2.0.1" + } + } + } + }, "@nestjs/schematics": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-7.2.5.tgz", @@ -970,6 +1194,15 @@ } } }, + "@nestjs/websockets": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-7.6.5.tgz", + "integrity": "sha512-PdrQtXvjFVAB8AhN4ZtfdJKtk11ds8reYWBZaDK14WSCOcjZNzdosaQ1Tde7wDA2vctPTSiQDxckIKsX80t3tQ==", + "requires": { + "iterare": "1.2.1", + "tslib": "2.0.3" + } + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -1124,6 +1357,15 @@ "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", "dev": true }, + "@types/engine.io": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.4.tgz", + "integrity": "sha512-98rXVukLD6/ozrQ2O80NAlWDGA4INg+tqsEReWJldqyi2fulC9V7Use/n28SWgROXKm6003ycWV4gZHoF8GA6w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/eslint": { "version": "7.2.6", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.6.tgz", @@ -1296,6 +1538,32 @@ "@types/node": "*" } }, + "@types/socket.io": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.12.tgz", + "integrity": "sha512-oStc5VFkpb0AsjOxQUj9ztX5Iziatyla/rjZTYbFGoVrrKwd+JU2mtxk7iSl5RGYx9WunLo6UXW1fBzQok/ZyA==", + "dev": true, + "requires": { + "@types/engine.io": "*", + "@types/node": "*", + "@types/socket.io-parser": "*" + } + }, + "@types/socket.io-client": { + "version": "1.4.34", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.34.tgz", + "integrity": "sha512-Lzia5OTQFJZJ5R4HsEEldywiiqT9+W2rDbyHJiiTGqOcju89sCsQ8aUXDljY6Ls33wKZZGC0bfMhr/VpOyjtXg==", + "dev": true + }, + "@types/socket.io-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/socket.io-parser/-/socket.io-parser-2.2.1.tgz", + "integrity": "sha512-+JNb+7N7tSINyXPxAJb62+NcpC1x/fPn7z818W4xeNCdPTp6VsO/X8fCsg6+ug4a56m1v9sEiTIIUKVupcHOFQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -1791,6 +2059,11 @@ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", "dev": true }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1935,6 +2208,11 @@ "es-abstract": "^1.18.0-next.1" } }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -1962,6 +2240,11 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2071,6 +2354,11 @@ "babel-preset-current-node-syntax": "^1.0.0" } }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -2131,11 +2419,21 @@ } } }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -2145,6 +2443,14 @@ "tweetnacl": "^0.14.3" } }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -2157,6 +2463,11 @@ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -2300,6 +2611,11 @@ "get-intrinsic": "^1.0.0" } }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2600,12 +2916,22 @@ "integrity": "sha512-GKNxVA7/iuTnAqGADlTWX4tkhzxZKXp5fLJqKTlQLHkE65XDUKutZ3BHaJC5IGcper2tT3QRD1xr4o3jNpgXXg==", "dev": true }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "dev": true }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3043,6 +3369,102 @@ "once": "^1.4.0" } }, + "engine.io": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz", + "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==", + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "0.3.1", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "ws": "^7.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "engine.io-client": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz", + "integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==", + "dev": true, + "requires": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==", + "dev": true + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", + "dev": true + }, + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", + "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.4", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, "enhanced-resolve": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", @@ -4295,6 +4717,26 @@ } } }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4463,6 +4905,11 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -6473,6 +6920,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -6736,6 +7188,24 @@ } } }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -7919,6 +8389,305 @@ } } }, + "socket.io": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", + "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", + "requires": { + "debug": "~4.1.0", + "engine.io": "~3.4.0", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.3.0", + "socket.io-parser": "~3.4.0" + }, + "dependencies": { + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "engine.io-client": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.4.tgz", + "integrity": "sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==", + "requires": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + } + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "socket.io-client": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "socket.io-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", + "integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==", + "requires": { + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + } + } + }, + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "socket.io-adapter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", + "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" + }, + "socket.io-client": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "dev": true, + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "socket.io-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.1.tgz", + "integrity": "sha512-1QLvVAe8dTz+mKmZ07Swxt+LAo4Y1ff50rlyoEx00TQmDFVQYPfcqGvIDJLGaBdhdNCecXtyKpD+EgKGcmmbuQ==", + "dev": true, + "requires": { + "component-emitter": "~1.3.0", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + } + } + }, + "socket.io-parser": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", + "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", + "requires": { + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -8528,6 +9297,11 @@ "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", "dev": true }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -9575,8 +10349,7 @@ "ws": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", - "integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==", - "dev": true + "integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==" }, "xml-name-validator": { "version": "3.0.0", @@ -9604,6 +10377,11 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -9700,6 +10478,11 @@ "decamelize": "^1.2.0" } }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index be08b3c..b11aa45 100644 --- a/package.json +++ b/package.json @@ -21,14 +21,16 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json --runInBand" }, "dependencies": { "@nestjs/common": "^7.5.1", "@nestjs/core": "^7.5.1", "@nestjs/platform-express": "^7.5.1", + "@nestjs/platform-socket.io": "^7.6.5", "@nestjs/swagger": "^4.7.8", "@nestjs/typeorm": "^7.1.5", + "@nestjs/websockets": "^7.6.5", "@types/jsonwebtoken": "^8.5.0", "class-transformer": "^0.3.1", "class-validator": "^0.12.2", @@ -42,6 +44,7 @@ "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^6.6.3", + "socket.io": "^2.3.0", "swagger-ui-express": "^4.1.5", "typeorm": "0.2.25", "uuid": "^8.3.2" @@ -55,6 +58,8 @@ "@types/helmet": "^4.0.0", "@types/jest": "^26.0.15", "@types/node": "^14.14.6", + "@types/socket.io": "^2.1.12", + "@types/socket.io-client": "^1.4.34", "@types/supertest": "^2.0.10", "@types/uuid": "^8.3.0", "@typescript-eslint/eslint-plugin": "^4.9.1", @@ -72,6 +77,7 @@ "jest-date-mock": "^1.0.8", "jest-extended": "^0.11.5", "prettier": "^2.1.2", + "socket.io-client": "^2.3.0", "supertest": "^6.0.0", "ts-jest": "^26.4.3", "ts-loader": "^8.0.8", diff --git a/setupTests.ts b/setupTests.ts index d2c9bc6..3f5e738 100644 --- a/setupTests.ts +++ b/setupTests.ts @@ -1 +1,4 @@ import 'reflect-metadata'; +import { Logger } from '@nestjs/common'; + +Logger.overrideLogger([]); diff --git a/src/ApplicationModule.ts b/src/ApplicationModule.ts index 580e25e..e003c40 100644 --- a/src/ApplicationModule.ts +++ b/src/ApplicationModule.ts @@ -4,12 +4,14 @@ import { ConnectionOptions } from 'typeorm'; import { JwtDecodeMiddleware } from './infrastructure/security/jwt/JwtDecodeMiddleware'; import { HttpApiModule } from './api/http/HttpApiModule'; import { VotingDomainModule } from './domain/VotingDomainModule'; -import { InfrastructureModule } from './infrastructure/InfrastructureModule'; // eslint-disable-line import/order +import { InfrastructureModule } from './infrastructure/InfrastructureModule'; +import { WebSocketDispatcherApiModule } from './api/ws/voting/dispatcher/WebSocketDispatcherApiModule'; +import { WebSocketReceiverApiModule } from './api/ws/voting/receiver/WebSocketReceiverApiModule'; // eslint-disable-line import/order, max-len // eslint-disable-next-line @typescript-eslint/no-var-requires const ormConfig = require('../ormconfig'); -const votingDomain = VotingDomainModule.forRoot([InfrastructureModule]); +const votingDomain = VotingDomainModule.forRoot([InfrastructureModule, WebSocketDispatcherApiModule]); @Module({ imports: [ @@ -21,6 +23,7 @@ const votingDomain = VotingDomainModule.forRoot([InfrastructureModule]); }, }), HttpApiModule.forRoot([votingDomain]), + WebSocketReceiverApiModule.forRoot([votingDomain]), ], }) export class ApplicationModule { diff --git a/src/api/AbstractExceptionFilter.ts b/src/api/AbstractExceptionFilter.ts new file mode 100644 index 0000000..6158852 --- /dev/null +++ b/src/api/AbstractExceptionFilter.ts @@ -0,0 +1,36 @@ +import { BaseExceptionFilter } from '@nestjs/core'; +import { ArgumentsHost } from '@nestjs/common'; + +export type ExceptionMap = Record Out>; + +export abstract class AbstractExceptionFilter extends BaseExceptionFilter { + /** + * Catches and processes the exception + * + * @param exception - Thrown error + * @param host - Argument host + */ + public catch(exception: Error, host: ArgumentsHost): void { + super.catch(this.transformException(exception), host); + } + + protected abstract get exceptionMap(): ExceptionMap; + + /** + * Transforms internal exceptions to related api exceptions + * + * @param exception - Thrown error + * @returns The transformed error + */ + private transformException(exception: Error): Error { + const httpExceptionFactoryMethodKey = Object.keys(this.exceptionMap).find( + (className) => exception.constructor.name === className, + ); + + if (!httpExceptionFactoryMethodKey) { + return exception; + } + + return this.exceptionMap[httpExceptionFactoryMethodKey](exception); + } +} diff --git a/src/api/http/rest/ApiExceptionFilter.spec.ts b/src/api/http/rest/ApiExceptionFilter.spec.ts index bd40d7c..40aa758 100644 --- a/src/api/http/rest/ApiExceptionFilter.spec.ts +++ b/src/api/http/rest/ApiExceptionFilter.spec.ts @@ -2,12 +2,14 @@ import { ArgumentsHost, HttpServer, Logger } from '@nestjs/common'; import { createMock } from '@golevelup/nestjs-testing'; import { TokenInvalidError } from '../../../infrastructure/security/jwt/TokenInvalidError'; import { TokenNotFoundError } from '../../../infrastructure/security/jwt/TokenNotFoundError'; -import { SessionNotFoundException } from '../../../domain'; -import { ParticipantForMandateNotExistingException } from '../../../domain/exception/ParticipantForMandateNotExistingException'; -import { ParticipantAlreadyExistsException } from '../../../domain/exception/ParticipantAlreadyExistsException'; -import { ParticipantDuplicatedException } from '../../../domain/exception/ParticipantDuplicatedException'; -import { TopicAlreadyExistsException } from '../../../domain/exception/TopicAlreadyExistsException'; -import { TopicDuplicatedException } from '../../../domain/exception/TopicDuplicatedException'; +import { + ParticipantAlreadyExistsException, + ParticipantDuplicatedException, + ParticipantForMandateNotExistingException, + SessionNotFoundException, + TopicAlreadyExistsException, + TopicDuplicatedException, +} from '../../../domain'; import { ApiExceptionFilter } from './ApiExceptionFilter'; describe('ApiExceptionFilter', () => { diff --git a/src/api/http/rest/ApiExceptionFilter.ts b/src/api/http/rest/ApiExceptionFilter.ts index 65f9326..c7de911 100644 --- a/src/api/http/rest/ApiExceptionFilter.ts +++ b/src/api/http/rest/ApiExceptionFilter.ts @@ -1,80 +1,51 @@ -import { BaseExceptionFilter } from '@nestjs/core'; -import { - ArgumentsHost, - BadRequestException, - Catch, - HttpException, - NotFoundException, - UnauthorizedException, -} from '@nestjs/common'; +import { BadRequestException, Catch, HttpException, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { TokenInvalidError } from '../../../infrastructure/security/jwt/TokenInvalidError'; import { TokenNotFoundError } from '../../../infrastructure/security/jwt/TokenNotFoundError'; -import { SessionNotFoundException } from '../../../domain'; -import { ParticipantForMandateNotExistingException } from '../../../domain/exception/ParticipantForMandateNotExistingException'; -import { ParticipantAlreadyExistsException } from '../../../domain/exception/ParticipantAlreadyExistsException'; -import { ParticipantDuplicatedException } from '../../../domain/exception/ParticipantDuplicatedException'; -import { TopicAlreadyExistsException } from '../../../domain/exception/TopicAlreadyExistsException'; -import { TopicDuplicatedException } from '../../../domain/exception/TopicDuplicatedException'; +import { + ParticipantAlreadyExistsException, + ParticipantDuplicatedException, + ParticipantForMandateNotExistingException, + SessionNotFoundException, + TopicAlreadyExistsException, + TopicDuplicatedException, +} from '../../../domain'; +import { AbstractExceptionFilter, ExceptionMap } from '../../AbstractExceptionFilter'; import { ExternalIdComposer } from './voting/session/factory/ExternalIdComposer'; @Catch() -export class ApiExceptionFilter extends BaseExceptionFilter { +export class ApiExceptionFilter extends AbstractExceptionFilter { private externalIdComposer = new ExternalIdComposer(); - private exceptionMap: Record HttpException> = { - [TokenInvalidError.name]: (e: Error) => new UnauthorizedException(e.message), - [TokenNotFoundError.name]: (e: Error) => new UnauthorizedException(e.message), - [SessionNotFoundException.name]: (e: Error) => new NotFoundException(e.message), - [ParticipantForMandateNotExistingException.name]: (e: ParticipantForMandateNotExistingException) => - new BadRequestException( - `Cannot create mandate for participant with id ${this.externalIdComposer.decompose( - e.id, - e.clientId, - )}. Participant does not exist`, - ), - [ParticipantAlreadyExistsException.name]: (e: ParticipantAlreadyExistsException) => - new BadRequestException( - `Participant with id ${this.externalIdComposer.decompose(e.id, e.clientId)} already exists`, - ), - [ParticipantDuplicatedException.name]: (e: ParticipantDuplicatedException) => - new BadRequestException( - `Participant with id ${this.externalIdComposer.decompose(e.id, e.clientId)} occurs multiple times`, - ), - [TopicAlreadyExistsException.name]: (e: TopicAlreadyExistsException) => - new BadRequestException( - `Topic with id ${this.externalIdComposer.decompose(e.id, e.clientId)} already exists`, - ), - [TopicDuplicatedException.name]: (e: TopicDuplicatedException) => - new BadRequestException( - `Topic with id ${this.externalIdComposer.decompose(e.id, e.clientId)} occurs multiple times`, - ), - }; - - /** - * Catches and processes the exception - * - * @param exception - Thrown error - * @param host - Argument host - */ - public catch(exception: Error, host: ArgumentsHost): void { - super.catch(this.transformException(exception), host); - } - - /** - * Transforms internal exceptions to related api exceptions - * - * @param exception - Thrown error - * @returns The transformed error - */ - private transformException(exception: Error): Error { - const httpExceptionFactoryMethodKey = Object.keys(this.exceptionMap).find( - (className) => exception.constructor.name === className, - ); - - if (!httpExceptionFactoryMethodKey) { - return exception; - } - - return this.exceptionMap[httpExceptionFactoryMethodKey](exception); + protected get exceptionMap(): ExceptionMap { + return { + [TokenInvalidError.name]: (e: Error): HttpException => new UnauthorizedException(e.message), + [TokenNotFoundError.name]: (e: Error): HttpException => new UnauthorizedException(e.message), + [SessionNotFoundException.name]: (e: Error): HttpException => new NotFoundException(e.message), + [ParticipantForMandateNotExistingException.name]: ( + e: ParticipantForMandateNotExistingException, + ): HttpException => + new BadRequestException( + `Cannot create mandate for participant with id ${this.externalIdComposer.decompose( + e.id, + e.clientId, + )}. Participant does not exist`, + ), + [ParticipantAlreadyExistsException.name]: (e: ParticipantAlreadyExistsException): HttpException => + new BadRequestException( + `Participant with id ${this.externalIdComposer.decompose(e.id, e.clientId)} already exists`, + ), + [ParticipantDuplicatedException.name]: (e: ParticipantDuplicatedException): HttpException => + new BadRequestException( + `Participant with id ${this.externalIdComposer.decompose(e.id, e.clientId)} occurs multiple times`, + ), + [TopicAlreadyExistsException.name]: (e: TopicAlreadyExistsException): HttpException => + new BadRequestException( + `Topic with id ${this.externalIdComposer.decompose(e.id, e.clientId)} already exists`, + ), + [TopicDuplicatedException.name]: (e: TopicDuplicatedException): HttpException => + new BadRequestException( + `Topic with id ${this.externalIdComposer.decompose(e.id, e.clientId)} occurs multiple times`, + ), + }; } } diff --git a/src/api/ws/WebSocketExceptionFilter.spec.ts b/src/api/ws/WebSocketExceptionFilter.spec.ts new file mode 100644 index 0000000..17a829e --- /dev/null +++ b/src/api/ws/WebSocketExceptionFilter.spec.ts @@ -0,0 +1,90 @@ +import { ArgumentsHost, Logger } from '@nestjs/common'; +import { createMock } from '@golevelup/nestjs-testing'; +import { WsArgumentsHost } from '@nestjs/common/interfaces'; +import { TokenInvalidError } from '../../infrastructure/security/jwt/TokenInvalidError'; +import { TokenNotFoundError } from '../../infrastructure/security/jwt/TokenNotFoundError'; +import { SessionNotFoundException } from '../../domain'; +import { WebSocketExceptionFilter } from './WebSocketExceptionFilter'; +import { ClientNotAuthorizedForSessionException } from './voting/exception/ClientNotAuthorizedForSessionException'; +import { ParticipantNotAuthorizedForSessionException } from './voting/exception/ParticipantNotAuthorizedForSessionException'; + +describe('WebSocketExceptionFilter', () => { + let filter: WebSocketExceptionFilter; + let argumentsHost: ArgumentsHost; + let client: { emit: (event: string, message: any) => void }; + + beforeEach(() => { + client = { + emit: jest.fn(), + }; + argumentsHost = createMock({ + switchToWs(): WsArgumentsHost { + return createMock({ + getClient() { + return client; + }, + }); + }, + }); + + filter = new WebSocketExceptionFilter(); + Logger.overrideLogger([]); + }); + + it('should be defined', () => { + expect(filter).toBeInstanceOf(WebSocketExceptionFilter); + }); + + it('should return the original exception when not in the map', () => { + const error = new Error('unknown'); + + filter.catch(error, argumentsHost); + + expect(client.emit).toHaveBeenCalledWith('exception', { status: 'error', message: 'unknown' }); + }); + + it('should transform a TokenInvalidError', () => { + filter.catch(new TokenInvalidError('token invalid'), argumentsHost); + + expect(client.emit).toHaveBeenCalledWith('exception', { + status: 'error', + message: 'Token is invalid: token invalid', + }); + }); + + it('should transform a TokenNotFoundError', () => { + filter.catch(new TokenNotFoundError(), argumentsHost); + + expect(client.emit).toHaveBeenCalledWith('exception', { + status: 'error', + message: 'Token not found', + }); + }); + + it('should transform a SessionNotFoundException', () => { + filter.catch(new SessionNotFoundException('sessionId'), argumentsHost); + + expect(client.emit).toHaveBeenCalledWith('exception', { + status: 'error', + message: 'Session with the id sessionId not found', + }); + }); + + it('should transform a ClientNotAuthorizedForSessionException', () => { + filter.catch(new ClientNotAuthorizedForSessionException(), argumentsHost); + + expect(client.emit).toHaveBeenCalledWith('exception', { + status: 'error', + message: 'Client is not authorized to connect to requested session', + }); + }); + + it('should transform a ParticipantNotAuthorizedForSessionException', () => { + filter.catch(new ParticipantNotAuthorizedForSessionException(), argumentsHost); + + expect(client.emit).toHaveBeenCalledWith('exception', { + status: 'error', + message: 'Participant is not authorized to connect to requested session', + }); + }); +}); diff --git a/src/api/ws/WebSocketExceptionFilter.ts b/src/api/ws/WebSocketExceptionFilter.ts new file mode 100644 index 0000000..120a277 --- /dev/null +++ b/src/api/ws/WebSocketExceptionFilter.ts @@ -0,0 +1,15 @@ +import { ArgumentsHost, Catch } from '@nestjs/common'; +import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets'; + +@Catch() +export class WebSocketExceptionFilter extends BaseWsExceptionFilter { + /** + * Catches and processes the exception + * + * @param exception - Thrown error + * @param host - Argument host + */ + public catch(exception: Error, host: ArgumentsHost): void { + super.catch(new WsException(exception.message), host); + } +} diff --git a/src/api/ws/voting/AuthorizedSocket.ts b/src/api/ws/voting/AuthorizedSocket.ts new file mode 100644 index 0000000..97f6581 --- /dev/null +++ b/src/api/ws/voting/AuthorizedSocket.ts @@ -0,0 +1,6 @@ +import { Socket } from 'socket.io'; +import { ClientTokenPayload } from '../../../infrastructure/security/clientToken/ClientTokenPayload'; + +export interface AuthorizedSocket extends Socket { + token: ClientTokenPayload; +} diff --git a/src/api/ws/voting/dispatcher/WebSocketDispatcherApiModule.ts b/src/api/ws/voting/dispatcher/WebSocketDispatcherApiModule.ts new file mode 100644 index 0000000..4d48342 --- /dev/null +++ b/src/api/ws/voting/dispatcher/WebSocketDispatcherApiModule.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AbstractSessionMessagingGateway } from '../../../../domain'; +import { SessionGateway } from './gateway/SessionGateway'; + +@Module({ + providers: [{ provide: AbstractSessionMessagingGateway, useClass: SessionGateway }], + exports: [AbstractSessionMessagingGateway], +}) +export class WebSocketDispatcherApiModule {} diff --git a/src/api/ws/voting/dispatcher/gateway/SessionGateway.spec.ts b/src/api/ws/voting/dispatcher/gateway/SessionGateway.spec.ts new file mode 100644 index 0000000..c49dc86 --- /dev/null +++ b/src/api/ws/voting/dispatcher/gateway/SessionGateway.spec.ts @@ -0,0 +1,13 @@ +import { SessionGateway } from './SessionGateway'; + +describe('SessionGateway', () => { + let gateway: SessionGateway; + + beforeEach(() => { + gateway = new SessionGateway(); + }); + + it('should be instantiable', () => { + expect(gateway).toBeInstanceOf(SessionGateway); + }); +}); diff --git a/src/api/ws/voting/dispatcher/gateway/SessionGateway.ts b/src/api/ws/voting/dispatcher/gateway/SessionGateway.ts new file mode 100644 index 0000000..e19cb3e --- /dev/null +++ b/src/api/ws/voting/dispatcher/gateway/SessionGateway.ts @@ -0,0 +1,8 @@ +import { WebSocketGateway } from '@nestjs/websockets'; +import { AbstractSessionMessagingGateway } from '../../../../../domain'; + +@WebSocketGateway() +export class SessionGateway extends AbstractSessionMessagingGateway { + // @WebSocketServer() + // private readonly server: Server; +} diff --git a/src/api/ws/voting/exception/ClientNotAuthorizedForSessionException.ts b/src/api/ws/voting/exception/ClientNotAuthorizedForSessionException.ts new file mode 100644 index 0000000..ecdee87 --- /dev/null +++ b/src/api/ws/voting/exception/ClientNotAuthorizedForSessionException.ts @@ -0,0 +1,7 @@ +import { WsException } from '@nestjs/websockets'; + +export class ClientNotAuthorizedForSessionException extends WsException { + public constructor() { + super('Client is not authorized to connect to requested session'); + } +} diff --git a/src/api/ws/voting/exception/ParticipantNotAuthorizedForSessionException.ts b/src/api/ws/voting/exception/ParticipantNotAuthorizedForSessionException.ts new file mode 100644 index 0000000..0c197ff --- /dev/null +++ b/src/api/ws/voting/exception/ParticipantNotAuthorizedForSessionException.ts @@ -0,0 +1,7 @@ +import { WsException } from '@nestjs/websockets'; + +export class ParticipantNotAuthorizedForSessionException extends WsException { + public constructor() { + super('Participant is not authorized to connect to requested session'); + } +} diff --git a/src/api/ws/voting/receiver/WebSocketReceiverApiModule.ts b/src/api/ws/voting/receiver/WebSocketReceiverApiModule.ts new file mode 100644 index 0000000..49c2e1b --- /dev/null +++ b/src/api/ws/voting/receiver/WebSocketReceiverApiModule.ts @@ -0,0 +1,15 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { ImportModule } from '../../../../util/ImportModule'; +import { VotingGateway } from './gateway/VotingGateway'; + +@Module({ + providers: [VotingGateway], +}) +export class WebSocketReceiverApiModule { + public static forRoot(imports: ImportModule[]): DynamicModule { + return { + module: WebSocketReceiverApiModule, + imports, + }; + } +} diff --git a/src/api/ws/voting/receiver/gateway/VotingGateway.spec.ts b/src/api/ws/voting/receiver/gateway/VotingGateway.spec.ts new file mode 100644 index 0000000..7d5e510 --- /dev/null +++ b/src/api/ws/voting/receiver/gateway/VotingGateway.spec.ts @@ -0,0 +1,108 @@ +import { createMock } from '@golevelup/nestjs-testing'; +import { Handshake } from 'socket.io'; +import { decodeClientPayload } from '../../../../../infrastructure/security/clientToken/decodeClientPayload'; +import { Participant, Session, SessionService } from '../../../../../domain'; +import { TokenInvalidError } from '../../../../../infrastructure/security/jwt/TokenInvalidError'; +import { AuthorizedSocket } from '../../AuthorizedSocket'; +import { ParticipantNotAuthorizedForSessionException } from '../../exception/ParticipantNotAuthorizedForSessionException'; +import { ClientNotAuthorizedForSessionException } from '../../exception/ClientNotAuthorizedForSessionException'; +import { VotingGateway } from './VotingGateway'; + +jest.mock('../../../../../infrastructure/security/clientToken/decodeClientPayload', () => ({ + decodeClientPayload: jest.fn().mockReturnValue({ + sub: 'clientId', + exp: new Date(new Date().getDate() + 1).toISOString(), + sess: 'sessionId', + ptc: 'participantId', + }), +})); + +describe('VotingGateway', () => { + let gateway: VotingGateway; + let sessionService: SessionService; + let socket: AuthorizedSocket; + + beforeEach(() => { + (decodeClientPayload as jest.Mock).mockClear(); + socket = createMock({ + handshake: createMock({ + headers: { + authorization: 'auth-token', + }, + }), + }); + sessionService = createMock(); + gateway = new VotingGateway(sessionService); + }); + + it('should be instantiable', () => { + expect(gateway).toBeInstanceOf(VotingGateway); + }); + + it('should handle a undefined headers object', async () => { + (decodeClientPayload as jest.Mock).mockImplementationOnce(() => { + throw new TokenInvalidError('error'); + }); + + const socketWithoutHeaders = createMock({ + handshake: ({} as unknown) as Handshake, + }); + + await expect(gateway.handleConnection(socketWithoutHeaders)).rejects.toThrow(TokenInvalidError); + expect(decodeClientPayload).toHaveBeenCalledWith(undefined); + }); + + it('should disconnect if the authorization token is invalid', async () => { + (decodeClientPayload as jest.Mock).mockImplementationOnce(() => { + throw new TokenInvalidError('error'); + }); + + await expect(gateway.handleConnection(socket)).rejects.toThrow(TokenInvalidError); + + expect(decodeClientPayload).toHaveBeenCalledWith('auth-token'); + + expect(socket.disconnect).toHaveBeenCalledTimes(1); + expect(socket.disconnect).toHaveBeenCalledWith(true); + + expect(socket.join).not.toHaveBeenCalled(); + }); + + it('should throw and disconnect if the session does not include the participant', async () => { + const session = new Session('clientId', new Date()); + jest.spyOn(session, 'hasParticipant').mockReturnValue(false); + jest.spyOn(sessionService, 'findById').mockResolvedValue(session); + + await expect(gateway.handleConnection(socket)).rejects.toThrow(ParticipantNotAuthorizedForSessionException); + + expect(socket.disconnect).toHaveBeenCalledTimes(1); + expect(socket.disconnect).toHaveBeenCalledWith(true); + + expect(socket.join).not.toHaveBeenCalled(); + }); + + it('should throw and disconnect if the session does not belong to the client', async () => { + const session = new Session('otherClientId', new Date(), undefined, 'sessionId', [ + new Participant('externalParticipantId', 1, 'participantId'), + ]); + jest.spyOn(sessionService, 'findById').mockResolvedValue(session); + + await expect(gateway.handleConnection(socket)).rejects.toThrow(ClientNotAuthorizedForSessionException); + + expect(socket.disconnect).toHaveBeenCalledTimes(1); + expect(socket.disconnect).toHaveBeenCalledWith(true); + + expect(socket.join).not.toHaveBeenCalled(); + }); + + it('should connect to the session channel', async () => { + const session = new Session('clientId', new Date(), undefined, 'sessionId', [ + new Participant('externalParticipantId', 1, 'participantId'), + ]); + jest.spyOn(sessionService, 'findById').mockResolvedValue(session); + + await gateway.handleConnection(socket); + + expect(socket.disconnect).not.toHaveBeenCalled(); + expect(socket.join).toHaveBeenCalledWith('sessionId'); + }); +}); diff --git a/src/api/ws/voting/receiver/gateway/VotingGateway.ts b/src/api/ws/voting/receiver/gateway/VotingGateway.ts new file mode 100644 index 0000000..5328d17 --- /dev/null +++ b/src/api/ws/voting/receiver/gateway/VotingGateway.ts @@ -0,0 +1,64 @@ +import { IncomingMessage, ServerResponse } from 'http'; +import { ConnectedSocket, WebSocketGateway } from '@nestjs/websockets'; +import { Logger } from '@nestjs/common'; +import { AuthorizedSocket } from '../../AuthorizedSocket'; +import { decodeClientPayload } from '../../../../../infrastructure/security/clientToken/decodeClientPayload'; +import { SessionService } from '../../../../../domain'; +import { ParticipantNotAuthorizedForSessionException } from '../../exception/ParticipantNotAuthorizedForSessionException'; +import { ClientNotAuthorizedForSessionException } from '../../exception/ClientNotAuthorizedForSessionException'; +import { ClientTokenPayload } from '../../../../../infrastructure/security/clientToken/ClientTokenPayload'; + +@WebSocketGateway({ + /* istanbul ignore next */ + handlePreflightRequest(req: IncomingMessage, res: ServerResponse) { + /* eslint-disable @typescript-eslint/naming-convention */ + const headers = { + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Access-Control-Allow-Origin': req.headers.origin, + 'Access-Control-Allow-Credentials': 'true', + }; + /* eslint-enable @typescript-eslint/naming-convention */ + res.writeHead(200, headers); + res.end(); + }, +}) +export class VotingGateway { + private readonly logger = new Logger(VotingGateway.name); + + public constructor(private readonly sessionService: SessionService) {} + + public async handleConnection(@ConnectedSocket() connectedSocket: AuthorizedSocket): Promise { + try { + const token = this.decodeToken(connectedSocket); + await this.connectToSessionChannel(connectedSocket, token); + + this.logger.debug(`Socket client connected. Id: ${connectedSocket.id}`); + } catch (e) { + this.logger.debug(e.message); + + connectedSocket.disconnect(true); + } + } + + private decodeToken(connectedSocket: AuthorizedSocket): ClientTokenPayload { + const authorizationHeader = connectedSocket.handshake.headers?.authorization; + const decodedToken = decodeClientPayload(authorizationHeader); + + // eslint-disable-next-line no-param-reassign + connectedSocket.token = decodedToken; + + return decodedToken; + } + + private async connectToSessionChannel(connectedSocket: AuthorizedSocket, token: ClientTokenPayload): Promise { + const session = await this.sessionService.findById(token.sess); + if (!session.hasParticipant(token.ptc)) { + throw new ParticipantNotAuthorizedForSessionException(); + } + if (session.getClientId() !== token.sub) { + throw new ClientNotAuthorizedForSessionException(); + } + + connectedSocket.join(session.getId()); + } +} diff --git a/src/domain/index.ts b/src/domain/index.ts index bc046bc..cb9fe97 100644 --- a/src/domain/index.ts +++ b/src/domain/index.ts @@ -6,3 +6,9 @@ export { Topic } from './model/Topic'; export { Mandate } from './model/Mandate'; export { MajorityType, Majority } from './model/Majority'; export { SessionNotFoundException } from './exception/SessionNotFoundException'; +export { ParticipantDuplicatedException } from './exception/ParticipantDuplicatedException'; +export { ParticipantAlreadyExistsException } from './exception/ParticipantAlreadyExistsException'; +export { TopicDuplicatedException } from './exception/TopicDuplicatedException'; +export { TopicAlreadyExistsException } from './exception/TopicAlreadyExistsException'; +export { ParticipantForMandateNotExistingException } from './exception/ParticipantForMandateNotExistingException'; +export { AbstractSessionMessagingGateway } from './messaging/AbstractSessionMessagingGateway'; diff --git a/src/domain/messaging/AbstractSessionMessagingGateway.ts b/src/domain/messaging/AbstractSessionMessagingGateway.ts new file mode 100644 index 0000000..7cef20f --- /dev/null +++ b/src/domain/messaging/AbstractSessionMessagingGateway.ts @@ -0,0 +1 @@ +export abstract class AbstractSessionMessagingGateway {} diff --git a/src/domain/model/Session.spec.ts b/src/domain/model/Session.spec.ts index 73c7fa9..0488909 100644 --- a/src/domain/model/Session.spec.ts +++ b/src/domain/model/Session.spec.ts @@ -66,6 +66,14 @@ describe('Session', () => { expect(session.getParticipants()).toContain(participant); }); + it('should check if a participant by id is included in the session', () => { + const participant = new Participant('abc', 1, 'participantId'); + session.setParticipants([participant]); + + expect(session.hasParticipant('participantId')).toBeTrue(); + expect(session.hasParticipant('notIncluded')).toBeFalse(); + }); + it('should be able to add a participant', () => { expect(session.getParticipants()).toBeArray(); expect(session.getParticipants()).toHaveLength(0); diff --git a/src/domain/model/Session.ts b/src/domain/model/Session.ts index 2136df1..c2d7181 100644 --- a/src/domain/model/Session.ts +++ b/src/domain/model/Session.ts @@ -80,6 +80,10 @@ export class Session { return this; } + public hasParticipant(participantId: string): boolean { + return this.participants.findIndex((participant) => participant.getId() === participantId) > -1; + } + public getTopics(): Topic[] { return this.topics; } diff --git a/src/domain/service/SessionService.spec.ts b/src/domain/service/SessionService.spec.ts index 2049ac2..6796623 100644 --- a/src/domain/service/SessionService.spec.ts +++ b/src/domain/service/SessionService.spec.ts @@ -229,4 +229,15 @@ describe('SessionService', () => { expect(persistenceService.save).not.toHaveBeenCalled(); }); + + it('should find a session by id', async () => { + const session = new Session('clientId', new Date()); + jest.spyOn(persistenceService, 'findById').mockResolvedValue(session); + + const foundSession = await service.findById('mySession'); + + expect(persistenceService.findById).toHaveBeenCalledWith('mySession'); + expect(persistenceService.findById).toHaveBeenCalledTimes(1); + expect(foundSession).toEqual(session); + }); }); diff --git a/src/domain/service/SessionService.ts b/src/domain/service/SessionService.ts index 35db45c..6b9faa0 100644 --- a/src/domain/service/SessionService.ts +++ b/src/domain/service/SessionService.ts @@ -57,6 +57,10 @@ export class SessionService { .find((savedParticipant) => savedParticipant.getExternalId() === participant.getExternalId()); } + public async findById(sessionId: string): Promise { + return this.sessionPersistenceService.findById(sessionId); + } + private validateParticipants(session: Session): void { const externalParticipantIds = new Set(); diff --git a/src/infrastructure/security/clientToken/ClientTokenPayload.ts b/src/infrastructure/security/clientToken/ClientTokenPayload.ts new file mode 100644 index 0000000..b6d3f91 --- /dev/null +++ b/src/infrastructure/security/clientToken/ClientTokenPayload.ts @@ -0,0 +1,6 @@ +export interface ClientTokenPayload { + sub: string; // Client id + exp: number; // Token expiry + sess: string; // session id + ptc: string; // participant id +} diff --git a/src/infrastructure/security/clientToken/decodeClientPayload.spec.ts b/src/infrastructure/security/clientToken/decodeClientPayload.spec.ts new file mode 100644 index 0000000..476c056 --- /dev/null +++ b/src/infrastructure/security/clientToken/decodeClientPayload.spec.ts @@ -0,0 +1,129 @@ +import { sign } from 'jsonwebtoken'; +import { TokenInvalidError } from '../jwt/TokenInvalidError'; +import { decodeClientPayload } from './decodeClientPayload'; + +describe('decodeClientPayload', () => { + it('should throw if the token is undefined', () => { + expect(() => decodeClientPayload(undefined)).toThrow(TokenInvalidError); + }); + + it('should throw if a token is not encoded with the valid secret', () => { + const token = sign( + { + sub: 'clientId', + exp: new Date(new Date().getTime() + new Date('1970-01-02').getTime()).getTime(), + sess: 'sessionId', + ptc: 'participantId', + }, + 'invalidSecret', + ); + + expect(() => decodeClientPayload(token)).toThrow(TokenInvalidError); + }); + + it('should throw if the token is expired', () => { + const token = sign( + { + sub: 'clientId', + exp: new Date(new Date().getTime() - new Date('1970-01-02').getTime()).getTime(), + sess: 'sessionId', + ptc: 'participantId', + }, + process.env.JWT_CLIENT_TOKEN_SECRET, + ); + + expect(() => decodeClientPayload(token)).toThrow(TokenInvalidError); + }); + + it('should throw if the token has no sub', () => { + const token = sign( + { + exp: new Date(new Date().getTime() + new Date('1970-01-02').getTime()).getTime(), + sess: 'sessionId', + ptc: 'participantId', + }, + process.env.JWT_CLIENT_TOKEN_SECRET, + ); + + expect(() => decodeClientPayload(token)).toThrow(TokenInvalidError); + }); + + it('should throw if the token has no expiry', () => { + const token = sign( + { + sub: 'clientId', + sess: 'sessionId', + ptc: 'participantId', + }, + process.env.JWT_CLIENT_TOKEN_SECRET, + ); + + expect(() => decodeClientPayload(token)).toThrow(TokenInvalidError); + }); + + it('should throw if the token has no session id', () => { + const token = sign( + { + sub: 'clientId', + exp: new Date(new Date().getTime() + new Date('1970-01-02').getTime()).getTime(), + ptc: 'participantId', + }, + process.env.JWT_CLIENT_TOKEN_SECRET, + ); + + expect(() => decodeClientPayload(token)).toThrow(TokenInvalidError); + }); + + it('should throw if the token has no participant id', () => { + const token = sign( + { + sub: 'clientId', + exp: new Date(new Date().getTime() + new Date('1970-01-02').getTime()).getTime(), + sess: 'sessionId', + }, + process.env.JWT_CLIENT_TOKEN_SECRET, + ); + + expect(() => decodeClientPayload(token)).toThrow(TokenInvalidError); + }); + + it('should return the decoded token', () => { + const exp = new Date(new Date().getTime() + new Date('1970-01-02').getTime()).getTime(); + const token = sign( + { + sub: 'clientId', + exp, + sess: 'sessionId', + ptc: 'participantId', + }, + process.env.JWT_CLIENT_TOKEN_SECRET, + ); + + const decoded = decodeClientPayload(token); + + expect(decoded.sub).toEqual('clientId'); + expect(decoded.exp).toEqual(exp); + expect(decoded.sess).toEqual('sessionId'); + expect(decoded.ptc).toEqual('participantId'); + }); + + it('should support a bearer prefix', () => { + const exp = new Date(new Date().getTime() + new Date('1970-01-02').getTime()).getTime(); + const token = sign( + { + sub: 'clientId', + exp, + sess: 'sessionId', + ptc: 'participantId', + }, + process.env.JWT_CLIENT_TOKEN_SECRET, + ); + + const decoded = decodeClientPayload(`Bearer ${token}`); + + expect(decoded.sub).toEqual('clientId'); + expect(decoded.exp).toEqual(exp); + expect(decoded.sess).toEqual('sessionId'); + expect(decoded.ptc).toEqual('participantId'); + }); +}); diff --git a/src/infrastructure/security/clientToken/decodeClientPayload.ts b/src/infrastructure/security/clientToken/decodeClientPayload.ts new file mode 100644 index 0000000..886260a --- /dev/null +++ b/src/infrastructure/security/clientToken/decodeClientPayload.ts @@ -0,0 +1,37 @@ +import { verify } from 'jsonwebtoken'; +import Joi from 'joi'; +import { TokenInvalidError } from '../jwt/TokenInvalidError'; +import { ClientTokenPayload } from './ClientTokenPayload'; + +function verifyToken(token: string): ClientTokenPayload { + try { + return verify(token, process.env.JWT_CLIENT_TOKEN_SECRET) as ClientTokenPayload; + } catch (e) { + throw new TokenInvalidError(e.message); + } +} + +export function decodeClientPayload(authorizationHeader: string | undefined): ClientTokenPayload { + const token = authorizationHeader?.replace(/^Bearer /i, ''); + + const payload = verifyToken(token); + + const validation = Joi.object({ + sub: Joi.string().required(), + exp: Joi.number().required(), + sess: Joi.string().required(), + ptc: Joi.string().required(), + }).required(); + + const { value, error } = validation.validate(payload, { allowUnknown: true, stripUnknown: false }); + + if (error) { + throw new TokenInvalidError(error.message); + } + + if (new Date().getTime() > new Date(value.exp).getTime()) { + throw new TokenInvalidError('Token expired'); + } + + return value; +} diff --git a/test/connect-to-session.e2e-spec.ts b/test/connect-to-session.e2e-spec.ts new file mode 100644 index 0000000..ccd51d0 --- /dev/null +++ b/test/connect-to-session.e2e-spec.ts @@ -0,0 +1,65 @@ +import request from 'supertest'; +import { Socket, connect } from 'socket.io-client'; +import { INestApplication } from '@nestjs/common'; +import { sign } from 'jsonwebtoken'; + +declare const validToken: string; +declare const socketUrl: string; +declare const app: INestApplication; + +describe('Connect to session', () => { + let session: { id: string }; + const exp = new Date(new Date().getTime() + new Date('1970-01-02').getTime()).getTime(); + let clientToken: string; + let unauthenticatedClient: typeof Socket; + let client: typeof Socket; + + beforeAll(async () => { + const response = await request(app.getHttpServer()) + .post('/api/v1/sessions') + .set('Authorization', validToken) + .send({ + start: '2020-12-24T10:00:00.000Z', + participants: [ + { id: 'participant1', shares: 1 }, + { id: 'participant2', shares: 1 }, + ], + }); + + session = response.body; + + clientToken = sign( + { sub: 'valid-user-id', exp, sess: session.id, ptc: 'participant1' }, + process.env.JWT_CLIENT_TOKEN_SECRET, + ); + }); + + afterAll(async () => { + client.close(); + unauthenticatedClient.close(); + }); + + it('should not be able to connect with an invalid authorization header', () => + new Promise((resolve) => { + unauthenticatedClient = connect(socketUrl); + + unauthenticatedClient.on('disconnect', () => { + expect(true).toBeTrue(); + resolve(true); + unauthenticatedClient.disconnect().close(); + }); + })); + + it('should be able to connect with a valid authorization header', () => + new Promise((resolve) => { + client = connect(socketUrl, { + transportOptions: { polling: { extraHeaders: { authorization: `Bearer ${clientToken}` } } }, + }); + + client.on('connect', () => { + expect(true).toBeTrue(); + resolve(true); + client.disconnect().close(); + }); + })); +}); diff --git a/test/jest.setup.e2e.after-env.ts b/test/jest.setup.e2e.after-env.ts index 7653de3..7458aca 100644 --- a/test/jest.setup.e2e.after-env.ts +++ b/test/jest.setup.e2e.after-env.ts @@ -5,6 +5,8 @@ import { ApplicationModule } from '../src/ApplicationModule'; (global as any).app = undefined; (global as any).validToken = undefined; +(global as any).socketPort = 3002; +(global as any).socketUrl = `ws://localhost:${(global as any).socketPort}`; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ @@ -15,6 +17,7 @@ beforeAll(async () => { app.useGlobalPipes(new ValidationPipe({ transform: true })); await app.init(); + await app.listen((global as any).socketPort); (global as any).app = app; (global as any).validToken = sign({ sub: 'valid-user-id' }, 'internal-secret'); From 23ed4ca5056a258c6f3305e2f708102c7c2e8224 Mon Sep 17 00:00:00 2001 From: Maximilian Beck Date: Mon, 4 Jan 2021 00:18:53 +0100 Subject: [PATCH 3/3] #20: Improve expect statements for socket disconnect events --- package.json | 2 +- test/connect-to-session.e2e-spec.ts | 25 ++++++++++++++++--------- test/jest.setup.e2e.after-env.ts | 3 ++- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index b11aa45..834a559 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json --runInBand" + "test:e2e": "jest --config ./test/jest-e2e.json --runInBand --detectOpenHandles --forceExit" }, "dependencies": { "@nestjs/common": "^7.5.1", diff --git a/test/connect-to-session.e2e-spec.ts b/test/connect-to-session.e2e-spec.ts index ccd51d0..8c4454a 100644 --- a/test/connect-to-session.e2e-spec.ts +++ b/test/connect-to-session.e2e-spec.ts @@ -40,26 +40,33 @@ describe('Connect to session', () => { }); it('should not be able to connect with an invalid authorization header', () => - new Promise((resolve) => { + new Promise((resolve, reject) => { unauthenticatedClient = connect(socketUrl); - unauthenticatedClient.on('disconnect', () => { - expect(true).toBeTrue(); - resolve(true); - unauthenticatedClient.disconnect().close(); + unauthenticatedClient.on('disconnect', (reason: string) => { + expect(reason).toBe('io server disconnect'); + resolve(reason); }); + + unauthenticatedClient.on('error', reject); + unauthenticatedClient.on('exception', reject); })); it('should be able to connect with a valid authorization header', () => - new Promise((resolve) => { + new Promise((resolve, reject) => { client = connect(socketUrl, { transportOptions: { polling: { extraHeaders: { authorization: `Bearer ${clientToken}` } } }, }); client.on('connect', () => { - expect(true).toBeTrue(); - resolve(true); - client.disconnect().close(); + client.disconnect(); + }); + + client.on('disconnect', (reason: string) => { + expect(reason).toBe('io client disconnect'); + resolve(reason); }); + client.on('error', reject); + client.on('exception', reject); })); }); diff --git a/test/jest.setup.e2e.after-env.ts b/test/jest.setup.e2e.after-env.ts index 7458aca..b23a455 100644 --- a/test/jest.setup.e2e.after-env.ts +++ b/test/jest.setup.e2e.after-env.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ValidationPipe } from '@nestjs/common'; +import { Logger, ValidationPipe } from '@nestjs/common'; import { sign } from 'jsonwebtoken'; import { ApplicationModule } from '../src/ApplicationModule'; @@ -9,6 +9,7 @@ import { ApplicationModule } from '../src/ApplicationModule'; (global as any).socketUrl = `ws://localhost:${(global as any).socketPort}`; beforeAll(async () => { + Logger.overrideLogger([]); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ApplicationModule], }).compile();