From 71530e0aa5788df2459d42ea3f213c713cd68a33 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Wed, 3 Jul 2024 17:02:38 +0200 Subject: [PATCH 01/17] improve performance of findCards --- .../board-node-authorizable.service.ts | 49 ++++++++++++++++++- .../board/service/board-node.service.ts | 6 +++ apps/server/src/modules/board/uc/card.uc.ts | 17 +++---- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/apps/server/src/modules/board/service/board-node-authorizable.service.ts b/apps/server/src/modules/board/service/board-node-authorizable.service.ts index bc9a846311f..b813761163b 100644 --- a/apps/server/src/modules/board/service/board-node-authorizable.service.ts +++ b/apps/server/src/modules/board/service/board-node-authorizable.service.ts @@ -1,7 +1,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { type EntityId } from '@shared/domain/types'; import { type AuthorizationLoaderService } from '@modules/authorization'; -import { AnyBoardNode, BoardNodeAuthorizable } from '../domain'; +import { AnyBoardNode, BoardNodeAuthorizable, UserWithBoardRoles } from '../domain'; import { BoardNodeRepo } from '../repo'; import { BoardContextService } from './internal/board-context.service'; import { BoardNodeService } from './board-node.service'; @@ -40,4 +40,51 @@ export class BoardNodeAuthorizableService implements AuthorizationLoaderService return boardNodeAuthorizable; } + + async getBoardAuthorizables(boardNodes: AnyBoardNode[]): Promise { + const rootIds = boardNodes.map((node) => node.rootId); + const parentIds = boardNodes.map((node) => node.parentId).filter((defined) => defined) as EntityId[]; + const boardNodeMap = await this.getBoardNodeMap([...rootIds, ...parentIds]); + const promises = boardNodes.map((boardNode) => { + const rootNode = boardNodeMap[boardNode.rootId]; + return this.boardContextService.getUsersWithBoardRoles(rootNode).then((users) => { + return { id: boardNode.id, users }; + }); + }); + + const results = await Promise.all(promises); + const usersMap = results.reduce((acc, { id, users }) => { + acc[id] = users; + return acc; + }, {} as Record); + + const boardNodeAuthorizables = boardNodes.map((boardNode) => { + const rootNode = boardNodeMap[boardNode.rootId]; + const parentNode = boardNode.parentId ? boardNodeMap[boardNode.parentId] : undefined; + const users = usersMap[boardNode.id]; + const boardNodeAuthorizable = new BoardNodeAuthorizable({ + users, + id: boardNode.id, + boardNode, + rootNode, + parentNode, + }); + return boardNodeAuthorizable; + }); + + return boardNodeAuthorizables; + } + + private async getBoardNodeMap(ids: EntityId[]): Promise> { + const idsUnique = Array.from(new Set(ids)); + const boardNodes = await this.boardNodeService.findByIds(idsUnique, 1); + const nodesMap: Record = boardNodes.reduce( + (map: Record, boardNode) => { + map[boardNode.id] = boardNode; + return map; + }, + {} as Record + ); + return nodesMap; + } } diff --git a/apps/server/src/modules/board/service/board-node.service.ts b/apps/server/src/modules/board/service/board-node.service.ts index bd8092a2e78..0ac96b627b1 100644 --- a/apps/server/src/modules/board/service/board-node.service.ts +++ b/apps/server/src/modules/board/service/board-node.service.ts @@ -76,6 +76,12 @@ export class BoardNodeService { return boardNode; } + async findByIds(ids: EntityId[], depth?: number): Promise { + const boardNode = this.boardNodeRepo.findByIds(ids, depth); + + return boardNode; + } + async findByClassAndId( Constructor: { new (props: S): T }, id: EntityId, diff --git a/apps/server/src/modules/board/uc/card.uc.ts b/apps/server/src/modules/board/uc/card.uc.ts index 9c627d0aa99..6ab2f392766 100644 --- a/apps/server/src/modules/board/uc/card.uc.ts +++ b/apps/server/src/modules/board/uc/card.uc.ts @@ -3,7 +3,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; -import { AnyContentElement, BoardNodeFactory, Card, ContentElementType } from '../domain'; +import { AnyBoardNode, AnyContentElement, BoardNodeFactory, Card, ContentElementType } from '../domain'; import { BoardNodeAuthorizableService, BoardNodePermissionService, BoardNodeService } from '../service'; @Injectable() @@ -29,19 +29,14 @@ export class CardUc { const user = await this.authorizationService.getUserWithPermissions(userId); const context: AuthorizationContext = { action: Action.read, requiredPermissions: [] }; - const promises = cards.map((card) => - this.boardNodeAuthorizableService.getBoardAuthorizable(card).then((boardNodeAuthorizable) => { - return { boardNodeAuthorizable, boardNode: card }; - }) - ); - const result = await Promise.all(promises); - - const allowedCards = result.reduce((allowedNodes: Card[], { boardNodeAuthorizable, boardNode }) => { + const boardAuthorizables = await this.boardNodeAuthorizableService.getBoardAuthorizables(cards); + + const allowedCards = boardAuthorizables.reduce((allowedNodes: AnyBoardNode[], boardNodeAuthorizable) => { if (this.authorizationService.hasPermission(user, boardNodeAuthorizable, context)) { - allowedNodes.push(boardNode); + allowedNodes.push(boardNodeAuthorizable.boardNode); } return allowedNodes; - }, []); + }, []) as Card[]; return allowedCards; } From 26c5b8e2dda20475ee7b7cc59f0058055e120e74 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Thu, 4 Jul 2024 10:30:30 +0200 Subject: [PATCH 02/17] track number action calls in separate metrics --- .../gateway/board-collaboration.gateway.ts | 11 +++++++++ .../modules/board/metrics/metrics.service.ts | 23 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts index a04045d0b53..efc778836af 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -61,6 +61,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { trackExecutionTime(methodName: string, executionTimeMs: number) { if (this.metricsService) { this.metricsService.setExecutionTime(methodName, executionTimeMs); + this.metricsService.incrementActionCount(methodName); } } @@ -128,6 +129,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('update-card-height-request') + @TrackExecutionTime() @UseRequestContext() async updateCardHeight(socket: Socket, data: UpdateCardHeightMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-card-height' }); @@ -142,6 +144,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('delete-card-request') + @TrackExecutionTime() @UseRequestContext() async deleteCard(socket: Socket, data: DeleteCardMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-card' }); @@ -178,6 +181,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('create-column-request') + @TrackExecutionTime() @UseRequestContext() async createColumn(socket: Socket, data: CreateColumnMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-column' }); @@ -219,6 +223,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('move-card-request') + @TrackExecutionTime() @UseRequestContext() async moveCard(socket: Socket, data: MoveCardMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-card' }); @@ -233,6 +238,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('move-column-request') + @TrackExecutionTime() @UseRequestContext() async moveColumn(socket: Socket, data: MoveColumnMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-column' }); @@ -267,6 +273,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('update-board-visibility-request') + @TrackExecutionTime() @UseRequestContext() async updateBoardVisibility(socket: Socket, data: UpdateBoardVisibilityMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-board-visibility' }); @@ -281,6 +288,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('delete-column-request') + @TrackExecutionTime() @UseRequestContext() async deleteColumn(socket: Socket, data: DeleteColumnMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-column' }); @@ -312,6 +320,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('create-element-request') + @TrackExecutionTime() @UseRequestContext() async createElement(socket: Socket, data: CreateContentElementMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-element' }); @@ -346,6 +355,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('delete-element-request') + @TrackExecutionTime() @UseRequestContext() async deleteElement(socket: Socket, data: DeleteContentElementMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-element' }); @@ -361,6 +371,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('move-element-request') + @TrackExecutionTime() @UseRequestContext() async moveElement(socket: Socket, data: MoveContentElementMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-element' }); diff --git a/apps/server/src/modules/board/metrics/metrics.service.ts b/apps/server/src/modules/board/metrics/metrics.service.ts index 8019220b40e..f46bec28fa6 100644 --- a/apps/server/src/modules/board/metrics/metrics.service.ts +++ b/apps/server/src/modules/board/metrics/metrics.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; import { UserService } from '@src/modules/user'; -import { Gauge, Summary, register } from 'prom-client'; +import { Counter, Gauge, Summary, register } from 'prom-client'; type ClientId = string; type Role = 'owner' | 'editor' | 'viewer'; @@ -19,6 +19,8 @@ export class MetricsService { private executionTimesSummary: Map> = new Map(); + private actionCounters: Map> = new Map(); + constructor(private readonly userService: UserService) { this.numberOfBoardroomsOnServerCounter = new Gauge({ name: 'sc_boards_rooms', @@ -97,4 +99,23 @@ export class MetricsService { } summary.observe(value); } + + public incrementActionCount(actionName: string): void { + let counter = this.actionCounters.get(actionName); + + if (!counter) { + counter = new Counter({ + name: `sc_boards_count_${actionName}`, + help: 'Number of calls for a specific action per minute', + // async collect() { + // // Invoked when the registry collects its metrics' values. + // const currentValue = await somethingAsync(); + // this.set(currentValue); + // }, + }); + this.actionCounters.set(actionName, counter); + register.registerMetric(counter); + } + counter.inc(); + } } From 4019bad0f2437182f5a1aa7fa2542e5159f3830c Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Thu, 4 Jul 2024 17:52:22 +0200 Subject: [PATCH 03/17] chore: switch from counter to gauge --- apps/server/src/modules/board/metrics/metrics.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/server/src/modules/board/metrics/metrics.service.ts b/apps/server/src/modules/board/metrics/metrics.service.ts index f46bec28fa6..a50a0d400da 100644 --- a/apps/server/src/modules/board/metrics/metrics.service.ts +++ b/apps/server/src/modules/board/metrics/metrics.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; import { UserService } from '@src/modules/user'; -import { Counter, Gauge, Summary, register } from 'prom-client'; +import { Gauge, Summary, register } from 'prom-client'; type ClientId = string; type Role = 'owner' | 'editor' | 'viewer'; @@ -19,7 +19,7 @@ export class MetricsService { private executionTimesSummary: Map> = new Map(); - private actionCounters: Map> = new Map(); + private actionCounters: Map> = new Map(); constructor(private readonly userService: UserService) { this.numberOfBoardroomsOnServerCounter = new Gauge({ @@ -104,7 +104,7 @@ export class MetricsService { let counter = this.actionCounters.get(actionName); if (!counter) { - counter = new Counter({ + counter = new Gauge({ name: `sc_boards_count_${actionName}`, help: 'Number of calls for a specific action per minute', // async collect() { @@ -117,5 +117,6 @@ export class MetricsService { register.registerMetric(counter); } counter.inc(); + // console.log(actionName, counter); } } From f2efa95503681343e6577b359bdc7cc524e2600b Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 5 Jul 2024 09:00:23 +0200 Subject: [PATCH 04/17] chore: log inc of counter metrics --- apps/server/src/modules/board/metrics/metrics.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/metrics/metrics.service.ts b/apps/server/src/modules/board/metrics/metrics.service.ts index a50a0d400da..af1b55fee70 100644 --- a/apps/server/src/modules/board/metrics/metrics.service.ts +++ b/apps/server/src/modules/board/metrics/metrics.service.ts @@ -117,6 +117,6 @@ export class MetricsService { register.registerMetric(counter); } counter.inc(); - // console.log(actionName, counter); + console.log(actionName, 'increased'); } } From 08f34108f21806125705690d66e31bc1ae18af40 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 5 Jul 2024 11:33:44 +0200 Subject: [PATCH 05/17] update to newer version of prom-client --- .../gateway/board-collaboration.gateway.ts | 1 + .../modules/board/metrics/metrics.service.ts | 9 +++++---- package-lock.json | 18 ++++++++++++++---- package.json | 2 +- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts index efc778836af..7ba5ba10d3f 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -62,6 +62,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { if (this.metricsService) { this.metricsService.setExecutionTime(methodName, executionTimeMs); this.metricsService.incrementActionCount(methodName); + this.metricsService.incrementActionCount('all'); } } diff --git a/apps/server/src/modules/board/metrics/metrics.service.ts b/apps/server/src/modules/board/metrics/metrics.service.ts index af1b55fee70..eec1adc1149 100644 --- a/apps/server/src/modules/board/metrics/metrics.service.ts +++ b/apps/server/src/modules/board/metrics/metrics.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; import { UserService } from '@src/modules/user'; -import { Gauge, Summary, register } from 'prom-client'; +import { Gauge, Summary, register, Counter } from 'prom-client'; type ClientId = string; type Role = 'owner' | 'editor' | 'viewer'; @@ -19,7 +19,7 @@ export class MetricsService { private executionTimesSummary: Map> = new Map(); - private actionCounters: Map> = new Map(); + private actionCounters: Map> = new Map(); constructor(private readonly userService: UserService) { this.numberOfBoardroomsOnServerCounter = new Gauge({ @@ -90,9 +90,10 @@ export class MetricsService { summary = new Summary({ name: `sc_boards_execution_time_${actionName}`, help: 'Average execution time of a specific action in milliseconds', - maxAgeSeconds: 60, + maxAgeSeconds: 600, ageBuckets: 5, percentiles: [0.01, 0.1, 0.9, 0.99], + pruneAgedBuckets: true, }); this.executionTimesSummary.set(actionName, summary); register.registerMetric(summary); @@ -104,7 +105,7 @@ export class MetricsService { let counter = this.actionCounters.get(actionName); if (!counter) { - counter = new Gauge({ + counter = new Counter({ name: `sc_boards_count_${actionName}`, help: 'Number of calls for a specific action per minute', // async collect() { diff --git a/package-lock.json b/package-lock.json index 52c01244aa7..d2cbd72d353 100644 --- a/package-lock.json +++ b/package-lock.json @@ -118,7 +118,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pdfmake": "^0.2.9", - "prom-client": "^13.1.0", + "prom-client": "^15.1.3", "qs": "^6.9.7", "read-chunk": "^3.0.0", "reflect-metadata": "^0.1.13", @@ -5407,6 +5407,14 @@ "node": ">=8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@panva/asn1.js": { "version": "1.0.0", "license": "MIT", @@ -19281,13 +19289,15 @@ } }, "node_modules/prom-client": { - "version": "13.2.0", - "license": "Apache-2.0", + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", "dependencies": { + "@opentelemetry/api": "^1.4.0", "tdigest": "^0.1.1" }, "engines": { - "node": ">=10" + "node": "^16 || ^18 || >=20" } }, "node_modules/promise-breaker": { diff --git a/package.json b/package.json index c064843247d..2cad05a7150 100644 --- a/package.json +++ b/package.json @@ -234,7 +234,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pdfmake": "^0.2.9", - "prom-client": "^13.1.0", + "prom-client": "^15.1.3", "qs": "^6.9.7", "read-chunk": "^3.0.0", "reflect-metadata": "^0.1.13", From df0cc615e1293f166267c77c7e80cb3fb0ef04dc Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 5 Jul 2024 13:15:14 +0200 Subject: [PATCH 06/17] chore: log executionTimes --- apps/server/src/modules/board/metrics/metrics.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/metrics/metrics.service.ts b/apps/server/src/modules/board/metrics/metrics.service.ts index eec1adc1149..f9e5b44eb98 100644 --- a/apps/server/src/modules/board/metrics/metrics.service.ts +++ b/apps/server/src/modules/board/metrics/metrics.service.ts @@ -98,6 +98,7 @@ export class MetricsService { this.executionTimesSummary.set(actionName, summary); register.registerMetric(summary); } + console.log(actionName, `executionTime: ${value.toFixed(3)} ms`); summary.observe(value); } @@ -118,6 +119,6 @@ export class MetricsService { register.registerMetric(counter); } counter.inc(); - console.log(actionName, 'increased'); + console.log(actionName, `count increased`); } } From 5609e4a911189c4287311162c3a5131a08f7aee6 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 5 Jul 2024 13:40:31 +0200 Subject: [PATCH 07/17] chore: add 0.5 percentile0 --- apps/server/src/modules/board/metrics/metrics.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/metrics/metrics.service.ts b/apps/server/src/modules/board/metrics/metrics.service.ts index f9e5b44eb98..db19ca038fb 100644 --- a/apps/server/src/modules/board/metrics/metrics.service.ts +++ b/apps/server/src/modules/board/metrics/metrics.service.ts @@ -92,7 +92,7 @@ export class MetricsService { help: 'Average execution time of a specific action in milliseconds', maxAgeSeconds: 600, ageBuckets: 5, - percentiles: [0.01, 0.1, 0.9, 0.99], + percentiles: [0.01, 0.1, 0.5, 0.9, 0.99], pruneAgedBuckets: true, }); this.executionTimesSummary.set(actionName, summary); From bb7d68f962cf2135353613874224c033c74f7bfa Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 5 Jul 2024 16:14:17 +0200 Subject: [PATCH 08/17] chore: change maxAge --- apps/server/src/modules/board/metrics/metrics.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/metrics/metrics.service.ts b/apps/server/src/modules/board/metrics/metrics.service.ts index db19ca038fb..ed249039b7c 100644 --- a/apps/server/src/modules/board/metrics/metrics.service.ts +++ b/apps/server/src/modules/board/metrics/metrics.service.ts @@ -90,7 +90,7 @@ export class MetricsService { summary = new Summary({ name: `sc_boards_execution_time_${actionName}`, help: 'Average execution time of a specific action in milliseconds', - maxAgeSeconds: 600, + maxAgeSeconds: 120, ageBuckets: 5, percentiles: [0.01, 0.1, 0.5, 0.9, 0.99], pruneAgedBuckets: true, From 513b361a6615971548c798abdbc3daef8b010d60 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 5 Jul 2024 16:30:23 +0200 Subject: [PATCH 09/17] chore: reset to 600s maxAge --- apps/server/src/modules/board/metrics/metrics.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/modules/board/metrics/metrics.service.ts b/apps/server/src/modules/board/metrics/metrics.service.ts index ed249039b7c..db19ca038fb 100644 --- a/apps/server/src/modules/board/metrics/metrics.service.ts +++ b/apps/server/src/modules/board/metrics/metrics.service.ts @@ -90,7 +90,7 @@ export class MetricsService { summary = new Summary({ name: `sc_boards_execution_time_${actionName}`, help: 'Average execution time of a specific action in milliseconds', - maxAgeSeconds: 120, + maxAgeSeconds: 600, ageBuckets: 5, percentiles: [0.01, 0.1, 0.5, 0.9, 0.99], pruneAgedBuckets: true, From eb7b13d5ad33470b6173788e46b5045b96e16629 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Fri, 5 Jul 2024 16:56:28 +0200 Subject: [PATCH 10/17] use gauge and counter --- .../gateway/board-collaboration.gateway.ts | 2 ++ .../modules/board/metrics/metrics.service.ts | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts index 7ba5ba10d3f..c9a6cc7163b 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -62,7 +62,9 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { if (this.metricsService) { this.metricsService.setExecutionTime(methodName, executionTimeMs); this.metricsService.incrementActionCount(methodName); + this.metricsService.incrementActionGauge(methodName); this.metricsService.incrementActionCount('all'); + this.metricsService.incrementActionGauge('all'); } } diff --git a/apps/server/src/modules/board/metrics/metrics.service.ts b/apps/server/src/modules/board/metrics/metrics.service.ts index db19ca038fb..b2a54d5ce75 100644 --- a/apps/server/src/modules/board/metrics/metrics.service.ts +++ b/apps/server/src/modules/board/metrics/metrics.service.ts @@ -21,6 +21,8 @@ export class MetricsService { private actionCounters: Map> = new Map(); + private actionGauges: Map> = new Map(); + constructor(private readonly userService: UserService) { this.numberOfBoardroomsOnServerCounter = new Gauge({ name: 'sc_boards_rooms', @@ -121,4 +123,24 @@ export class MetricsService { counter.inc(); console.log(actionName, `count increased`); } + + public incrementActionGauge(actionName: string): void { + let counter = this.actionGauges.get(actionName); + + if (!counter) { + counter = new Gauge({ + name: `sc_boards_count2_${actionName}`, + help: 'Number of calls for a specific action per minute', + // async collect() { + // // Invoked when the registry collects its metrics' values. + // const currentValue = await somethingAsync(); + // this.set(currentValue); + // }, + }); + this.actionGauges.set(actionName, counter); + register.registerMetric(counter); + } + counter.inc(); + console.log(actionName, `count increased`); + } } From 02f5c4758e82189a64cf115ec06a0ddd1f05f29b Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Tue, 9 Jul 2024 16:43:14 +0200 Subject: [PATCH 11/17] implementation of load testing --- .../src/modules/board/loadtest/readme.md | 66 ++++++++++ .../src/modules/board/loadtest/runScenario.sh | 116 ++++++++++++++++++ .../loadtest/scenarios/30users_5minutes.yml | 75 +++++++++++ .../board/loadtest/scenarios/3users.yml | 57 +++++++++ .../scenarios/6createCard_6UpdateTitle.yml | 70 +++++++++++ 5 files changed, 384 insertions(+) create mode 100644 apps/server/src/modules/board/loadtest/readme.md create mode 100644 apps/server/src/modules/board/loadtest/runScenario.sh create mode 100644 apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml create mode 100644 apps/server/src/modules/board/loadtest/scenarios/3users.yml create mode 100644 apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml diff --git a/apps/server/src/modules/board/loadtest/readme.md b/apps/server/src/modules/board/loadtest/readme.md new file mode 100644 index 00000000000..60bd4d5e3d6 --- /dev/null +++ b/apps/server/src/modules/board/loadtest/readme.md @@ -0,0 +1,66 @@ +# Loadtesting the boards + +The socket.io documentation suggests to use the tool artillery in order to load test a socket-io tool like our board-collaboration service. + +For defining scenarios you need to use/create Yaml-files that define which operations with which parameters need to be executed in which order. + +Some sceneraios were already prepared and are stored in the subfolder scenarios. + +## install artillery + +To run artillery from your local environment you need to install it first including an adapter that supports socketio-v3-websocket communication: + +```sh +npm install -g artillery artillery-engine-socketio-v3 +``` + +## manual execution + +To execute a scenario you can run artillery from the shell / commandline...: + +Using the `--variables` parameter it is possible to define several variables and there values that can be used in the scenerio-yaml-file: + +- **target**: defines the base url for all requests (REST and WebSocket) + e.g. https://main.dbc.dbildungscloud.dev +- **token**: a valid JWT for the targeted system +- **board_id**: id of an existing board the tests should be executed on + +```powershell +npx artillery run --variables "{'target': 'https://main.dbc.dbildungscloud.dev', 'token': 'eJ....', 'board_id': '668d0e03bf3689d12e1e86fb' }" './scenarios/3users.yml' --output artilleryreport.json +``` + +## visualizing the recorded results + +It is possible to generate a HTML-report based on the recorded data. + +```powershell +npx artillery report --output=$board_title.html artilleryreport.json +``` + +## automatic execution + +You can run one of the existing scenarios by executing: + +```bash +bash runScenario.sh +``` + +This will: + +1. let you choose from scenario-files +2. create a fresh JWT-webtoken +3. create a fresh board (in one of the courses) the user has access to +4. name the board by a combination of datetime and the scenario name. +5. start the execution of the scenario against this newly created board +6. generate a html report in the end + +## Open Todos: + +- [x] split code into functions +- [x] suppress curl output +- [ ] add proper error handling of failed requests +- [x] cleanup scenarios +- x ] write documentation for linux => runScenario.sh +- [x] write documentation for windows => direct command +- [ ] properly handle credentials - env-vars? +- [ ] replace selects with parameters from script call diff --git a/apps/server/src/modules/board/loadtest/runScenario.sh b/apps/server/src/modules/board/loadtest/runScenario.sh new file mode 100644 index 00000000000..afdd475b0ff --- /dev/null +++ b/apps/server/src/modules/board/loadtest/runScenario.sh @@ -0,0 +1,116 @@ +#!/bin/bash +function select_target() { + declare -a targets=("https://main.nbc.dbildungscloud.dev" "https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev") + echo "Please select the target for the test:" >&2 + select target in "${targets[@]}"; do + if [[ -n $target ]]; then + break + else + echo "Invalid selection. Please try again." >&2 + fi + done +} + +function select_scenario() { + # list files in the scenarios directory + scenarios_dir="./scenarios" + declare -a scenario_files=($(ls $scenarios_dir)) + + echo "Please select a scenario file for the test:" >&2 + select scenario_file in "${scenario_files[@]}"; do + if [[ -n $scenario_file ]]; then + echo "You have selected: $scenario_file" >&2 + break + else + echo "Invalid selection. Please try again." >&2 + fi + done + + scenario_name="${scenario_file%.*}" +} + +function get_token() { + response=$(curl -s -f -X 'POST' \ + "$target/api/v3/authentication/local" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "username": "lehrer@schul-cloud.org", + "password": "Schulcloud1!" + }') + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to get token. Please check your credentials and target URL." >&2 + exit 1 + fi + + token=$(echo $response | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p') +} + +function get_course_id() { + response=$(curl -s -f -X 'GET' \ + "$target/api/v3/courses" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer $token") + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to get course list. Please check your credentials and target URL." >&2 + exit 1 + fi + + course_id=$(echo $response | sed -n 's/.*"id":"\([^"]*\)".*/\1/p') +} + +function create_board_title() { + current_date=$(date +%Y-%m-%d_%H:%M) + board_title="${current_date}_$1" +} + +function create_board() { + response=$(curl -s -f -X 'POST' \ + "$target/api/v3/boards" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $token" \ + -d "{ + \"title\": \"$board_title\", + \"parentId\": \"$course_id\", + \"parentType\": \"course\", + \"layout\": \"columns\" + }") + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to create a board." >&2 + exit 1 + fi + + board_id=$(echo $response | sed -n 's/.*"id":"\([^"]*\)".*/\1/p' ) +} + +#select_target +target=https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev +echo "target: $target" + + +echo $1 +select_scenario +echo "scenario_name: $scenario_name" + +get_token +echo "token: ${token:0:50}..." + +get_course_id +echo "course_id: $course_id" + +create_board_title $scenario_name +echo "board_title: $board_title" + +create_board +echo "board_id $board_id" + +echo "board: $target/rooms/$board_id/board" +echo "Running artillery test..." + +npx artillery run --variables "{\"target\": \"$target\", \"token\": \"$token\", \"board_id\": \"$board_id\" }" "./scenarios/$scenario_name.yml" --output artilleryreport.json + +npx artillery report --output=$board_title.html artilleryreport.json diff --git a/apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml b/apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml new file mode 100644 index 00000000000..567cbaf703a --- /dev/null +++ b/apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml @@ -0,0 +1,75 @@ +config: + target: '{{ target }}' + engines: + socketio: + transport: ['websocket', 'polling'] + path: '/board-collaboration' + socketio-v3: + path: '/board-collaboration' + timeout: 1000000 + extraHeaders: + Cookie: 'jwt={{ token }}' + + phases: + - duration: 300 + arrivalRate: 10 + maxVusers: 30 + +scenarios: + - name: create card + engine: socketio-v3 + socketio-v3: + extraHeaders: + Cookie: 'jwt={{ token }}' + flow: + - think: 1 + + - emit: + channel: 'fetch-board-request' + data: + boardId: '{{ board_id }}' + + - think: 1 + + - emit: + channel: 'create-column-request' + data: + boardId: '{{ board_id }}' + response: + on: 'create-column-success' + capture: + - json: $.newColumn.id + as: columnId + + - think: 1 + + - loop: + - emit: + channel: 'create-card-request' + data: + columnId: '{{ columnId}}' + response: + on: 'create-card-success' + capture: + - json: $.newCard.id + as: cardId + + - think: 1 + + - emit: + channel: 'fetch-card-request' + data: + cardIds: + - '{{ cardId }}' + + - think: 2 + + - emit: + channel: 'update-card-title-request' + data: + cardId: '{{ cardId }}' + newTitle: 'Card {{ cardId}}' + + - think: 1 + + count: 20 diff --git a/apps/server/src/modules/board/loadtest/scenarios/3users.yml b/apps/server/src/modules/board/loadtest/scenarios/3users.yml new file mode 100644 index 00000000000..4fbeef037c8 --- /dev/null +++ b/apps/server/src/modules/board/loadtest/scenarios/3users.yml @@ -0,0 +1,57 @@ +config: + target: '{{ target }}' + engines: + socketio: + transport: ['websocket', 'polling'] + path: '/board-collaboration' + socketio-v3: + path: '/board-collaboration' + timeout: 1000000 + extraHeaders: + Cookie: 'jwt={{ token }}' + + phases: + - duration: 1 + arrivalRate: 3 + +scenarios: + - name: create card + engine: socketio-v3 + socketio-v3: + extraHeaders: + Cookie: 'jwt={{ token }}' + flow: + - log: '{{ target }}' + - think: 1 + + - emit: + channel: 'create-column-request' + data: + boardId: '{{ board_id }}' + response: + on: 'create-column-success' + capture: + - json: $.newColumn.id + as: columnId + + - think: 1 + + - emit: + channel: 'create-card-request' + data: + columnId: '{{ columnId}}' + response: + on: 'create-card-success' + capture: + - json: $.newCard.id + as: cardId + + - think: 1 + + - emit: + channel: 'update-card-title-request' + data: + cardId: '{{ cardId }}' + newTitle: 'One {{ cardId}}' + + - think: 2 diff --git a/apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml b/apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml new file mode 100644 index 00000000000..ad7e993f829 --- /dev/null +++ b/apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml @@ -0,0 +1,70 @@ +config: + target: '{{ target }}' + engines: + socketio: + transport: ['websocket', 'polling'] + path: '/board-collaboration' + socketio-v3: + path: '/board-collaboration' + timeout: 1000000 + extraHeaders: + Cookie: 'jwt={{ token }}' + + phases: + - duration: 2 + arrivalRate: 50 + +scenarios: + - name: create card + engine: socketio-v3 + socketio: + extraHeaders: + Cookie: 'jwt={{ token }}' + socketio-v3: + extraHeaders: + Cookie: 'jwt={{ token }}' + flow: + - think: 1 + + - emit: + channel: 'create-column-request' + data: + boardId: '{{ board_id }}' + response: + on: 'create-column-success' + capture: + - json: $.newColumn.id + as: columnId + + - think: 2 + + - loop: + - emit: + channel: 'create-card-request' + data: + columnId: '{{ columnId}}' + response: + on: 'create-card-success' + capture: + - json: $.newCard.id + as: cardId + + - think: 1 + + - emit: + channel: 'fetch-card-request' + data: + cardIds: + - '{{ cardId }}' + + - think: 2 + + - emit: + channel: 'update-card-title-request' + data: + cardId: '{{ cardId }}' + newTitle: 'Card {{ cardId}}' + + - think: 1 + + count: 6 From d9cb4228973d84482d4cda2f7cac93e24b873813 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Tue, 9 Jul 2024 16:56:30 +0200 Subject: [PATCH 12/17] added option to pass target and scenerio as parameters --- .../server/src/modules/board/loadtest/readme.md | 17 ++++++++++++----- .../src/modules/board/loadtest/runScenario.sh | 15 +++++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/server/src/modules/board/loadtest/readme.md b/apps/server/src/modules/board/loadtest/readme.md index 60bd4d5e3d6..8841fb0c25a 100644 --- a/apps/server/src/modules/board/loadtest/readme.md +++ b/apps/server/src/modules/board/loadtest/readme.md @@ -51,16 +51,23 @@ This will: 2. create a fresh JWT-webtoken 3. create a fresh board (in one of the courses) the user has access to 4. name the board by a combination of datetime and the scenario name. -5. start the execution of the scenario against this newly created board -6. generate a html report in the end +5. output a link to the generated board (in order open and see the test live) +6. start the execution of the scenario against this newly created board +7. generate a html report in the end + +You can also provide the target as the first and the name of the scenario as the second parameter - to avoid the need to select those: + +```bash +bash runScenario.sh https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev 3users +``` ## Open Todos: - [x] split code into functions - [x] suppress curl output -- [ ] add proper error handling of failed requests +- [x] add proper error handling of failed requests - [x] cleanup scenarios -- x ] write documentation for linux => runScenario.sh +- [x] write documentation for linux => runScenario.sh - [x] write documentation for windows => direct command +- [x] replace selects with parameters from script call - [ ] properly handle credentials - env-vars? -- [ ] replace selects with parameters from script call diff --git a/apps/server/src/modules/board/loadtest/runScenario.sh b/apps/server/src/modules/board/loadtest/runScenario.sh index afdd475b0ff..e4641983fb3 100644 --- a/apps/server/src/modules/board/loadtest/runScenario.sh +++ b/apps/server/src/modules/board/loadtest/runScenario.sh @@ -87,13 +87,20 @@ function create_board() { board_id=$(echo $response | sed -n 's/.*"id":"\([^"]*\)".*/\1/p' ) } -#select_target -target=https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev +if [ -z "$1" ]; then + select_target +else + target=$1 +fi echo "target: $target" -echo $1 -select_scenario +if [ -z "$2" ]; then + select_scenario + echo "scenario_name: $scenario_name" +else + scenario_name=$2 +fi echo "scenario_name: $scenario_name" get_token From a28cc32fb3ac2ec787bc14348353ee491c8173a3 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Thu, 11 Jul 2024 09:51:09 +0200 Subject: [PATCH 13/17] chore: take credentials from environment variables or user input --- .../src/modules/board/loadtest/readme.md | 17 +++++------- .../src/modules/board/loadtest/runScenario.sh | 26 +++++++++++++++---- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/apps/server/src/modules/board/loadtest/readme.md b/apps/server/src/modules/board/loadtest/readme.md index 8841fb0c25a..208c5d2d9da 100644 --- a/apps/server/src/modules/board/loadtest/readme.md +++ b/apps/server/src/modules/board/loadtest/readme.md @@ -55,19 +55,16 @@ This will: 6. start the execution of the scenario against this newly created board 7. generate a html report in the end -You can also provide the target as the first and the name of the scenario as the second parameter - to avoid the need to select those: +You can also provide the target as the first and the name of the scenario as the second parameter - to avoid the need to select those. Here is an example: ```bash bash runScenario.sh https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev 3users ``` -## Open Todos: +## password -- [x] split code into functions -- [x] suppress curl output -- [x] add proper error handling of failed requests -- [x] cleanup scenarios -- [x] write documentation for linux => runScenario.sh -- [x] write documentation for windows => direct command -- [x] replace selects with parameters from script call -- [ ] properly handle credentials - env-vars? +By typeing `export CARL_CORD_PASSWORD=realpassword` the script will not ask you anymore for the password to create a token. + +## Todos + +- [ ] enable optional parameter course_id diff --git a/apps/server/src/modules/board/loadtest/runScenario.sh b/apps/server/src/modules/board/loadtest/runScenario.sh index e4641983fb3..0187f1c579f 100644 --- a/apps/server/src/modules/board/loadtest/runScenario.sh +++ b/apps/server/src/modules/board/loadtest/runScenario.sh @@ -1,4 +1,5 @@ #!/bin/bash + function select_target() { declare -a targets=("https://main.nbc.dbildungscloud.dev" "https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev") echo "Please select the target for the test:" >&2 @@ -29,15 +30,23 @@ function select_scenario() { scenario_name="${scenario_file%.*}" } +function get_credentials() { + if [ -z "$CARL_CORD_PASSWORD" ]; then + echo "Password for Carl Cord is unknown. Provide it as an enviroment variable (CARL_CORD_PASSWORD) or enter it:" + read CARL_CORD_PASSWORD + export CARL_CORD_PASSWORD + fi +} + function get_token() { response=$(curl -s -f -X 'POST' \ "$target/api/v3/authentication/local" \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ - -d '{ - "username": "lehrer@schul-cloud.org", - "password": "Schulcloud1!" - }') + -d "{ + \"username\": \"lehrer@schul-cloud.org\", + \"password\": \"$CARL_CORD_PASSWORD\" + }") if [ $? -ne 0 ]; then echo "ERROR: Failed to get token. Please check your credentials and target URL." >&2 @@ -92,6 +101,7 @@ if [ -z "$1" ]; then else target=$1 fi +echo " " echo "target: $target" @@ -99,15 +109,20 @@ if [ -z "$2" ]; then select_scenario echo "scenario_name: $scenario_name" else - scenario_name=$2 + scenario_name="$2" + scenario_name=${scenario_name//.yml/} fi echo "scenario_name: $scenario_name" +get_credentials + get_token echo "token: ${token:0:50}..." +echo " " get_course_id echo "course_id: $course_id" +echo " " create_board_title $scenario_name echo "board_title: $board_title" @@ -116,6 +131,7 @@ create_board echo "board_id $board_id" echo "board: $target/rooms/$board_id/board" +echo " " echo "Running artillery test..." npx artillery run --variables "{\"target\": \"$target\", \"token\": \"$token\", \"board_id\": \"$board_id\" }" "./scenarios/$scenario_name.yml" --output artilleryreport.json From 416689ad39b11b418c21dc9268e14ee3099e173b Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Thu, 11 Jul 2024 09:51:45 +0200 Subject: [PATCH 14/17] chore: extend gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 130655cf16d..a045507c02b 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,5 @@ build /coverage /.nyc_output /.idea/ +/apps/server/src/modules/board/loadtest/**/*.html +/apps/server/src/modules/board/loadtest/artilleryreport.json From ea1c78fd13318e4240fa95e9ef59c191abf584e1 Mon Sep 17 00:00:00 2001 From: Thomas Feldtkeller Date: Thu, 11 Jul 2024 12:00:29 +0200 Subject: [PATCH 15/17] add steps for windows --- apps/server/src/modules/board/loadtest/readme.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/board/loadtest/readme.md b/apps/server/src/modules/board/loadtest/readme.md index 208c5d2d9da..24cbd18605c 100644 --- a/apps/server/src/modules/board/loadtest/readme.md +++ b/apps/server/src/modules/board/loadtest/readme.md @@ -21,14 +21,20 @@ To execute a scenario you can run artillery from the shell / commandline...: Using the `--variables` parameter it is possible to define several variables and there values that can be used in the scenerio-yaml-file: - **target**: defines the base url for all requests (REST and WebSocket) - e.g. https://main.dbc.dbildungscloud.dev + e.g. `https://main.dbc.dbildungscloud.dev` - **token**: a valid JWT for the targeted system - **board_id**: id of an existing board the tests should be executed on -```powershell +```bash npx artillery run --variables "{'target': 'https://main.dbc.dbildungscloud.dev', 'token': 'eJ....', 'board_id': '668d0e03bf3689d12e1e86fb' }" './scenarios/3users.yml' --output artilleryreport.json ``` +On Windows Powershell, the variables value needs to be wrapped in singlequotes, and inside the json you need to use backslash-escaped doublequotes: + +```powershell +npx artillery run --variables '{\"target\": \"https://main.dbc.dbildungscloud.dev\", \"token\": \"eJ....\", \"board_id\": \"668d0e03bf3689d12e1e86fb\" }' './scenarios/3users.yml' --output artilleryreport.json +``` + ## visualizing the recorded results It is possible to generate a HTML-report based on the recorded data. From 325d9dc563d5f7061b047195b2667cd8c76aee3f Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Mon, 15 Jul 2024 13:54:45 +0200 Subject: [PATCH 16/17] chore: fix tests --- .../src/modules/board/uc/card.uc.spec.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/server/src/modules/board/uc/card.uc.spec.ts b/apps/server/src/modules/board/uc/card.uc.spec.ts index 19a37861ea4..6261998a22b 100644 --- a/apps/server/src/modules/board/uc/card.uc.spec.ts +++ b/apps/server/src/modules/board/uc/card.uc.spec.ts @@ -73,14 +73,26 @@ describe(CardUc.name, () => { const cards = cardFactory.buildList(3); const cardIds = cards.map((c) => c.id); - boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValue( + boardNodeAuthorizableService.getBoardAuthorizables.mockResolvedValue([ new BoardNodeAuthorizable({ users: [], - id: new ObjectId().toHexString(), + id: cards[0].id, boardNode: cards[0], rootNode: columnBoardFactory.build(), - }) - ); + }), + new BoardNodeAuthorizable({ + users: [], + id: cards[1].id, + boardNode: cards[1], + rootNode: columnBoardFactory.build(), + }), + new BoardNodeAuthorizable({ + users: [], + id: cards[2].id, + boardNode: cards[2], + rootNode: columnBoardFactory.build(), + }), + ]); authorizationService.hasPermission.mockReturnValue(true); return { user, cards, cardIds }; @@ -109,7 +121,7 @@ describe(CardUc.name, () => { await uc.findCards(user.id, cardIds); - expect(boardNodeAuthorizableService.getBoardAuthorizable).toHaveBeenCalledTimes(3); + expect(boardNodeAuthorizableService.getBoardAuthorizables).toHaveBeenCalledTimes(1); }); it('should call the service to check the user permission', async () => { From 5ebe901c1bc74c404552b8c8c63d5074e41e8c74 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport Date: Mon, 15 Jul 2024 15:50:30 +0200 Subject: [PATCH 17/17] chore: remove unneeded import --- apps/server/src/modules/board/uc/card.uc.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/server/src/modules/board/uc/card.uc.spec.ts b/apps/server/src/modules/board/uc/card.uc.spec.ts index 6261998a22b..be5bba4afa8 100644 --- a/apps/server/src/modules/board/uc/card.uc.spec.ts +++ b/apps/server/src/modules/board/uc/card.uc.spec.ts @@ -1,5 +1,4 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ObjectId } from '@mikro-orm/mongodb'; import { Action, AuthorizationService } from '@modules/authorization'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities, userFactory } from '@shared/testing';