From 9d022dc17945d9338d19bec71feed7dca20ae608 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Sun, 9 Feb 2025 21:02:26 +0900 Subject: [PATCH 01/26] =?UTF-8?q?fix:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=B4=EB=82=B4=EC=A7=80=20=EC=95=8A=EA=B3=A0=20?= =?UTF-8?q?=EB=A6=AC=ED=84=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/board-trees/board-trees.gateway.ts | 9 ++++++--- nestjs-BE/server/test/board-trees.e2e-spec.ts | 16 ++-------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts index 72eabc92..4e96fd9f 100644 --- a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts +++ b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts @@ -1,5 +1,6 @@ import { JwtService } from '@nestjs/jwt'; import { + MessageBody, OnGatewayConnection, OnGatewayInit, SubscribeMessage, @@ -56,12 +57,14 @@ export class BoardTreesGateway implements OnGatewayInit, OnGatewayConnection { async handleCreateOperation(client: Socket, operation: BoardOperation) { await this.boardTreesService.createOperationLog(operation); client.broadcast.to(operation.boardId).emit('operation', operation); - client.emit('operationCreated'); + return { status: true }; } @SubscribeMessage('getOperations') - async handleGetOperations(client: Socket, boardId: string) { + async handleGetOperations( + @MessageBody() boardId: string, + ): Promise { const operations = await this.boardTreesService.getOperationLogs(boardId); - client.emit('getOperations', operations); + return operations; } } diff --git a/nestjs-BE/server/test/board-trees.e2e-spec.ts b/nestjs-BE/server/test/board-trees.e2e-spec.ts index 8e7b89b9..6cbeb363 100644 --- a/nestjs-BE/server/test/board-trees.e2e-spec.ts +++ b/nestjs-BE/server/test/board-trees.e2e-spec.ts @@ -164,13 +164,7 @@ describe('BoardTreesGateway (e2e)', () => { content: 'new node', }; - await new Promise((resolve) => { - client.on('operationCreated', () => { - resolve(null); - }); - - client.emit('createOperation', testOperation); - }); + await client.emitWithAck('createOperation', testOperation); const operations = await boardTreesService.getOperationLogs( testOperation.boardId, @@ -240,13 +234,7 @@ describe('BoardTreesGateway (e2e)', () => { }); it('get operation logs', async () => { - const response = await new Promise((resolve) => { - client.on('getOperations', (operationLogs) => { - resolve(operationLogs); - }); - - client.emit('getOperations', boardId); - }); + const response = await client.emitWithAck('getOperations', boardId); expect(response).toEqual(expect.arrayContaining(testOperations)); }); From 27200544b544ebd1686eb79cb3fec9a1fca98401 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Sun, 9 Feb 2025 21:05:50 +0900 Subject: [PATCH 02/26] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=94=84=EB=A1=9C=ED=8D=BC?= =?UTF-8?q?=ED=8B=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/src/board-trees/board-trees.gateway.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts index 4e96fd9f..da9446ac 100644 --- a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts +++ b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts @@ -5,7 +5,6 @@ import { OnGatewayInit, SubscribeMessage, WebSocketGateway, - WebSocketServer, WsException, } from '@nestjs/websockets'; import { ConfigService } from '@nestjs/config'; @@ -21,9 +20,6 @@ export class BoardTreesGateway implements OnGatewayInit, OnGatewayConnection { private configService: ConfigService, ) {} - @WebSocketServer() - server: Server; - afterInit(server: Server) { server.use((socket, next) => { const token = socket.handshake.auth.token; From 0190a9e2be63291c967506010aa7a666ff831575 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Sun, 9 Feb 2025 21:21:11 +0900 Subject: [PATCH 03/26] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/src/board-trees/board-trees.gateway.ts | 4 ++-- nestjs-BE/server/test/board-trees.e2e-spec.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts index da9446ac..496de526 100644 --- a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts +++ b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts @@ -42,11 +42,11 @@ export class BoardTreesGateway implements OnGatewayInit, OnGatewayConnection { const boardId = query.boardId; if (!boardId) { - client.emit('board_id_required', new WsException('board id required')); + client.emit('boardIdRequired', new WsException('board id required')); client.disconnect(); } client.join(boardId); - client.emit('board_joined', boardId); + client.emit('boardJoined', boardId); } @SubscribeMessage('createOperation') diff --git a/nestjs-BE/server/test/board-trees.e2e-spec.ts b/nestjs-BE/server/test/board-trees.e2e-spec.ts index 6cbeb363..9fe8433c 100644 --- a/nestjs-BE/server/test/board-trees.e2e-spec.ts +++ b/nestjs-BE/server/test/board-trees.e2e-spec.ts @@ -103,12 +103,12 @@ describe('BoardTreesGateway (e2e)', () => { testToken = await createUserToken(prisma, config); }); - it('board_id_required error when board id not included', async () => { + it('boardIdRequired error when board id not included', async () => { const error = new Promise((resolve, reject) => { const socket = io(serverUrl, { auth: { token: testToken }, }); - socket.on('board_id_required', (error) => { + socket.on('boardIdRequired', (error) => { reject(error); }); }); @@ -127,7 +127,7 @@ describe('BoardTreesGateway (e2e)', () => { auth: { token: testToken }, query: { boardId }, }); - socket.on('board_joined', (boardId) => { + socket.on('boardJoined', (boardId) => { socket.disconnect(); resolve(boardId); }); @@ -258,7 +258,7 @@ async function createClientSocket( let client: Socket; await new Promise((resolve) => { client = io(uri, opts); - client.on('board_joined', () => { + client.on('boardJoined', () => { resolve(null); }); }); From fa3f34e66eed4686902e8a37bad658a7ac2d9554 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Tue, 11 Feb 2025 17:38:38 +0900 Subject: [PATCH 04/26] =?UTF-8?q?test:=20getOperations=EC=97=90=20jwt=20gu?= =?UTF-8?q?ard=20=ED=85=8C=EC=8A=A4=ED=8A=B8=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/test/board-trees.e2e-spec.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/nestjs-BE/server/test/board-trees.e2e-spec.ts b/nestjs-BE/server/test/board-trees.e2e-spec.ts index 9fe8433c..d4677cf8 100644 --- a/nestjs-BE/server/test/board-trees.e2e-spec.ts +++ b/nestjs-BE/server/test/board-trees.e2e-spec.ts @@ -15,6 +15,15 @@ import type { BoardOperation } from '../src/board-trees/schemas/board-operation. const PORT = 3000; +type WsException = { + status: string; + message: string; + cause: { + pattern: string; + data: object; + }; +}; + describe('BoardTreesGateway (e2e)', () => { const serverUrl = `ws://localhost:${PORT}/board`; let app: INestApplication; @@ -233,8 +242,24 @@ describe('BoardTreesGateway (e2e)', () => { } }); + it('exception if access token not include', async () => { + const response: WsException = await new Promise((resolve) => { + client.on('exception', (exception) => { + resolve(exception); + }); + client.emit('getOperations', { boardId }); + }); + + expect(response.status).toBe('error'); + expect(response.message).toBe('access token required'); + expect(response.cause.pattern).toBe('getOperations'); + }); + it('get operation logs', async () => { - const response = await client.emitWithAck('getOperations', boardId); + const response = await client.emitWithAck('getOperations', { + boardId, + token: testToken, + }); expect(response).toEqual(expect.arrayContaining(testOperations)); }); From aaec470747932f443be55bbb3e22acf831fb8d44 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Tue, 11 Feb 2025 17:39:31 +0900 Subject: [PATCH 05/26] =?UTF-8?q?feat:=20websocket=EC=9A=A9=20JwtAuth=20?= =?UTF-8?q?=EA=B0=80=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/board-trees/guards/jwt-auth.guard.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.ts diff --git a/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.ts b/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.ts new file mode 100644 index 00000000..a2e5451f --- /dev/null +++ b/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.ts @@ -0,0 +1,27 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { WsException } from '@nestjs/websockets'; + +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor( + private jwtService: JwtService, + private configService: ConfigService, + ) {} + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToWs().getData(); + if (!request.token) { + throw new WsException('access token required'); + } + try { + this.jwtService.verify(request.token, { + secret: this.configService.get('JWT_ACCESS_SECRET'), + }); + } catch (error) { + throw new WsException('access token invalid'); + } + return true; + } +} From 0fadf22be5ce988cc6fdd0b1c5cfdc27d495cf54 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Tue, 11 Feb 2025 19:39:05 +0900 Subject: [PATCH 06/26] =?UTF-8?q?test:=20JwtAuthGuard=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board-trees/guards/jwt-auth.guard.spec.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.spec.ts diff --git a/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.spec.ts b/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.spec.ts new file mode 100644 index 00000000..18d4a21d --- /dev/null +++ b/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.spec.ts @@ -0,0 +1,65 @@ +import { ConfigService } from '@nestjs/config'; +import { JwtModule, JwtService } from '@nestjs/jwt'; +import { Test } from '@nestjs/testing'; +import { WsException } from '@nestjs/websockets'; +import { sign } from 'jsonwebtoken'; +import { JwtAuthGuard } from './jwt-auth.guard'; + +import type { ExecutionContext } from '@nestjs/common'; +import type { TestingModule } from '@nestjs/testing'; + +const JWT_ACCESS_SECRET = 'access token secret'; + +describe('JwtAuthGuard', () => { + let guard: JwtAuthGuard; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [JwtModule], + providers: [ + { provide: ConfigService, useValue: { get: () => JWT_ACCESS_SECRET } }, + ], + }).compile(); + + const jwtService = module.get(JwtService); + const configService = module.get(ConfigService); + + guard = new JwtAuthGuard(jwtService, configService); + }); + + it('throw WsException when token not included', () => { + const context: ExecutionContext = { + switchToWs: () => ({ + getData: () => ({}), + }), + } as ExecutionContext; + + expect(() => guard.canActivate(context)).toThrow(WsException); + }); + + it('throw WsException if token is invalid', () => { + const testToken = sign({ sub: 'test uuid' }, JWT_ACCESS_SECRET, { + expiresIn: '-5m', + }); + const context: ExecutionContext = { + switchToWs: () => ({ + getData: () => ({ token: testToken }), + }), + } as ExecutionContext; + + expect(() => guard.canActivate(context)).toThrow(WsException); + }); + + it('return true if token is valid', () => { + const testToken = sign({ sub: 'test uuid' }, JWT_ACCESS_SECRET, { + expiresIn: '5m', + }); + const context: ExecutionContext = { + switchToWs: () => ({ + getData: () => ({ token: testToken }), + }), + } as ExecutionContext; + + expect(guard.canActivate(context)).toBeTruthy(); + }); +}); From 9fd0bf79728fa35ca4fcb138da35ea2693f6bbdf Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Tue, 11 Feb 2025 19:52:52 +0900 Subject: [PATCH 07/26] =?UTF-8?q?feat:=20getOperations=EC=97=90=20?= =?UTF-8?q?=EA=B0=80=EB=93=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/src/board-trees/board-trees.gateway.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts index 496de526..f1e7cf59 100644 --- a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts +++ b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts @@ -1,3 +1,4 @@ +import { UseGuards } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { MessageBody, @@ -9,6 +10,7 @@ import { } from '@nestjs/websockets'; import { ConfigService } from '@nestjs/config'; import { Server, Socket } from 'socket.io'; +import { JwtAuthGuard } from './guards/jwt-auth.guard'; import { BoardTreesService } from './board-trees.service'; import type { BoardOperation } from './schemas/board-operation.schema'; @@ -56,9 +58,10 @@ export class BoardTreesGateway implements OnGatewayInit, OnGatewayConnection { return { status: true }; } + @UseGuards(JwtAuthGuard) @SubscribeMessage('getOperations') async handleGetOperations( - @MessageBody() boardId: string, + @MessageBody('boardId') boardId: string, ): Promise { const operations = await this.boardTreesService.getOperationLogs(boardId); return operations; From 8193b399ff62296ea8e2241d755e8813dacbf05a Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Tue, 11 Feb 2025 20:25:04 +0900 Subject: [PATCH 08/26] =?UTF-8?q?test:=20createOperation=EC=97=90=20JwtAut?= =?UTF-8?q?hGuard=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/test/board-trees.e2e-spec.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/nestjs-BE/server/test/board-trees.e2e-spec.ts b/nestjs-BE/server/test/board-trees.e2e-spec.ts index d4677cf8..fee22bf0 100644 --- a/nestjs-BE/server/test/board-trees.e2e-spec.ts +++ b/nestjs-BE/server/test/board-trees.e2e-spec.ts @@ -165,6 +165,26 @@ describe('BoardTreesGateway (e2e)', () => { } }); + it('fail if access token not included', async () => { + const testOperation = { + boardId: uuid(), + type: 'add', + parentId: 'root', + content: 'new node', + }; + + const response: WsException = await new Promise((resolve) => { + client.on('exception', (exception) => { + resolve(exception); + }); + client.emit('createOperation', { operation: testOperation }); + }); + + expect(response.status).toBe('error'); + expect(response.message).toBe('access token required'); + expect(response.cause.pattern).toBe('createOperation'); + }); + it('create operation', async () => { const testOperation = { boardId: uuid(), @@ -173,7 +193,10 @@ describe('BoardTreesGateway (e2e)', () => { content: 'new node', }; - await client.emitWithAck('createOperation', testOperation); + await client.emitWithAck('createOperation', { + operation: testOperation, + token: testToken, + }); const operations = await boardTreesService.getOperationLogs( testOperation.boardId, @@ -201,7 +224,10 @@ describe('BoardTreesGateway (e2e)', () => { resolve(operation); }); - client.emit('createOperation', testOperation); + client.emit('createOperation', { + operation: testOperation, + token: testToken, + }); }); expect(response).toEqual(testOperation); From 84ffa453742d3f684353bfc3749367bafc0a4e5c Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Tue, 11 Feb 2025 20:26:11 +0900 Subject: [PATCH 09/26] =?UTF-8?q?feat:=20createOperation=EC=97=90=20JWT=20?= =?UTF-8?q?=EA=B0=80=EB=93=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/src/board-trees/board-trees.gateway.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts index f1e7cf59..8375f590 100644 --- a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts +++ b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts @@ -1,6 +1,7 @@ import { UseGuards } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { + ConnectedSocket, MessageBody, OnGatewayConnection, OnGatewayInit, @@ -51,8 +52,12 @@ export class BoardTreesGateway implements OnGatewayInit, OnGatewayConnection { client.emit('boardJoined', boardId); } + @UseGuards(JwtAuthGuard) @SubscribeMessage('createOperation') - async handleCreateOperation(client: Socket, operation: BoardOperation) { + async handleCreateOperation( + @ConnectedSocket() client: Socket, + @MessageBody('operation') operation: BoardOperation, + ) { await this.boardTreesService.createOperationLog(operation); client.broadcast.to(operation.boardId).emit('operation', operation); return { status: true }; From c731fc74aa6754cc94ffa8339a12eccb6f968656 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Wed, 12 Feb 2025 16:49:19 +0900 Subject: [PATCH 10/26] =?UTF-8?q?refactor:=20WsJwtAuthGuard=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/src/board-trees/board-trees.gateway.ts | 6 +++--- .../server/src/board-trees/guards/jwt-auth.guard.spec.ts | 6 +++--- nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts index 8375f590..4fb4acda 100644 --- a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts +++ b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts @@ -11,7 +11,7 @@ import { } from '@nestjs/websockets'; import { ConfigService } from '@nestjs/config'; import { Server, Socket } from 'socket.io'; -import { JwtAuthGuard } from './guards/jwt-auth.guard'; +import { WsJwtAuthGuard } from './guards/jwt-auth.guard'; import { BoardTreesService } from './board-trees.service'; import type { BoardOperation } from './schemas/board-operation.schema'; @@ -52,7 +52,7 @@ export class BoardTreesGateway implements OnGatewayInit, OnGatewayConnection { client.emit('boardJoined', boardId); } - @UseGuards(JwtAuthGuard) + @UseGuards(WsJwtAuthGuard) @SubscribeMessage('createOperation') async handleCreateOperation( @ConnectedSocket() client: Socket, @@ -63,7 +63,7 @@ export class BoardTreesGateway implements OnGatewayInit, OnGatewayConnection { return { status: true }; } - @UseGuards(JwtAuthGuard) + @UseGuards(WsJwtAuthGuard) @SubscribeMessage('getOperations') async handleGetOperations( @MessageBody('boardId') boardId: string, diff --git a/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.spec.ts b/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.spec.ts index 18d4a21d..2615b114 100644 --- a/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.spec.ts +++ b/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.spec.ts @@ -3,7 +3,7 @@ import { JwtModule, JwtService } from '@nestjs/jwt'; import { Test } from '@nestjs/testing'; import { WsException } from '@nestjs/websockets'; import { sign } from 'jsonwebtoken'; -import { JwtAuthGuard } from './jwt-auth.guard'; +import { WsJwtAuthGuard } from './jwt-auth.guard'; import type { ExecutionContext } from '@nestjs/common'; import type { TestingModule } from '@nestjs/testing'; @@ -11,7 +11,7 @@ import type { TestingModule } from '@nestjs/testing'; const JWT_ACCESS_SECRET = 'access token secret'; describe('JwtAuthGuard', () => { - let guard: JwtAuthGuard; + let guard: WsJwtAuthGuard; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -24,7 +24,7 @@ describe('JwtAuthGuard', () => { const jwtService = module.get(JwtService); const configService = module.get(ConfigService); - guard = new JwtAuthGuard(jwtService, configService); + guard = new WsJwtAuthGuard(jwtService, configService); }); it('throw WsException when token not included', () => { diff --git a/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.ts b/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.ts index a2e5451f..3d36d19a 100644 --- a/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.ts +++ b/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.ts @@ -4,7 +4,7 @@ import { JwtService } from '@nestjs/jwt'; import { WsException } from '@nestjs/websockets'; @Injectable() -export class JwtAuthGuard implements CanActivate { +export class WsJwtAuthGuard implements CanActivate { constructor( private jwtService: JwtService, private configService: ConfigService, From 4035ed9a6120ffb1a8983236fc62871097d0a82a Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Wed, 12 Feb 2025 16:54:13 +0900 Subject: [PATCH 11/26] =?UTF-8?q?rename:=20WsJwtAuthGuard=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/src/board-trees/board-trees.gateway.ts | 2 +- .../{jwt-auth.guard.spec.ts => ws-jwt-auth.guard.spec.ts} | 2 +- .../guards/{jwt-auth.guard.ts => ws-jwt-auth.guard.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename nestjs-BE/server/src/board-trees/guards/{jwt-auth.guard.spec.ts => ws-jwt-auth.guard.spec.ts} (97%) rename nestjs-BE/server/src/board-trees/guards/{jwt-auth.guard.ts => ws-jwt-auth.guard.ts} (100%) diff --git a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts index 4fb4acda..30fdac5b 100644 --- a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts +++ b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts @@ -11,7 +11,7 @@ import { } from '@nestjs/websockets'; import { ConfigService } from '@nestjs/config'; import { Server, Socket } from 'socket.io'; -import { WsJwtAuthGuard } from './guards/jwt-auth.guard'; +import { WsJwtAuthGuard } from './guards/ws-jwt-auth.guard'; import { BoardTreesService } from './board-trees.service'; import type { BoardOperation } from './schemas/board-operation.schema'; diff --git a/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.spec.ts b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts similarity index 97% rename from nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.spec.ts rename to nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts index 2615b114..e6792bce 100644 --- a/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.spec.ts +++ b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts @@ -3,7 +3,7 @@ import { JwtModule, JwtService } from '@nestjs/jwt'; import { Test } from '@nestjs/testing'; import { WsException } from '@nestjs/websockets'; import { sign } from 'jsonwebtoken'; -import { WsJwtAuthGuard } from './jwt-auth.guard'; +import { WsJwtAuthGuard } from './ws-jwt-auth.guard'; import type { ExecutionContext } from '@nestjs/common'; import type { TestingModule } from '@nestjs/testing'; diff --git a/nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.ts b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.ts similarity index 100% rename from nestjs-BE/server/src/board-trees/guards/jwt-auth.guard.ts rename to nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.ts From ce0d04804785b48ed150392dd25e99fe4856b5fd Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Wed, 12 Feb 2025 17:13:07 +0900 Subject: [PATCH 12/26] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/board-trees/board-trees.service.ts | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/nestjs-BE/server/src/board-trees/board-trees.service.ts b/nestjs-BE/server/src/board-trees/board-trees.service.ts index 1fe2c693..d5a0d8d8 100644 --- a/nestjs-BE/server/src/board-trees/board-trees.service.ts +++ b/nestjs-BE/server/src/board-trees/board-trees.service.ts @@ -1,66 +1,15 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; -import { BoardTree } from './schemas/board-tree.schema'; import { BoardOperation } from './schemas/board-operation.schema'; -import { CrdtTree } from '../crdt/crdt-tree'; -import { Operation } from '../crdt/operation'; @Injectable() export class BoardTreesService { constructor( - @InjectModel(BoardTree.name) private boardTreeModel: Model, @InjectModel(BoardOperation.name) private boardOperationModel: Model, ) {} - private boardTrees = new Map(); - - async create(boardId: string, tree: string) { - const createdTree = new this.boardTreeModel({ - boardId, - tree, - }); - return createdTree.save(); - } - - async findByBoardId(boardId: string) { - return this.boardTreeModel.findOne({ boardId }).exec(); - } - - getTreeData(boardId: string) { - const boardTree = this.boardTrees.get(boardId); - return JSON.stringify(boardTree); - } - - async initBoardTree(boardId: string, boardName: string) { - const existingTree = await this.findByBoardId(boardId); - if (existingTree) { - this.boardTrees.set(boardId, CrdtTree.parse(existingTree.tree)); - } else { - const newTree = new CrdtTree(boardId); - newTree.tree.get('root').description = boardName; - this.create(boardId, JSON.stringify(newTree)); - this.boardTrees.set(boardId, newTree); - } - } - - applyOperation(boardId: string, operation: Operation) { - const boardTree = this.boardTrees.get(boardId); - boardTree.applyOperation(operation); - } - - hasTree(boardId: string) { - return this.boardTrees.has(boardId); - } - - updateTreeData(boardId: string) { - const tree = this.boardTrees.get(boardId); - this.boardTreeModel - .updateOne({ boardId }, { tree: JSON.stringify(tree) }) - .exec(); - } - async createOperationLog(operation: BoardOperation) { return this.boardOperationModel.create(operation); } From 3a9c1fbcb19aa9a74032ad607f0c8378cd6ddff5 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Thu, 13 Feb 2025 15:17:32 +0900 Subject: [PATCH 13/26] =?UTF-8?q?test:=20WsJwtAuthGuard=EC=97=90=EC=84=9C?= =?UTF-8?q?=20user=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../guards/ws-jwt-auth.guard.spec.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts index e6792bce..ad7b4f3b 100644 --- a/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts +++ b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts @@ -62,4 +62,21 @@ describe('JwtAuthGuard', () => { expect(guard.canActivate(context)).toBeTruthy(); }); + + it('add data when successful', () => { + const testUuid = 'test uuid'; + const testToken = sign({ sub: testUuid }, JWT_ACCESS_SECRET, { + expiresIn: '5m', + }); + const data = { token: testToken }; + const context: ExecutionContext = { + switchToWs: () => ({ + getData: () => data, + }), + } as ExecutionContext; + + guard.canActivate(context); + + expect(context.switchToWs().getData().user).toEqual({ uuid: testUuid }); + }); }); From df2247551004d446246ab7fdde832876be9b797c Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Thu, 13 Feb 2025 15:23:37 +0900 Subject: [PATCH 14/26] =?UTF-8?q?refactor:=20executionContext=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../guards/ws-jwt-auth.guard.spec.ts | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts index ad7b4f3b..582c6fd6 100644 --- a/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts +++ b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts @@ -28,11 +28,7 @@ describe('JwtAuthGuard', () => { }); it('throw WsException when token not included', () => { - const context: ExecutionContext = { - switchToWs: () => ({ - getData: () => ({}), - }), - } as ExecutionContext; + const context = createExecutionContext({}); expect(() => guard.canActivate(context)).toThrow(WsException); }); @@ -41,11 +37,7 @@ describe('JwtAuthGuard', () => { const testToken = sign({ sub: 'test uuid' }, JWT_ACCESS_SECRET, { expiresIn: '-5m', }); - const context: ExecutionContext = { - switchToWs: () => ({ - getData: () => ({ token: testToken }), - }), - } as ExecutionContext; + const context = createExecutionContext({ token: testToken }); expect(() => guard.canActivate(context)).toThrow(WsException); }); @@ -54,11 +46,7 @@ describe('JwtAuthGuard', () => { const testToken = sign({ sub: 'test uuid' }, JWT_ACCESS_SECRET, { expiresIn: '5m', }); - const context: ExecutionContext = { - switchToWs: () => ({ - getData: () => ({ token: testToken }), - }), - } as ExecutionContext; + const context = createExecutionContext({ token: testToken }); expect(guard.canActivate(context)).toBeTruthy(); }); @@ -68,15 +56,20 @@ describe('JwtAuthGuard', () => { const testToken = sign({ sub: testUuid }, JWT_ACCESS_SECRET, { expiresIn: '5m', }); - const data = { token: testToken }; - const context: ExecutionContext = { - switchToWs: () => ({ - getData: () => data, - }), - } as ExecutionContext; + const context = createExecutionContext({ token: testToken }); guard.canActivate(context); expect(context.switchToWs().getData().user).toEqual({ uuid: testUuid }); }); }); + +function createExecutionContext(payload: object): ExecutionContext { + const innerPayload = { ...payload }; + const context: ExecutionContext = { + switchToWs: () => ({ + getData: () => innerPayload, + }), + } as ExecutionContext; + return context; +} From d3645d41ccd5189dba2f4762ea47e2a3aa9de42d Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Thu, 13 Feb 2025 15:26:25 +0900 Subject: [PATCH 15/26] =?UTF-8?q?feat:=20payload=EC=97=90=20user=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.ts b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.ts index 3d36d19a..f22f692d 100644 --- a/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.ts +++ b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.ts @@ -16,9 +16,10 @@ export class WsJwtAuthGuard implements CanActivate { throw new WsException('access token required'); } try { - this.jwtService.verify(request.token, { + const payload = this.jwtService.verify(request.token, { secret: this.configService.get('JWT_ACCESS_SECRET'), }); + request.user = { uuid: payload.sub }; } catch (error) { throw new WsException('access token invalid'); } From 626e29d0c37831ec55be421f70b918e43d3bb1f5 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Thu, 13 Feb 2025 16:52:17 +0900 Subject: [PATCH 16/26] =?UTF-8?q?feat:=20=EC=9B=B9=EC=86=8C=EC=BC=93?= =?UTF-8?q?=EC=9A=A9=20=EC=9C=A0=EC=A0=80=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EB=A7=A4=EC=B9=98=20=EA=B0=80=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/board-trees/board-trees.module.ts | 5 ++++- .../guards/ws-match-user-profile.guard.ts | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.ts diff --git a/nestjs-BE/server/src/board-trees/board-trees.module.ts b/nestjs-BE/server/src/board-trees/board-trees.module.ts index d7e47a91..8f32d1ab 100644 --- a/nestjs-BE/server/src/board-trees/board-trees.module.ts +++ b/nestjs-BE/server/src/board-trees/board-trees.module.ts @@ -8,6 +8,8 @@ import { BoardOperation, BoardOperationSchema, } from './schemas/board-operation.schema'; +import { WsMatchUserProfileGuard } from './guards/ws-match-user-profile.guard'; +import { ProfilesModule } from '../profiles/profiles.module'; @Module({ imports: [ @@ -16,7 +18,8 @@ import { { name: BoardOperation.name, schema: BoardOperationSchema }, ]), JwtModule, + ProfilesModule, ], - providers: [BoardTreesService, BoardTreesGateway], + providers: [BoardTreesService, BoardTreesGateway, WsMatchUserProfileGuard], }) export class BoardTreesModule {} diff --git a/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.ts b/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.ts new file mode 100644 index 00000000..f5f65cbd --- /dev/null +++ b/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.ts @@ -0,0 +1,22 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { WsException } from '@nestjs/websockets'; +import { ProfilesService } from '../../profiles/profiles.service'; + +@Injectable() +export class WsMatchUserProfileGuard implements CanActivate { + constructor(private profilesService: ProfilesService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToWs().getData(); + const userUuid = request.user?.uuid; + const profileUuid = request.profileUuid; + if (!profileUuid || !userUuid) + throw new WsException('profile uuid or user uuid required'); + const profile = + await this.profilesService.findProfileByProfileUuid(profileUuid); + if (!profile || userUuid !== profile.userUuid) { + throw new WsException('forbidden request'); + } + return true; + } +} From 15954005b2345d1f7f1f804267f135491b46cb11 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Thu, 13 Feb 2025 19:34:39 +0900 Subject: [PATCH 17/26] =?UTF-8?q?test:=20WsMatchUserProfileGuard=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ws-match-user-profile.guard.spec.ts | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.spec.ts diff --git a/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.spec.ts b/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.spec.ts new file mode 100644 index 00000000..f4b8b2a4 --- /dev/null +++ b/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.spec.ts @@ -0,0 +1,106 @@ +import { Test } from '@nestjs/testing'; +import { WsException } from '@nestjs/websockets'; +import { WsMatchUserProfileGuard } from './ws-match-user-profile.guard'; +import { ProfilesService } from '../../profiles/profiles.service'; + +import type { ExecutionContext } from '@nestjs/common'; +import type { TestingModule } from '@nestjs/testing'; + +describe('WsMatchUserProfileGuard', () => { + let guard: WsMatchUserProfileGuard; + let profilesService: ProfilesService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: ProfilesService, + useValue: { + findProfileByProfileUuid: jest.fn(() => ({ + userUuid: 'user uuid', + })), + }, + }, + ], + }).compile(); + + profilesService = module.get(ProfilesService); + + guard = new WsMatchUserProfileGuard(profilesService); + }); + + it('throw WsException when user uuid not included', async () => { + const context = createExecutionContext({ user: { uuid: 'user uuid' } }); + + const res = guard.canActivate(context); + + await expect(res).rejects.toThrow(WsException); + await expect(res).rejects.toMatchObject({ + message: 'profile uuid or user uuid required', + }); + }); + + it('throw WsException when profile uuid not included', async () => { + const context = createExecutionContext({ profileUuid: 'profile uuid' }); + + const res = guard.canActivate(context); + + await expect(res).rejects.toThrow(WsException); + await expect(res).rejects.toMatchObject({ + message: 'profile uuid or user uuid required', + }); + }); + + it('bad request if profile not exist', async () => { + const context = createExecutionContext({ + user: { uuid: 'user uuid' }, + profileUuid: 'profile uuid', + }); + (profilesService.findProfileByProfileUuid as jest.Mock).mockReturnValueOnce( + null, + ); + + const res = guard.canActivate(context); + + await expect(res).rejects.toThrow(WsException); + await expect(res).rejects.toMatchObject({ + message: 'forbidden request', + }); + }); + + it('bad request if profile uuid not match', async () => { + const context = createExecutionContext({ + user: { uuid: 'user uuid' }, + profileUuid: 'profile uuid', + }); + (profilesService.findProfileByProfileUuid as jest.Mock).mockReturnValueOnce( + { userUuid: 'other user uuid' }, + ); + + const res = guard.canActivate(context); + + await expect(res).rejects.toThrow(WsException); + await expect(res).rejects.toMatchObject({ + message: 'forbidden request', + }); + }); + + it('success', async () => { + const context = createExecutionContext({ + user: { uuid: 'user uuid' }, + profileUuid: 'profile uuid', + }); + + await expect(guard.canActivate(context)).resolves.toBeTruthy(); + }); +}); + +function createExecutionContext(payload: object): ExecutionContext { + const innerPayload = { ...payload }; + const context: ExecutionContext = { + switchToWs: () => ({ + getData: () => innerPayload, + }), + } as ExecutionContext; + return context; +} From 0ff34b8e84424adc1752fa04669305cafe2ec372 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Thu, 13 Feb 2025 21:09:20 +0900 Subject: [PATCH 18/26] =?UTF-8?q?fix:=20exception=20=EB=B0=9C=EC=83=9D=20?= =?UTF-8?q?=EC=8B=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8B=A4=ED=8C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/test/board-trees.e2e-spec.ts | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/nestjs-BE/server/test/board-trees.e2e-spec.ts b/nestjs-BE/server/test/board-trees.e2e-spec.ts index fee22bf0..9dc9c8f3 100644 --- a/nestjs-BE/server/test/board-trees.e2e-spec.ts +++ b/nestjs-BE/server/test/board-trees.e2e-spec.ts @@ -193,10 +193,17 @@ describe('BoardTreesGateway (e2e)', () => { content: 'new node', }; - await client.emitWithAck('createOperation', { + const response = client.emitWithAck('createOperation', { operation: testOperation, token: testToken, }); + const exceptionPromise = new Promise((_, reject) => { + client.on('exception', () => { + reject(new Error('exception occured')); + }); + }); + + await Promise.race([response, exceptionPromise]); const operations = await boardTreesService.getOperationLogs( testOperation.boardId, @@ -218,7 +225,7 @@ describe('BoardTreesGateway (e2e)', () => { content: 'new node', }; - const response = await new Promise((resolve) => { + const response = new Promise((resolve) => { otherClient.on('operation', (operation) => { otherClient.disconnect(); resolve(operation); @@ -229,8 +236,16 @@ describe('BoardTreesGateway (e2e)', () => { token: testToken, }); }); + const exceptionPromise = new Promise((_, reject) => { + client.on('exception', () => { + otherClient.disconnect(); + reject(new Error('exception occured')); + }); + }); + + await Promise.race([response, exceptionPromise]); - expect(response).toEqual(testOperation); + await expect(response).resolves.toEqual(testOperation); }); }); @@ -282,10 +297,17 @@ describe('BoardTreesGateway (e2e)', () => { }); it('get operation logs', async () => { - const response = await client.emitWithAck('getOperations', { + const response = client.emitWithAck('getOperations', { boardId, token: testToken, }); + const exceptionPromise = new Promise((_, reject) => { + client.on('exception', () => { + reject(new Error('exception occured')); + }); + }); + + await Promise.race([response, exceptionPromise]); expect(response).toEqual(expect.arrayContaining(testOperations)); }); From 9d628bdd021466b85c948c5a46d8ad6462e1e2ac Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Sat, 15 Feb 2025 15:42:29 +0900 Subject: [PATCH 19/26] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/test/board-trees.e2e-spec.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/nestjs-BE/server/test/board-trees.e2e-spec.ts b/nestjs-BE/server/test/board-trees.e2e-spec.ts index 9dc9c8f3..c0548f5b 100644 --- a/nestjs-BE/server/test/board-trees.e2e-spec.ts +++ b/nestjs-BE/server/test/board-trees.e2e-spec.ts @@ -90,7 +90,8 @@ describe('BoardTreesGateway (e2e)', () => { }); it('success', async () => { - const testToken = await createUserToken(prisma, config); + const testUser = await createUser(prisma); + const testToken = await createUserToken(testUser.uuid, config); const connected = await new Promise((resolve) => { const socket = io(serverUrl, { auth: { token: testToken } }); @@ -109,7 +110,8 @@ describe('BoardTreesGateway (e2e)', () => { let testToken: string; beforeEach(async () => { - testToken = await createUserToken(prisma, config); + const testUser = await createUser(prisma); + testToken = await createUserToken(testUser.uuid, config); }); it('boardIdRequired error when board id not included', async () => { @@ -152,7 +154,8 @@ describe('BoardTreesGateway (e2e)', () => { let client: Socket; beforeEach(async () => { - testToken = await createUserToken(prisma, config); + const user = await createUser(prisma); + testToken = await createUserToken(user.uuid, config); client = await createClientSocket(serverUrl, { auth: { token: testToken }, query: { boardId }, @@ -212,7 +215,8 @@ describe('BoardTreesGateway (e2e)', () => { }); it('other client received operation', async () => { - const otherToken = await createUserToken(prisma, config); + const otherUser = await createUser(prisma); + const otherToken = await createUserToken(otherUser.uuid, config); const otherClient = await createClientSocket(serverUrl, { auth: { token: otherToken }, query: { boardId }, @@ -256,7 +260,8 @@ describe('BoardTreesGateway (e2e)', () => { let client: Socket; beforeEach(async () => { - testToken = await createUserToken(prisma, config); + const testUser = await createUser(prisma); + testToken = await createUserToken(testUser.uuid, config); client = await createClientSocket(serverUrl, { auth: { token: testToken }, query: { boardId }, @@ -314,10 +319,13 @@ describe('BoardTreesGateway (e2e)', () => { }); }); -async function createUserToken(prisma: PrismaService, config: ConfigService) { - const user = await prisma.user.create({ data: { uuid: uuid() } }); +async function createUser(prisma: PrismaService) { + return prisma.user.create({ data: { uuid: uuid() } }); +} + +async function createUserToken(userUuid: string, config: ConfigService) { const token = sign( - { sub: user.uuid }, + { sub: userUuid }, config.get('JWT_ACCESS_SECRET'), { expiresIn: '5m' }, ); From c0710c4723a6a7691d26a43af87a8955ca5914e1 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Sat, 15 Feb 2025 16:43:16 +0900 Subject: [PATCH 20/26] =?UTF-8?q?test:=20WsMatchUserProfileGuard=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EC=97=AC=EB=B6=80=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/test/board-trees.e2e-spec.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/nestjs-BE/server/test/board-trees.e2e-spec.ts b/nestjs-BE/server/test/board-trees.e2e-spec.ts index c0548f5b..39bf732d 100644 --- a/nestjs-BE/server/test/board-trees.e2e-spec.ts +++ b/nestjs-BE/server/test/board-trees.e2e-spec.ts @@ -148,6 +148,57 @@ describe('BoardTreesGateway (e2e)', () => { }); }); + describe('Checking if WsMatchUserProfileGuard is applied', () => { + const boardId = uuid(); + let testToken: string; + let client: Socket; + + beforeEach(async () => { + const testUser = await createUser(prisma); + testToken = await createUserToken(testUser.uuid, config); + client = await createClientSocket(serverUrl, { + auth: { token: testToken }, + query: { boardId }, + }); + }); + + afterEach(() => { + if (client.connected) { + client.disconnect(); + } + }); + + it('createOperation', async () => { + const response: WsException = await new Promise((resolve, reject) => { + client.on('exception', (exception) => { + resolve(exception); + }); + client.emit('createOperation', { token: testToken }, (response) => { + reject(response); + }); + }); + + expect(response.status).toBe('error'); + expect(response.message).toBe('profile uuid or user uuid required'); + expect(response.cause.pattern).toBe('createOperation'); + }); + + it('getOperations', async () => { + const response: WsException = await new Promise((resolve, reject) => { + client.on('exception', (exception) => { + resolve(exception); + }); + client.emit('getOperations', { token: testToken }, (response) => { + reject(response); + }); + }); + + expect(response.status).toBe('error'); + expect(response.message).toBe('profile uuid or user uuid required'); + expect(response.cause.pattern).toBe('getOperations'); + }); + }); + describe('createOperation', () => { const boardId = 'board id'; let testToken: string; From da2657eb1c7a10331f551e5bc2e7c5a305a3e893 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Sat, 15 Feb 2025 17:10:39 +0900 Subject: [PATCH 21/26] =?UTF-8?q?test:=20=EA=B0=80=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/test/board-trees.e2e-spec.ts | 77 ++++++++++++------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/nestjs-BE/server/test/board-trees.e2e-spec.ts b/nestjs-BE/server/test/board-trees.e2e-spec.ts index 39bf732d..82bd2931 100644 --- a/nestjs-BE/server/test/board-trees.e2e-spec.ts +++ b/nestjs-BE/server/test/board-trees.e2e-spec.ts @@ -2,6 +2,7 @@ import { INestApplication } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { MongooseModule } from '@nestjs/mongoose'; +import { Profile } from '@prisma/client'; import { sign } from 'jsonwebtoken'; import { io, Socket } from 'socket.io-client'; import { v4 as uuid } from 'uuid'; @@ -201,11 +202,13 @@ describe('BoardTreesGateway (e2e)', () => { describe('createOperation', () => { const boardId = 'board id'; + let testProfile: Profile; let testToken: string; let client: Socket; beforeEach(async () => { const user = await createUser(prisma); + testProfile = await createProfile(user.uuid, prisma); testToken = await createUserToken(user.uuid, config); client = await createClientSocket(serverUrl, { auth: { token: testToken }, @@ -247,18 +250,23 @@ describe('BoardTreesGateway (e2e)', () => { content: 'new node', }; - const response = client.emitWithAck('createOperation', { - operation: testOperation, - token: testToken, - }); - const exceptionPromise = new Promise((_, reject) => { - client.on('exception', () => { - reject(new Error('exception occured')); + await new Promise((resolve, reject) => { + client.on('exception', (exception) => { + reject(exception); }); + client.emit( + 'createOperation', + { + operation: testOperation, + token: testToken, + profileUuid: testProfile.uuid, + }, + (response) => { + resolve(response); + }, + ); }); - await Promise.race([response, exceptionPromise]); - const operations = await boardTreesService.getOperationLogs( testOperation.boardId, ); @@ -280,26 +288,23 @@ describe('BoardTreesGateway (e2e)', () => { content: 'new node', }; - const response = new Promise((resolve) => { + const response = new Promise((resolve, reject) => { otherClient.on('operation', (operation) => { otherClient.disconnect(); resolve(operation); }); + client.on('exception', (exception) => { + otherClient.disconnect(); + reject(exception); + }); client.emit('createOperation', { operation: testOperation, token: testToken, - }); - }); - const exceptionPromise = new Promise((_, reject) => { - client.on('exception', () => { - otherClient.disconnect(); - reject(new Error('exception occured')); + profileUuid: testProfile.uuid, }); }); - await Promise.race([response, exceptionPromise]); - await expect(response).resolves.toEqual(testOperation); }); }); @@ -307,11 +312,13 @@ describe('BoardTreesGateway (e2e)', () => { describe('getOperations', () => { const boardId = uuid(); let testToken: string; + let testProfile: Profile; let testOperations: BoardOperation[]; let client: Socket; beforeEach(async () => { const testUser = await createUser(prisma); + testProfile = await createProfile(testUser.uuid, prisma); testToken = await createUserToken(testUser.uuid, config); client = await createClientSocket(serverUrl, { auth: { token: testToken }, @@ -353,19 +360,22 @@ describe('BoardTreesGateway (e2e)', () => { }); it('get operation logs', async () => { - const response = client.emitWithAck('getOperations', { - boardId, - token: testToken, - }); - const exceptionPromise = new Promise((_, reject) => { - client.on('exception', () => { - reject(new Error('exception occured')); + const response = new Promise((resolve, reject) => { + client.on('exception', (exception) => { + reject(exception); }); + client.emit( + 'getOperations', + { boardId, token: testToken, profileUuid: testProfile.uuid }, + (response: BoardOperation[]) => { + resolve(response); + }, + ); }); - await Promise.race([response, exceptionPromise]); - - expect(response).toEqual(expect.arrayContaining(testOperations)); + await expect(response).resolves.toEqual( + expect.arrayContaining(testOperations), + ); }); }); }); @@ -374,6 +384,17 @@ async function createUser(prisma: PrismaService) { return prisma.user.create({ data: { uuid: uuid() } }); } +async function createProfile(userUuid: string, prisma: PrismaService) { + return prisma.profile.create({ + data: { + uuid: uuid(), + userUuid, + image: 'test image', + nickname: 'test nickname', + }, + }); +} + async function createUserToken(userUuid: string, config: ConfigService) { const token = sign( { sub: userUuid }, From c6925462be5ee1bd7017fffdbd7d7a78c7849207 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Sun, 16 Feb 2025 15:07:52 +0900 Subject: [PATCH 22/26] =?UTF-8?q?feat:=20WsMatchUserProfileGuard=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/src/board-trees/board-trees.gateway.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts index 30fdac5b..ccf50158 100644 --- a/nestjs-BE/server/src/board-trees/board-trees.gateway.ts +++ b/nestjs-BE/server/src/board-trees/board-trees.gateway.ts @@ -13,6 +13,7 @@ import { ConfigService } from '@nestjs/config'; import { Server, Socket } from 'socket.io'; import { WsJwtAuthGuard } from './guards/ws-jwt-auth.guard'; import { BoardTreesService } from './board-trees.service'; +import { WsMatchUserProfileGuard } from './guards/ws-match-user-profile.guard'; import type { BoardOperation } from './schemas/board-operation.schema'; @WebSocketGateway({ namespace: 'board' }) @@ -52,7 +53,7 @@ export class BoardTreesGateway implements OnGatewayInit, OnGatewayConnection { client.emit('boardJoined', boardId); } - @UseGuards(WsJwtAuthGuard) + @UseGuards(WsJwtAuthGuard, WsMatchUserProfileGuard) @SubscribeMessage('createOperation') async handleCreateOperation( @ConnectedSocket() client: Socket, @@ -63,7 +64,7 @@ export class BoardTreesGateway implements OnGatewayInit, OnGatewayConnection { return { status: true }; } - @UseGuards(WsJwtAuthGuard) + @UseGuards(WsJwtAuthGuard, WsMatchUserProfileGuard) @SubscribeMessage('getOperations') async handleGetOperations( @MessageBody('boardId') boardId: string, From a9558beef6a5dcbdc933a8a14dbc8505a66b6945 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Sun, 16 Feb 2025 20:49:11 +0900 Subject: [PATCH 23/26] =?UTF-8?q?fix:=20=EC=9C=A0=EC=A0=80=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=80=EC=9E=A5=20=EC=8B=9C=EC=97=90=20?= =?UTF-8?q?reflect=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getData()에 직접 추가하는 식으로 할 경우에는 exception 발생 시에 추가한 데이터를 같이 응답하므로 reflect로 데이터 저장 --- nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.ts | 2 +- .../src/board-trees/guards/ws-match-user-profile.guard.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.ts b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.ts index f22f692d..bf2e099f 100644 --- a/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.ts +++ b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.ts @@ -19,7 +19,7 @@ export class WsJwtAuthGuard implements CanActivate { const payload = this.jwtService.verify(request.token, { secret: this.configService.get('JWT_ACCESS_SECRET'), }); - request.user = { uuid: payload.sub }; + Reflect.defineMetadata('user', { uuid: payload.sub }, context); } catch (error) { throw new WsException('access token invalid'); } diff --git a/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.ts b/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.ts index f5f65cbd..d2fe7076 100644 --- a/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.ts +++ b/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.ts @@ -8,7 +8,7 @@ export class WsMatchUserProfileGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToWs().getData(); - const userUuid = request.user?.uuid; + const userUuid = Reflect.getMetadata('user', context)?.uuid; const profileUuid = request.profileUuid; if (!profileUuid || !userUuid) throw new WsException('profile uuid or user uuid required'); From 10e1d6f2ef44f084e78aeb037312bdf0159904a8 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Sun, 16 Feb 2025 21:02:56 +0900 Subject: [PATCH 24/26] =?UTF-8?q?test:=20Reflect=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../guards/ws-jwt-auth.guard.spec.ts | 2 +- .../ws-match-user-profile.guard.spec.ts | 21 +++++++------------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts index 582c6fd6..c4cb7064 100644 --- a/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts +++ b/nestjs-BE/server/src/board-trees/guards/ws-jwt-auth.guard.spec.ts @@ -60,7 +60,7 @@ describe('JwtAuthGuard', () => { guard.canActivate(context); - expect(context.switchToWs().getData().user).toEqual({ uuid: testUuid }); + expect(Reflect.getMetadata('user', context)).toEqual({ uuid: testUuid }); }); }); diff --git a/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.spec.ts b/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.spec.ts index f4b8b2a4..4462a64b 100644 --- a/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.spec.ts +++ b/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.spec.ts @@ -30,7 +30,8 @@ describe('WsMatchUserProfileGuard', () => { }); it('throw WsException when user uuid not included', async () => { - const context = createExecutionContext({ user: { uuid: 'user uuid' } }); + const context = createExecutionContext({}); + Reflect.defineMetadata('user', { uuid: 'user uuid' }, context); const res = guard.canActivate(context); @@ -52,10 +53,8 @@ describe('WsMatchUserProfileGuard', () => { }); it('bad request if profile not exist', async () => { - const context = createExecutionContext({ - user: { uuid: 'user uuid' }, - profileUuid: 'profile uuid', - }); + const context = createExecutionContext({ profileUuid: 'profile uuid' }); + Reflect.defineMetadata('user', { uuid: 'user uuid' }, context); (profilesService.findProfileByProfileUuid as jest.Mock).mockReturnValueOnce( null, ); @@ -69,10 +68,8 @@ describe('WsMatchUserProfileGuard', () => { }); it('bad request if profile uuid not match', async () => { - const context = createExecutionContext({ - user: { uuid: 'user uuid' }, - profileUuid: 'profile uuid', - }); + const context = createExecutionContext({ profileUuid: 'profile uuid' }); + Reflect.defineMetadata('user', { uuid: 'user uuid' }, context); (profilesService.findProfileByProfileUuid as jest.Mock).mockReturnValueOnce( { userUuid: 'other user uuid' }, ); @@ -86,10 +83,8 @@ describe('WsMatchUserProfileGuard', () => { }); it('success', async () => { - const context = createExecutionContext({ - user: { uuid: 'user uuid' }, - profileUuid: 'profile uuid', - }); + const context = createExecutionContext({ profileUuid: 'profile uuid' }); + Reflect.defineMetadata('user', { uuid: 'user uuid' }, context); await expect(guard.canActivate(context)).resolves.toBeTruthy(); }); From e3f4d10035e7ce4eccff0f36520f8b9742fd8b7d Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Sun, 16 Feb 2025 21:03:18 +0900 Subject: [PATCH 25/26] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board-trees/guards/ws-match-user-profile.guard.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.spec.ts b/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.spec.ts index 4462a64b..6067a01d 100644 --- a/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.spec.ts +++ b/nestjs-BE/server/src/board-trees/guards/ws-match-user-profile.guard.spec.ts @@ -29,7 +29,7 @@ describe('WsMatchUserProfileGuard', () => { guard = new WsMatchUserProfileGuard(profilesService); }); - it('throw WsException when user uuid not included', async () => { + it('throw WsException when profile uuid not included', async () => { const context = createExecutionContext({}); Reflect.defineMetadata('user', { uuid: 'user uuid' }, context); @@ -41,7 +41,7 @@ describe('WsMatchUserProfileGuard', () => { }); }); - it('throw WsException when profile uuid not included', async () => { + it('throw WsException when user uuid not included', async () => { const context = createExecutionContext({ profileUuid: 'profile uuid' }); const res = guard.canActivate(context); From 174d3eaf0ef6ef1df153047745175d7c5e9595b6 Mon Sep 17 00:00:00 2001 From: Conut-1 <1mim1@naver.com> Date: Sun, 16 Feb 2025 21:17:00 +0900 Subject: [PATCH 26/26] =?UTF-8?q?refactor:=20=EA=B0=80=EB=93=9C=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=EC=97=90=20=EB=8C=80=ED=95=B4=EC=84=9C=20?= =?UTF-8?q?=EB=AC=B6=EC=96=B4=EC=84=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nestjs-BE/server/test/board-trees.e2e-spec.ts | 86 ++++++++++++------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/nestjs-BE/server/test/board-trees.e2e-spec.ts b/nestjs-BE/server/test/board-trees.e2e-spec.ts index 82bd2931..447ccd34 100644 --- a/nestjs-BE/server/test/board-trees.e2e-spec.ts +++ b/nestjs-BE/server/test/board-trees.e2e-spec.ts @@ -149,6 +149,59 @@ describe('BoardTreesGateway (e2e)', () => { }); }); + describe('Checking if WsJwtAuthGuard is applied', () => { + const boardId = uuid(); + let client: Socket; + + beforeEach(async () => { + const testUser = await createUser(prisma); + const testToken = await createUserToken(testUser.uuid, config); + client = await createClientSocket(serverUrl, { + auth: { token: testToken }, + query: { boardId }, + }); + }); + + afterEach(() => { + if (client.connected) { + client.disconnect(); + } + }); + + it('createOperation', async () => { + const testOperation = { + boardId, + type: 'add', + parentId: 'root', + content: 'new node', + }; + + const response: WsException = await new Promise((resolve) => { + client.on('exception', (exception) => { + resolve(exception); + }); + client.emit('createOperation', { operation: testOperation }); + }); + + expect(response.status).toBe('error'); + expect(response.message).toBe('access token required'); + expect(response.cause.pattern).toBe('createOperation'); + }); + + it('getOperations', async () => { + const response: WsException = await new Promise((resolve) => { + client.on('exception', (exception) => { + resolve(exception); + }); + client.emit('getOperations', { boardId }); + }); + + expect(response.status).toBe('error'); + expect(response.message).toBe('access token required'); + expect(response.cause.pattern).toBe('getOperations'); + }); + }); + describe('Checking if WsMatchUserProfileGuard is applied', () => { const boardId = uuid(); let testToken: string; @@ -222,26 +275,6 @@ describe('BoardTreesGateway (e2e)', () => { } }); - it('fail if access token not included', async () => { - const testOperation = { - boardId: uuid(), - type: 'add', - parentId: 'root', - content: 'new node', - }; - - const response: WsException = await new Promise((resolve) => { - client.on('exception', (exception) => { - resolve(exception); - }); - client.emit('createOperation', { operation: testOperation }); - }); - - expect(response.status).toBe('error'); - expect(response.message).toBe('access token required'); - expect(response.cause.pattern).toBe('createOperation'); - }); - it('create operation', async () => { const testOperation = { boardId: uuid(), @@ -346,19 +379,6 @@ describe('BoardTreesGateway (e2e)', () => { } }); - it('exception if access token not include', async () => { - const response: WsException = await new Promise((resolve) => { - client.on('exception', (exception) => { - resolve(exception); - }); - client.emit('getOperations', { boardId }); - }); - - expect(response.status).toBe('error'); - expect(response.message).toBe('access token required'); - expect(response.cause.pattern).toBe('getOperations'); - }); - it('get operation logs', async () => { const response = new Promise((resolve, reject) => { client.on('exception', (exception) => {