From 5b48a9a07b62d158e1075727e314f677411b9091 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Fri, 30 Aug 2024 15:22:15 +0200 Subject: [PATCH 01/20] Add tldraw-document controller --- .env.default | 13 +++ package-lock.json | 90 +++++++++++++++++++ package.json | 7 ++ src/infra/auth-guard/auth-guard.module.ts | 10 +++ src/infra/auth-guard/config/index.ts | 1 + .../auth-guard/config/x-api-key.config.ts | 3 + src/infra/auth-guard/guard/index.ts | 1 + .../auth-guard/guard/x-api-key-auth.guard.ts | 6 ++ src/infra/auth-guard/index.ts | 2 + src/infra/auth-guard/interface/index.ts | 1 + .../auth-guard/interface/strategy-type.ts | 3 + src/infra/auth-guard/strategy/index.ts | 1 + .../strategy/x-api-key.strategy.spec.ts | 76 ++++++++++++++++ .../auth-guard/strategy/x-api-key.strategy.ts | 23 +++++ src/modules/server/api/dto/index.ts | 1 + .../api/dto/tldraw-document-delete.params.ts | 3 + .../server/api/tldraw-document.controller.ts | 13 +++ src/modules/server/server.module.ts | 10 ++- 18 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 .env.default create mode 100644 src/infra/auth-guard/auth-guard.module.ts create mode 100644 src/infra/auth-guard/config/index.ts create mode 100644 src/infra/auth-guard/config/x-api-key.config.ts create mode 100644 src/infra/auth-guard/guard/index.ts create mode 100644 src/infra/auth-guard/guard/x-api-key-auth.guard.ts create mode 100644 src/infra/auth-guard/index.ts create mode 100644 src/infra/auth-guard/interface/index.ts create mode 100644 src/infra/auth-guard/interface/strategy-type.ts create mode 100644 src/infra/auth-guard/strategy/index.ts create mode 100644 src/infra/auth-guard/strategy/x-api-key.strategy.spec.ts create mode 100644 src/infra/auth-guard/strategy/x-api-key.strategy.ts create mode 100644 src/modules/server/api/dto/index.ts create mode 100644 src/modules/server/api/dto/tldraw-document-delete.params.ts create mode 100644 src/modules/server/api/tldraw-document.controller.ts diff --git a/.env.default b/.env.default new file mode 100644 index 0000000..e5ae46a --- /dev/null +++ b/.env.default @@ -0,0 +1,13 @@ +REDIS=redis://172.18.0.5:6379 +REDIS_PREFIX=y +API_HOST=http://localhost:3030 +ADMIN_API__ALLOWED_API_KEYS=randomString + +S3_ENDPOINT=localhost +S3_BUCKET=ydocs +S3_PORT=9000 +S3_SSL=false +S3_ACCESS_KEY=miniouser +S3_SECRET_KEY=miniouser + +WS_PORT="3345" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8b801e4..26116d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,20 @@ "version": "0.0.1", "license": "AGPL-3.0", "dependencies": { + "@golevelup/ts-jest": "^0.5.4", "@nestjs/common": "^10.4.1", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.4.1", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.4.1", "@y/redis": "github:hpi-schul-cloud/y-redis#9933ff3abd948bff33c540f07b0671fe657ecd41", "ioredis": "^5.4.1", + "passport": "^0.7.0", + "passport-headerapikey": "^1.2.2", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "save-dev": "^0.0.1-security", "uws": "github:uNetworking/uWebSockets.js#v20.47.0", "y-protocols": "^1.0.6" }, @@ -28,6 +33,8 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/node": "^20.3.1", + "@types/passport": "^1.0.16", + "@types/passport-strategy": "^0.2.38", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", @@ -859,6 +866,12 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@golevelup/ts-jest": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@golevelup/ts-jest/-/ts-jest-0.5.4.tgz", + "integrity": "sha512-2djwElTE0dMyjK6uYk7YqGs5BoA++O+bqhzDVsWHXrUD9XlOj3Ig0JeyILC8P7eBvSbWPaA61xaJX4doDvi4oQ==", + "license": "MIT" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1661,6 +1674,16 @@ } } }, + "node_modules/@nestjs/passport": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz", @@ -2134,6 +2157,27 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/passport": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", + "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", @@ -7022,6 +7066,42 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-headerapikey": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/passport-headerapikey/-/passport-headerapikey-1.2.2.tgz", + "integrity": "sha512-4BvVJRrWsNJPrd3UoZfcnnl4zvUWYKEtfYkoDsaOKBsrWHYmzTApCjs7qUbncOLexE9ul0IRiYBFfBG0y9IVQA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7091,6 +7171,11 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", @@ -7749,6 +7834,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/save-dev": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/save-dev/-/save-dev-0.0.1-security.tgz", + "integrity": "sha512-k6knZTDNK8PKKbIqnvxiOveJinuw2LcQjqDoaorZWP9M5AR2EPsnpDeSbeoZZ0pHr5ze1uoaKdK8NBGQrJ34Uw==" + }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", diff --git a/package.json b/package.json index 19c4e46..30ab5f5 100644 --- a/package.json +++ b/package.json @@ -27,15 +27,20 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand" }, "dependencies": { + "@golevelup/ts-jest": "^0.5.4", "@nestjs/common": "^10.4.1", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.4.1", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.4.1", "@y/redis": "github:hpi-schul-cloud/y-redis#9933ff3abd948bff33c540f07b0671fe657ecd41", "ioredis": "^5.4.1", + "passport": "^0.7.0", + "passport-headerapikey": "^1.2.2", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "save-dev": "^0.0.1-security", "uws": "github:uNetworking/uWebSockets.js#v20.47.0", "y-protocols": "^1.0.6" }, @@ -46,6 +51,8 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/node": "^20.3.1", + "@types/passport": "^1.0.16", + "@types/passport-strategy": "^0.2.38", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", diff --git a/src/infra/auth-guard/auth-guard.module.ts b/src/infra/auth-guard/auth-guard.module.ts new file mode 100644 index 0000000..c175934 --- /dev/null +++ b/src/infra/auth-guard/auth-guard.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { PassportModule } from '@nestjs/passport'; +import { XApiKeyStrategy } from './strategy/index.js'; + +@Module({ + imports: [PassportModule], + providers: [XApiKeyStrategy], + exports: [], +}) +export class AuthGuardModule {} diff --git a/src/infra/auth-guard/config/index.ts b/src/infra/auth-guard/config/index.ts new file mode 100644 index 0000000..63c916b --- /dev/null +++ b/src/infra/auth-guard/config/index.ts @@ -0,0 +1 @@ +export * from './x-api-key.config.js'; diff --git a/src/infra/auth-guard/config/x-api-key.config.ts b/src/infra/auth-guard/config/x-api-key.config.ts new file mode 100644 index 0000000..97c189e --- /dev/null +++ b/src/infra/auth-guard/config/x-api-key.config.ts @@ -0,0 +1,3 @@ +export interface XApiKeyConfig { + ADMIN_API__ALLOWED_API_KEYS: string[]; +} diff --git a/src/infra/auth-guard/guard/index.ts b/src/infra/auth-guard/guard/index.ts new file mode 100644 index 0000000..7ee9d81 --- /dev/null +++ b/src/infra/auth-guard/guard/index.ts @@ -0,0 +1 @@ +export * from './x-api-key-auth.guard.js'; diff --git a/src/infra/auth-guard/guard/x-api-key-auth.guard.ts b/src/infra/auth-guard/guard/x-api-key-auth.guard.ts new file mode 100644 index 0000000..1f7e6d7 --- /dev/null +++ b/src/infra/auth-guard/guard/x-api-key-auth.guard.ts @@ -0,0 +1,6 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { StrategyType } from '../interface/index.js'; + +@Injectable() +export class ApiKeyGuard extends AuthGuard(StrategyType.API_KEY) {} diff --git a/src/infra/auth-guard/index.ts b/src/infra/auth-guard/index.ts new file mode 100644 index 0000000..e991c13 --- /dev/null +++ b/src/infra/auth-guard/index.ts @@ -0,0 +1,2 @@ +export { AuthGuardModule } from './auth-guard.module.js'; +export { ApiKeyGuard } from './guard/index.js'; diff --git a/src/infra/auth-guard/interface/index.ts b/src/infra/auth-guard/interface/index.ts new file mode 100644 index 0000000..89ea46c --- /dev/null +++ b/src/infra/auth-guard/interface/index.ts @@ -0,0 +1 @@ +export * from './strategy-type.js'; diff --git a/src/infra/auth-guard/interface/strategy-type.ts b/src/infra/auth-guard/interface/strategy-type.ts new file mode 100644 index 0000000..310df1d --- /dev/null +++ b/src/infra/auth-guard/interface/strategy-type.ts @@ -0,0 +1,3 @@ +export enum StrategyType { + API_KEY = 'api-key', +} diff --git a/src/infra/auth-guard/strategy/index.ts b/src/infra/auth-guard/strategy/index.ts new file mode 100644 index 0000000..d44dd23 --- /dev/null +++ b/src/infra/auth-guard/strategy/index.ts @@ -0,0 +1 @@ +export * from './x-api-key.strategy.js'; diff --git a/src/infra/auth-guard/strategy/x-api-key.strategy.spec.ts b/src/infra/auth-guard/strategy/x-api-key.strategy.spec.ts new file mode 100644 index 0000000..7bc0050 --- /dev/null +++ b/src/infra/auth-guard/strategy/x-api-key.strategy.spec.ts @@ -0,0 +1,76 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { createMock } from '@golevelup/ts-jest'; +import { XApiKeyStrategy } from './x-api-key.strategy.js'; +import { XApiKeyConfig } from '../config/x-api-key.config.js'; + +describe('XApiKeyStrategy', () => { + let module: TestingModule; + let strategy: XApiKeyStrategy; + let configService: ConfigService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [], + providers: [ + XApiKeyStrategy, + { + provide: ConfigService, + useValue: createMock>({ + get: () => ['7ccd4e11-c6f6-48b0-81eb-cccf7922e7a4'], + }), + }, + ], + }).compile(); + + strategy = module.get(XApiKeyStrategy); + configService = module.get(ConfigService); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('validate', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const done = jest.fn((error: Error | null, data: boolean | null) => {}); + describe('when a valid api key is provided', () => { + const setup = () => { + const CORRECT_API_KEY = '7ccd4e11-c6f6-48b0-81eb-cccf7922e7a4'; + + return { CORRECT_API_KEY, done }; + }; + it('should do nothing', () => { + const { CORRECT_API_KEY } = setup(); + strategy.validate(CORRECT_API_KEY, done); + expect(done).toBeCalledWith(null, true); + }); + }); + + describe('when a invalid api key is provided', () => { + const setup = () => { + const INVALID_API_KEY = '7ccd4e11-c6f6-48b0-81eb-cccf7922e7a4BAD'; + + return { INVALID_API_KEY, done }; + }; + it('should throw error', () => { + const { INVALID_API_KEY } = setup(); + strategy.validate(INVALID_API_KEY, done); + expect(done).toBeCalledWith(new UnauthorizedException(), null); + }); + }); + }); + + describe('constructor', () => { + it('should create strategy', () => { + const ApiKeyStrategy = new XApiKeyStrategy(configService); + expect(ApiKeyStrategy).toBeDefined(); + expect(ApiKeyStrategy).toBeInstanceOf(XApiKeyStrategy); + }); + }); +}); diff --git a/src/infra/auth-guard/strategy/x-api-key.strategy.ts b/src/infra/auth-guard/strategy/x-api-key.strategy.ts new file mode 100644 index 0000000..8df6b6b --- /dev/null +++ b/src/infra/auth-guard/strategy/x-api-key.strategy.ts @@ -0,0 +1,23 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { XApiKeyConfig } from '../config/index.js'; +import { StrategyType } from '../interface/index.js'; +import { Strategy } from 'passport-headerapikey/lib/Strategy.js'; + +@Injectable() +export class XApiKeyStrategy extends PassportStrategy(Strategy, StrategyType.API_KEY) { + private readonly allowedApiKeys: string[]; + + constructor(private readonly configService: ConfigService) { + super({ header: 'X-API-KEY' }, false); + this.allowedApiKeys = this.configService.get('ADMIN_API__ALLOWED_API_KEYS'); + } + + public validate = (apiKey: string, done: (error: Error | null, data: boolean | null) => void) => { + if (this.allowedApiKeys.includes(apiKey)) { + done(null, true); + } + done(new UnauthorizedException(), null); + }; +} diff --git a/src/modules/server/api/dto/index.ts b/src/modules/server/api/dto/index.ts new file mode 100644 index 0000000..c8fa647 --- /dev/null +++ b/src/modules/server/api/dto/index.ts @@ -0,0 +1 @@ +export * from './tldraw-document-delete.params.js'; diff --git a/src/modules/server/api/dto/tldraw-document-delete.params.ts b/src/modules/server/api/dto/tldraw-document-delete.params.ts new file mode 100644 index 0000000..5feeba0 --- /dev/null +++ b/src/modules/server/api/dto/tldraw-document-delete.params.ts @@ -0,0 +1,3 @@ +export class TldrawDocumentDeleteParams { + docName!: string; +} diff --git a/src/modules/server/api/tldraw-document.controller.ts b/src/modules/server/api/tldraw-document.controller.ts new file mode 100644 index 0000000..5f1f32f --- /dev/null +++ b/src/modules/server/api/tldraw-document.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Delete, HttpCode, Param, UseGuards } from '@nestjs/common'; +import { TldrawDocumentDeleteParams } from './dto/index.js'; +import { ApiKeyGuard } from '../../../infra/auth-guard/guard/index.js'; + +@UseGuards(ApiKeyGuard) +@Controller('tldraw-document') +export class TldrawDocumentController { + @HttpCode(204) + @Delete(':docName') + async deleteByDocName(@Param() urlParams: TldrawDocumentDeleteParams) { + console.log('deleteByDocName', urlParams); + } +} diff --git a/src/modules/server/server.module.ts b/src/modules/server/server.module.ts index 84ecaf1..b75f908 100644 --- a/src/modules/server/server.module.ts +++ b/src/modules/server/server.module.ts @@ -6,9 +6,17 @@ import { LoggerModule } from '../../infra/logging/logger.module.js'; import { RedisModule } from '../../infra/redis/index.js'; import { StorageModule } from '../../infra/storage/storage.module.js'; import { UWS, WebsocketGateway } from './api/websocket.gateway.js'; +import { AuthGuardModule } from '../../infra/auth-guard/auth-guard.module.js'; @Module({ - imports: [ConfigModule.forRoot({ isGlobal: true }), RedisModule, StorageModule, AuthorizationModule, LoggerModule], + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + RedisModule, + StorageModule, + AuthorizationModule, + LoggerModule, + AuthGuardModule, + ], providers: [ WebsocketGateway, { From 447f00cab5b875893e64267d22b2d25e5a06c877 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Tue, 3 Sep 2024 13:56:44 +0200 Subject: [PATCH 02/20] Add delete handling --- src/apps/tldraw-server.ts | 2 +- .../server/api/tldraw-document.controller.ts | 33 +++++++++++++++++-- src/modules/server/server.module.ts | 2 ++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/apps/tldraw-server.ts b/src/apps/tldraw-server.ts index 6ba638b..d7b8501 100644 --- a/src/apps/tldraw-server.ts +++ b/src/apps/tldraw-server.ts @@ -4,7 +4,7 @@ import { MetricsModule } from '../infra/metrics/metrics.module.js'; import { ServerModule } from '../modules/server/server.module.js'; async function bootstrap() { - const httpPort = 3347; + const httpPort = 3349; const nestApp = await NestFactory.create(ServerModule); nestApp.enableCors(); await nestApp.init(); diff --git a/src/modules/server/api/tldraw-document.controller.ts b/src/modules/server/api/tldraw-document.controller.ts index 5f1f32f..ca5d61a 100644 --- a/src/modules/server/api/tldraw-document.controller.ts +++ b/src/modules/server/api/tldraw-document.controller.ts @@ -1,13 +1,42 @@ -import { Controller, Delete, HttpCode, Param, UseGuards } from '@nestjs/common'; +import { Controller, Delete, HttpCode, Inject, Param, UseGuards } from '@nestjs/common'; import { TldrawDocumentDeleteParams } from './dto/index.js'; import { ApiKeyGuard } from '../../../infra/auth-guard/guard/index.js'; +import { StorageService } from '../../../infra/storage/storage.service.js'; +import { TemplatedApp } from 'uws'; +import { UWS } from './websocket.gateway.js'; +import { RedisService } from '../../../infra/redis/index.js'; @UseGuards(ApiKeyGuard) @Controller('tldraw-document') export class TldrawDocumentController { + constructor( + private readonly storage: StorageService, + @Inject(UWS) private webSocketServer: TemplatedApp, + private readonly redisService: RedisService, + ) {} + @HttpCode(204) @Delete(':docName') async deleteByDocName(@Param() urlParams: TldrawDocumentDeleteParams) { - console.log('deleteByDocName', urlParams); + const docName = `y:room:${urlParams.docName}:index`; + + // Tell the client that the doc is deleted + this.webSocketServer.publish(docName, 'deleted'); + + // Delete doc in redis + const redis = await this.redisService.createRedisInstance(); + redis.del(docName); + + // Delete doc in s3 + const store = await this.storage.get(); + + const objectsList = []; + const stream = store.client.listObjectsV2('ydocs', urlParams.docName, true); + + for await (const obj of stream) { + objectsList.push(obj.name); + } + + await store.client.removeObjects('ydocs', objectsList); } } diff --git a/src/modules/server/server.module.ts b/src/modules/server/server.module.ts index b75f908..532f2cf 100644 --- a/src/modules/server/server.module.ts +++ b/src/modules/server/server.module.ts @@ -7,6 +7,7 @@ import { RedisModule } from '../../infra/redis/index.js'; import { StorageModule } from '../../infra/storage/storage.module.js'; import { UWS, WebsocketGateway } from './api/websocket.gateway.js'; import { AuthGuardModule } from '../../infra/auth-guard/auth-guard.module.js'; +import { TldrawDocumentController } from './api/tldraw-document.controller.js'; @Module({ imports: [ @@ -24,5 +25,6 @@ import { AuthGuardModule } from '../../infra/auth-guard/auth-guard.module.js'; useValue: App({}), }, ], + controllers: [TldrawDocumentController], }) export class ServerModule {} From 1e77f403b2e1ef34851445c1d3a12ba0621e7788 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Wed, 4 Sep 2024 15:24:37 +0200 Subject: [PATCH 03/20] Encode delete message in yjs format --- .../server/api/tldraw-document.controller.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/modules/server/api/tldraw-document.controller.ts b/src/modules/server/api/tldraw-document.controller.ts index ca5d61a..9dd3a5b 100644 --- a/src/modules/server/api/tldraw-document.controller.ts +++ b/src/modules/server/api/tldraw-document.controller.ts @@ -5,6 +5,8 @@ import { StorageService } from '../../../infra/storage/storage.service.js'; import { TemplatedApp } from 'uws'; import { UWS } from './websocket.gateway.js'; import { RedisService } from '../../../infra/redis/index.js'; +import * as encoding from 'lib0/encoding'; +import * as Y from 'yjs'; @UseGuards(ApiKeyGuard) @Controller('tldraw-document') @@ -21,7 +23,7 @@ export class TldrawDocumentController { const docName = `y:room:${urlParams.docName}:index`; // Tell the client that the doc is deleted - this.webSocketServer.publish(docName, 'deleted'); + this.sendDeleteMessage(docName); // Delete doc in redis const redis = await this.redisService.createRedisInstance(); @@ -39,4 +41,13 @@ export class TldrawDocumentController { await store.client.removeObjects('ydocs', objectsList); } + + private sendDeleteMessage(topic: string) { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, 3); + encoding.writeVarString(encoder, 'deleted'); + const message = encoding.toUint8Array(encoder); + + this.webSocketServer.publish(topic, message, true); + } } From bce795a95db835a8344f22219be24e5a2c46066b Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Thu, 5 Sep 2024 13:20:41 +0200 Subject: [PATCH 04/20] Simplify publish --- .../server/api/tldraw-document.controller.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/modules/server/api/tldraw-document.controller.ts b/src/modules/server/api/tldraw-document.controller.ts index 9dd3a5b..ca5d61a 100644 --- a/src/modules/server/api/tldraw-document.controller.ts +++ b/src/modules/server/api/tldraw-document.controller.ts @@ -5,8 +5,6 @@ import { StorageService } from '../../../infra/storage/storage.service.js'; import { TemplatedApp } from 'uws'; import { UWS } from './websocket.gateway.js'; import { RedisService } from '../../../infra/redis/index.js'; -import * as encoding from 'lib0/encoding'; -import * as Y from 'yjs'; @UseGuards(ApiKeyGuard) @Controller('tldraw-document') @@ -23,7 +21,7 @@ export class TldrawDocumentController { const docName = `y:room:${urlParams.docName}:index`; // Tell the client that the doc is deleted - this.sendDeleteMessage(docName); + this.webSocketServer.publish(docName, 'deleted'); // Delete doc in redis const redis = await this.redisService.createRedisInstance(); @@ -41,13 +39,4 @@ export class TldrawDocumentController { await store.client.removeObjects('ydocs', objectsList); } - - private sendDeleteMessage(topic: string) { - const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, 3); - encoding.writeVarString(encoder, 'deleted'); - const message = encoding.toUint8Array(encoder); - - this.webSocketServer.publish(topic, message, true); - } } From 3da32082223946001be65cfa5ca910bfcb8c8d29 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Thu, 5 Sep 2024 14:02:19 +0200 Subject: [PATCH 05/20] Add service to controller --- src/infra/redis/redis.service.ts | 8 ++++- src/infra/storage/storage.service.ts | 15 +++++++- .../api/dto/tldraw-document-delete.params.ts | 2 +- .../server/api/tldraw-document.controller.ts | 36 +++---------------- src/modules/server/server.module.ts | 2 ++ .../server/service/tldraw-document.service.ts | 24 +++++++++++++ 6 files changed, 53 insertions(+), 34 deletions(-) create mode 100644 src/modules/server/service/tldraw-document.service.ts diff --git a/src/infra/redis/redis.service.ts b/src/infra/redis/redis.service.ts index 97a578d..d39ecd0 100644 --- a/src/infra/redis/redis.service.ts +++ b/src/infra/redis/redis.service.ts @@ -18,7 +18,7 @@ export class RedisService { this.logger.setContext(RedisService.name); } - async createRedisInstance() { + public async createRedisInstance(): Promise { let redisInstance: Redis; if (this.sentinelServiceName) { redisInstance = await this.createRedisSentinelInstance(); @@ -29,6 +29,12 @@ export class RedisService { return redisInstance; } + public async deleteDocument(docName: string): Promise { + const redisInstance = await this.createRedisInstance(); + + await redisInstance.del(docName); + } + private createNewRedisInstance() { const redisUrl = this.configService.getOrThrow('REDIS'); const redisInstance = new Redis(redisUrl); diff --git a/src/infra/storage/storage.service.ts b/src/infra/storage/storage.service.ts index 3208e21..b21776d 100644 --- a/src/infra/storage/storage.service.ts +++ b/src/infra/storage/storage.service.ts @@ -11,7 +11,7 @@ export class StorageService { this.logger.setContext(StorageService.name); } - async get() { + public async get() { const s3Endpoint = this.configService.get('S3_ENDPOINT'); const bucketName = this.configService.get('S3_BUCKET') || 'ydocs'; @@ -35,4 +35,17 @@ export class StorageService { } return store; } + + public async deleteDocument(parentId: string): Promise { + const store = await this.get(); + + const objectsList = []; + const stream = store.client.listObjectsV2('ydocs', parentId, true); + + for await (const obj of stream) { + objectsList.push(obj.name); + } + + await store.client.removeObjects('ydocs', objectsList); + } } diff --git a/src/modules/server/api/dto/tldraw-document-delete.params.ts b/src/modules/server/api/dto/tldraw-document-delete.params.ts index 5feeba0..2d392c9 100644 --- a/src/modules/server/api/dto/tldraw-document-delete.params.ts +++ b/src/modules/server/api/dto/tldraw-document-delete.params.ts @@ -1,3 +1,3 @@ export class TldrawDocumentDeleteParams { - docName!: string; + parentId!: string; } diff --git a/src/modules/server/api/tldraw-document.controller.ts b/src/modules/server/api/tldraw-document.controller.ts index ca5d61a..e56c5be 100644 --- a/src/modules/server/api/tldraw-document.controller.ts +++ b/src/modules/server/api/tldraw-document.controller.ts @@ -1,42 +1,16 @@ -import { Controller, Delete, HttpCode, Inject, Param, UseGuards } from '@nestjs/common'; +import { Controller, Delete, HttpCode, Param, UseGuards } from '@nestjs/common'; import { TldrawDocumentDeleteParams } from './dto/index.js'; import { ApiKeyGuard } from '../../../infra/auth-guard/guard/index.js'; -import { StorageService } from '../../../infra/storage/storage.service.js'; -import { TemplatedApp } from 'uws'; -import { UWS } from './websocket.gateway.js'; -import { RedisService } from '../../../infra/redis/index.js'; +import { TldrawDocumentService } from '../service/tldraw-document.service.js'; @UseGuards(ApiKeyGuard) @Controller('tldraw-document') export class TldrawDocumentController { - constructor( - private readonly storage: StorageService, - @Inject(UWS) private webSocketServer: TemplatedApp, - private readonly redisService: RedisService, - ) {} + constructor(private readonly tldrawDocumentService: TldrawDocumentService) {} @HttpCode(204) - @Delete(':docName') + @Delete(':parentId') async deleteByDocName(@Param() urlParams: TldrawDocumentDeleteParams) { - const docName = `y:room:${urlParams.docName}:index`; - - // Tell the client that the doc is deleted - this.webSocketServer.publish(docName, 'deleted'); - - // Delete doc in redis - const redis = await this.redisService.createRedisInstance(); - redis.del(docName); - - // Delete doc in s3 - const store = await this.storage.get(); - - const objectsList = []; - const stream = store.client.listObjectsV2('ydocs', urlParams.docName, true); - - for await (const obj of stream) { - objectsList.push(obj.name); - } - - await store.client.removeObjects('ydocs', objectsList); + this.tldrawDocumentService.deleteByDocName(urlParams.parentId); } } diff --git a/src/modules/server/server.module.ts b/src/modules/server/server.module.ts index 532f2cf..9dac117 100644 --- a/src/modules/server/server.module.ts +++ b/src/modules/server/server.module.ts @@ -8,6 +8,7 @@ import { StorageModule } from '../../infra/storage/storage.module.js'; import { UWS, WebsocketGateway } from './api/websocket.gateway.js'; import { AuthGuardModule } from '../../infra/auth-guard/auth-guard.module.js'; import { TldrawDocumentController } from './api/tldraw-document.controller.js'; +import { TldrawDocumentService } from './service/tldraw-document.service.js'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { TldrawDocumentController } from './api/tldraw-document.controller.js'; ], providers: [ WebsocketGateway, + TldrawDocumentService, { provide: UWS, useValue: App({}), diff --git a/src/modules/server/service/tldraw-document.service.ts b/src/modules/server/service/tldraw-document.service.ts new file mode 100644 index 0000000..cf6615e --- /dev/null +++ b/src/modules/server/service/tldraw-document.service.ts @@ -0,0 +1,24 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { RedisService } from '../../../infra/redis/index.js'; +import { StorageService } from '../../../infra/storage/index.js'; +import { TemplatedApp } from 'uws'; +import { UWS } from '../api/websocket.gateway.js'; + +@Injectable() +export class TldrawDocumentService { + constructor( + private readonly storage: StorageService, + @Inject(UWS) private webSocketServer: TemplatedApp, + private readonly redisService: RedisService, + ) {} + + async deleteByDocName(parentId: string) { + const docName = `y:room:${parentId}:index`; + + this.webSocketServer.publish(docName, 'deleted'); + + await this.redisService.deleteDocument(docName); + + this.storage.deleteDocument(parentId); + } +} From fbcc394bb8a4461eaf079a2d6506d12d13178784 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Thu, 5 Sep 2024 14:30:20 +0200 Subject: [PATCH 06/20] Remove package save dev --- package-lock.json | 6 ------ package.json | 1 - 2 files changed, 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 26116d4..28b8967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "save-dev": "^0.0.1-security", "uws": "github:uNetworking/uWebSockets.js#v20.47.0", "y-protocols": "^1.0.6" }, @@ -7834,11 +7833,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/save-dev": { - "version": "0.0.1-security", - "resolved": "https://registry.npmjs.org/save-dev/-/save-dev-0.0.1-security.tgz", - "integrity": "sha512-k6knZTDNK8PKKbIqnvxiOveJinuw2LcQjqDoaorZWP9M5AR2EPsnpDeSbeoZZ0pHr5ze1uoaKdK8NBGQrJ34Uw==" - }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", diff --git a/package.json b/package.json index 30ab5f5..77498a5 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "save-dev": "^0.0.1-security", "uws": "github:uNetworking/uWebSockets.js#v20.47.0", "y-protocols": "^1.0.6" }, From 1368ee6751abbb4e517a3013a9fe2b57b0c1ad25 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Mon, 9 Sep 2024 12:54:13 +0200 Subject: [PATCH 07/20] Add instance getter to stroage and redis service --- src/infra/redis/redis.service.ts | 17 +++++++++++++---- src/infra/storage/storage.service.ts | 12 +++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/infra/redis/redis.service.ts b/src/infra/redis/redis.service.ts index d39ecd0..2ff27e9 100644 --- a/src/infra/redis/redis.service.ts +++ b/src/infra/redis/redis.service.ts @@ -4,17 +4,18 @@ import * as dns from 'dns'; import { Redis } from 'ioredis'; import * as util from 'util'; import { Logger } from '../logging/logger.js'; +import internal from 'stream'; @Injectable() export class RedisService { private sentinelServiceName: string; + private internalRedisInstance?: Redis; constructor( private configService: ConfigService, private logger: Logger, ) { this.sentinelServiceName = this.configService.get('REDIS_SENTINEL_SERVICE_NAME') || ''; - this.logger.setContext(RedisService.name); } @@ -30,19 +31,27 @@ export class RedisService { } public async deleteDocument(docName: string): Promise { - const redisInstance = await this.createRedisInstance(); + const redisInstance = await this.getInternalRedisInstance(); await redisInstance.del(docName); } - private createNewRedisInstance() { + private async getInternalRedisInstance(): Promise { + if (!this.internalRedisInstance) { + this.internalRedisInstance = this.createNewRedisInstance(); + } + + return this.internalRedisInstance; + } + + private createNewRedisInstance(): Redis { const redisUrl = this.configService.getOrThrow('REDIS'); const redisInstance = new Redis(redisUrl); return redisInstance; } - private async createRedisSentinelInstance() { + private async createRedisSentinelInstance(): Promise { const sentinelName = this.configService.get('REDIS_SENTINEL_NAME') || 'mymaster'; const sentinelPassword = this.configService.getOrThrow('REDIS_SENTINEL_PASSWORD'); const sentinels = await this.discoverSentinelHosts(); diff --git a/src/infra/storage/storage.service.ts b/src/infra/storage/storage.service.ts index b21776d..bb08a1f 100644 --- a/src/infra/storage/storage.service.ts +++ b/src/infra/storage/storage.service.ts @@ -7,6 +7,7 @@ export class StorageService { constructor( private configService: ConfigService, private logger: Logger, + private internalStorageInstance?: any, ) { this.logger.setContext(StorageService.name); } @@ -33,11 +34,12 @@ export class StorageService { const { createMemoryStorage } = await import('@y/redis/storage/memory'); store = createMemoryStorage(); } + return store; } public async deleteDocument(parentId: string): Promise { - const store = await this.get(); + const store = await this.getInternalStorageInstance(); const objectsList = []; const stream = store.client.listObjectsV2('ydocs', parentId, true); @@ -48,4 +50,12 @@ export class StorageService { await store.client.removeObjects('ydocs', objectsList); } + + private async getInternalStorageInstance(): Promise { + if (!this.internalStorageInstance) { + this.internalStorageInstance = await this.get(); + } + + return this.internalStorageInstance; + } } From 3f94e8da088edbe4b0761bfd20f0106815996968 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Fri, 13 Sep 2024 08:08:33 +0200 Subject: [PATCH 08/20] Add api test --- jest.config.cjs | 24 +++ package.json | 17 -- src/infra/storage/storage.service.ts | 3 +- .../server/api/test/test-api-client.ts | 132 ++++++++++++ .../api/test/tldraw-document.api.spec.ts | 200 ++++++++++++++++++ .../server/service/tldraw-document.service.ts | 2 +- 6 files changed, 359 insertions(+), 19 deletions(-) create mode 100644 jest.config.cjs create mode 100644 src/modules/server/api/test/test-api-client.ts create mode 100644 src/modules/server/api/test/tldraw-document.api.spec.ts diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 0000000..074f5ac --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,24 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + extensionsToTreatAsEsm: ['.ts'], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + transform: { + '^.+\\.(t|j)s$': [ + 'ts-jest', + { + useESM: true, + }, + ], + }, + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: 'src', + testRegex: '.*\\.spec\\.ts$', + collectCoverageFrom: ['**/*.(t|j)s'], + coverageDirectory: '../coverage', + testEnvironment: 'node', + moduleNameMapper: { + '@y/redis': '@y/redis', + }, +}; diff --git a/package.json b/package.json index 77498a5..75de8a3 100644 --- a/package.json +++ b/package.json @@ -67,22 +67,5 @@ "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.5.4" - }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], - "coverageDirectory": "../coverage", - "testEnvironment": "node" } } diff --git a/src/infra/storage/storage.service.ts b/src/infra/storage/storage.service.ts index bb08a1f..601aeef 100644 --- a/src/infra/storage/storage.service.ts +++ b/src/infra/storage/storage.service.ts @@ -4,10 +4,11 @@ import { Logger } from '../logging/logger.js'; @Injectable() export class StorageService { + private internalStorageInstance?: any; + constructor( private configService: ConfigService, private logger: Logger, - private internalStorageInstance?: any, ) { this.logger.setContext(StorageService.name); } diff --git a/src/modules/server/api/test/test-api-client.ts b/src/modules/server/api/test/test-api-client.ts new file mode 100644 index 0000000..9024421 --- /dev/null +++ b/src/modules/server/api/test/test-api-client.ts @@ -0,0 +1,132 @@ +import { INestApplication } from '@nestjs/common'; +import supertest from 'supertest'; + +const headerConst = { + accept: 'accept', + json: 'application/json', +}; + +const testReqestConst = { + prefix: 'Bearer', + loginPath: '/authentication/local', + accessToken: 'accessToken', + errorMessage: 'TestApiClient: Can not cast to local AutenticationResponse:', +}; + +/** + * Note res.cookie is not supported atm, feel free to add this + */ +export class TestApiClient { + private readonly app: INestApplication; + + private readonly baseRoute: string; + + private readonly authHeader: string; + + private readonly kindOfAuth: string; + + constructor(app: INestApplication, baseRoute: string, authValue?: string, useAsApiKey = false) { + this.app = app; + this.baseRoute = this.checkAndAddPrefix(baseRoute); + this.authHeader = useAsApiKey ? `${authValue || ''}` : `${testReqestConst.prefix} ${authValue || ''}`; + this.kindOfAuth = useAsApiKey ? 'X-API-KEY' : 'authorization'; + } + + public get(subPath?: string): supertest.Test { + const path = this.getPath(subPath); + const testRequestInstance = supertest(this.app.getHttpServer()) + .get(path) + .set(this.kindOfAuth, this.authHeader) + .set(headerConst.accept, headerConst.json); + + return testRequestInstance; + } + + public delete(subPath?: string): supertest.Test { + const path = this.getPath(subPath); + const testRequestInstance = supertest(this.app.getHttpServer()) + .delete(path) + .set(this.kindOfAuth, this.authHeader) + .set(headerConst.accept, headerConst.json); + + return testRequestInstance; + } + + public put(subPath?: string, data?: T): supertest.Test { + const path = this.getPath(subPath); + const testRequestInstance = supertest(this.app.getHttpServer()) + .put(path) + .set(this.kindOfAuth, this.authHeader) + .send(data); + + return testRequestInstance; + } + + public patch(subPath?: string, data?: T): supertest.Test { + const path = this.getPath(subPath); + const testRequestInstance = supertest(this.app.getHttpServer()) + .patch(path) + .set(this.kindOfAuth, this.authHeader) + .send(data); + + return testRequestInstance; + } + + public post(subPath?: string, data?: T): supertest.Test { + const path = this.getPath(subPath); + const testRequestInstance = supertest(this.app.getHttpServer()) + .post(path) + .set(this.kindOfAuth, this.authHeader) + .set(headerConst.accept, headerConst.json) + .send(data); + + return testRequestInstance; + } + + public postWithAttachment( + subPath: string | undefined, + fieldName: string, + data: Buffer, + fileName: string, + ): supertest.Test { + const path = this.getPath(subPath); + const testRequestInstance = supertest(this.app.getHttpServer()) + .post(path) + .set(this.kindOfAuth, this.authHeader) + .attach(fieldName, data, fileName); + + return testRequestInstance; + } + + private isSlash(inputPath: string, pos: number): boolean { + const isSlash = inputPath.charAt(pos) === '/'; + + return isSlash; + } + + private checkAndAddPrefix(inputPath = '/'): string { + let path = ''; + if (!this.isSlash(inputPath, 0)) { + path = '/'; + } + path += inputPath; + + return path; + } + + private cleanupPath(inputPath: string): string { + let path = inputPath; + if (this.isSlash(path, 0) && this.isSlash(path, 1)) { + path = path.slice(1); + } + + return path; + } + + private getPath(routeNameInput = ''): string { + const routeName = this.checkAndAddPrefix(routeNameInput); + const path = this.cleanupPath(this.baseRoute + routeName); + + return path; + } +} diff --git a/src/modules/server/api/test/tldraw-document.api.spec.ts b/src/modules/server/api/test/tldraw-document.api.spec.ts new file mode 100644 index 0000000..bbacf4c --- /dev/null +++ b/src/modules/server/api/test/tldraw-document.api.spec.ts @@ -0,0 +1,200 @@ +jest.mock('@y/redis'); + +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { StorageService } from '../../../../infra/storage/storage.service.js'; +import { RedisService } from '../../../../infra/redis/redis.service.js'; +import { App } from 'uws'; +import { createMock } from '@golevelup/ts-jest'; +import { ServerModule } from '../../server.module.js'; +import { WebsocketGateway } from '../websocket.gateway.js'; + +describe('Tldraw-Document Api Test', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [ServerModule], + }) + .overrideProvider(StorageService) + .useValue(createMock()) + .overrideProvider(RedisService) + .useValue(createMock()) + .overrideProvider('UWS') + .useValue(createMock()) + .overrideProvider(WebsocketGateway) + .useValue(createMock()) + .compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('deleteByDocName', () => { + it('true to be true', () => { + expect(true).toBe(true); + }); + /* describe('when no api key is provided', () => { + it('should return 401', async () => { + const someId = '123'; + + const response = await testApiClient.get(someId); + + expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); + }); + }); */ + + /* describe('when id in params is not a mongo id', () => { + const setup = async () => { + const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); + + await em.persistAndFlush([studentAccount, studentUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + return { loggedInClient }; + }; + + it('should return 400', async () => { + const { loggedInClient } = await setup(); + + const response = await loggedInClient.get(`id/123`); + + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body).toEqual( + expect.objectContaining({ + validationErrors: [{ errors: ['schoolId must be a mongodb id'], field: ['schoolId'] }], + }), + ); + }); + }); + + describe('when requested school is not found', () => { + const setup = async () => { + const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); + + await em.persistAndFlush([studentAccount, studentUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + return { loggedInClient }; + }; + + it('should return 404', async () => { + const { loggedInClient } = await setup(); + const someId = new ObjectId().toHexString(); + + const response = await loggedInClient.get(`id/${someId}`); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + }); + }); + + describe('when user is not in requested school', () => { + const setup = async () => { + const school = schoolEntityFactory.build(); + const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); + + await em.persistAndFlush([school, studentAccount, studentUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + return { schoolId: school.id, loggedInClient }; + }; + + it('should return 403', async () => { + const { schoolId, loggedInClient } = await setup(); + + const response = await loggedInClient.get(`id/${schoolId}`); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + }); + }); + + describe('when user is in requested school', () => { + const setup = async () => { + const schoolYears = schoolYearFactory.withStartYear(2002).buildList(3); + const currentYear = schoolYears[1]; + const federalState = federalStateFactory.build(); + const county = countyEmbeddableFactory.build(); + const systems = systemEntityFactory.buildList(3); + const school = schoolEntityFactory.build({ currentYear, federalState, systems, county }); + const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({ school }); + + await em.persistAndFlush([...schoolYears, federalState, school, studentAccount, studentUser]); + em.clear(); + + const schoolYearResponses = schoolYears.map((schoolYear) => { + return { + id: schoolYear.id, + name: schoolYear.name, + startDate: schoolYear.startDate.toISOString(), + endDate: schoolYear.endDate.toISOString(), + }; + }); + + const expectedResponse = { + id: school.id, + createdAt: school.createdAt.toISOString(), + updatedAt: school.updatedAt.toISOString(), + name: school.name, + federalState: { + id: federalState.id, + name: federalState.name, + abbreviation: federalState.abbreviation, + logoUrl: federalState.logoUrl, + counties: federalState.counties?.map((item) => { + return { + id: item._id.toHexString(), + name: item.name, + countyId: item.countyId, + antaresKey: item.antaresKey, + }; + }), + }, + county: { + id: county._id.toHexString(), + name: county.name, + countyId: county.countyId, + antaresKey: county.antaresKey, + }, + inUserMigration: undefined, + inMaintenance: false, + isExternal: false, + currentYear: schoolYearResponses[1], + years: { + schoolYears: schoolYearResponses, + activeYear: schoolYearResponses[1], + lastYear: schoolYearResponses[0], + nextYear: schoolYearResponses[2], + }, + features: [], + systemIds: systems.map((system) => system.id), + // TODO: The feature isTeamCreationByStudentsEnabled is set based on the config value STUDENT_TEAM_CREATION. + // We need to discuss how to go about the config in API tests! + instanceFeatures: ['isTeamCreationByStudentsEnabled'], + }; + + const loggedInClient = await testApiClient.login(studentAccount); + + return { schoolId: school.id, loggedInClient, expectedResponse }; + }; + + it('should return school', async () => { + const { schoolId, loggedInClient, expectedResponse } = await setup(); + + const response = await loggedInClient.get(`id/${schoolId}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body).toEqual(expectedResponse); + }); + }); */ + }); +}); diff --git a/src/modules/server/service/tldraw-document.service.ts b/src/modules/server/service/tldraw-document.service.ts index cf6615e..85133e5 100644 --- a/src/modules/server/service/tldraw-document.service.ts +++ b/src/modules/server/service/tldraw-document.service.ts @@ -2,7 +2,7 @@ import { Injectable, Inject } from '@nestjs/common'; import { RedisService } from '../../../infra/redis/index.js'; import { StorageService } from '../../../infra/storage/index.js'; import { TemplatedApp } from 'uws'; -import { UWS } from '../api/websocket.gateway.js'; +const UWS = 'UWS'; @Injectable() export class TldrawDocumentService { From 0273430f2f8929441c6261668f90fa48dbe4800e Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Fri, 13 Sep 2024 10:49:55 +0200 Subject: [PATCH 09/20] Add tldraw document service test --- .../service/tldraw-document.service.spec.ts | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/modules/server/service/tldraw-document.service.spec.ts diff --git a/src/modules/server/service/tldraw-document.service.spec.ts b/src/modules/server/service/tldraw-document.service.spec.ts new file mode 100644 index 0000000..1f63dc4 --- /dev/null +++ b/src/modules/server/service/tldraw-document.service.spec.ts @@ -0,0 +1,105 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { TldrawDocumentService } from './tldraw-document.service.js'; +import { RedisService } from '../../../infra/redis/index.js'; +import { StorageService } from '../../../infra/storage/index.js'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { TemplatedApp } from 'uws'; + +describe('Tldraw-Document Service', () => { + let app: INestApplication; + let service: TldrawDocumentService; + let webSocketServer: TemplatedApp; + let redisService: DeepMocked; + let storageService: DeepMocked; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + providers: [ + TldrawDocumentService, + { + provide: RedisService, + useValue: createMock(), + }, + { + provide: StorageService, + useValue: createMock(), + }, + { provide: 'UWS', useValue: createMock() }, + ], + }).compile(); + + service = moduleFixture.get(TldrawDocumentService); + webSocketServer = moduleFixture.get('UWS'); + redisService = moduleFixture.get(RedisService); + storageService = moduleFixture.get(StorageService); + }); + + describe('when redis and storage service returns successfully', () => { + const setup = () => { + const parentId = '123'; + const docName = `y:room:${parentId}:index`; + const expectedMessage = 'deleted'; + + return { parentId, docName, expectedMessage }; + }; + + it('should call publish', async () => { + const { parentId, docName, expectedMessage } = setup(); + + await service.deleteByDocName(parentId); + + expect(webSocketServer.publish).toHaveBeenCalledWith(docName, expectedMessage); + }); + + it('should call redisService deleteDocument', async () => { + const { parentId, docName, expectedMessage } = setup(); + + await service.deleteByDocName(parentId); + + expect(redisService.deleteDocument).toHaveBeenCalledWith(docName); + }); + + it('should call storageService deleteDocument', async () => { + const { parentId, docName, expectedMessage } = setup(); + + await service.deleteByDocName(parentId); + + expect(storageService.deleteDocument).toHaveBeenCalledWith(parentId); + }); + }); + + describe('when redis service throws error', () => { + const setup = () => { + const error = new Error('error'); + const parentId = '123'; + + redisService.deleteDocument.mockRejectedValueOnce(error); + + return { error, parentId }; + }; + + it('should return error', () => { + const { error, parentId } = setup(); + + expect(service.deleteByDocName(parentId)).rejects.toThrow(error); + }); + }); + + describe('when storage service throws error', () => { + const setup = () => { + const error = new Error('error'); + const parentId = '123'; + + storageService.deleteDocument.mockRejectedValueOnce(error); + + return { error, parentId }; + }; + + it('should return error', () => { + const { error, parentId } = setup(); + + expect(service.deleteByDocName(parentId)).rejects.toThrow(error); + }); + }); +}); From 2912dd969ab1a0c739dea5bafb7b76787452fb74 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Fri, 13 Sep 2024 10:50:25 +0200 Subject: [PATCH 10/20] Rename storage service in document service --- src/modules/server/service/tldraw-document.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/server/service/tldraw-document.service.ts b/src/modules/server/service/tldraw-document.service.ts index 85133e5..52c2a5b 100644 --- a/src/modules/server/service/tldraw-document.service.ts +++ b/src/modules/server/service/tldraw-document.service.ts @@ -7,7 +7,7 @@ const UWS = 'UWS'; @Injectable() export class TldrawDocumentService { constructor( - private readonly storage: StorageService, + private readonly storageService: StorageService, @Inject(UWS) private webSocketServer: TemplatedApp, private readonly redisService: RedisService, ) {} @@ -19,6 +19,6 @@ export class TldrawDocumentService { await this.redisService.deleteDocument(docName); - this.storage.deleteDocument(parentId); + await this.storageService.deleteDocument(parentId); } } From 48bf7e2901a5cf66759b5d3fa9fb93b79fc834b2 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Fri, 13 Sep 2024 12:25:54 +0200 Subject: [PATCH 11/20] Add redis service setup --- src/infra/redis/redis.service.spec.ts | 77 +++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/infra/redis/redis.service.spec.ts diff --git a/src/infra/redis/redis.service.spec.ts b/src/infra/redis/redis.service.spec.ts new file mode 100644 index 0000000..28c8a7b --- /dev/null +++ b/src/infra/redis/redis.service.spec.ts @@ -0,0 +1,77 @@ +import { ConfigService } from '@nestjs/config'; +import { RedisService } from './redis.service.js'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test } from '@nestjs/testing'; +import * as util from 'util'; +import * as ioredisModule from 'ioredis'; +import { Logger } from '../logging/logger.js'; + +const Redis = ioredisModule.Redis; + +describe('Redis Service', () => { + let service: RedisService; + let configService: DeepMocked; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + providers: [ + RedisService, + { + provide: ConfigService, + useValue: createMock(), + }, + { + provide: Logger, + useValue: createMock(), + }, + ], + }).compile(); + + service = moduleFixture.get(RedisService); + configService = moduleFixture.get(ConfigService); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('createRedisInstance', () => { + describe('when sentinelServiceName, sentinelName, sentinelPassword are set', () => { + describe('when resolveSrv resolves', () => { + const setup = () => { + const sentinelServiceName = 'serviceName'; + const sentinelName = 'sentinelName'; + const sentinelPassword = 'sentinelPassword'; + + configService.get.mockReturnValueOnce(sentinelServiceName); + configService.get.mockReturnValueOnce(sentinelName); + configService.get.mockReturnValueOnce(sentinelPassword); + + const name1 = 'name1'; + const name2 = 'name2'; + const port1 = '11'; + const port2 = '22'; + const records = [ + { name: name1, port: port1 }, + { name: name2, port: port2 }, + ]; + const resolveSrv = jest.fn().mockResolvedValueOnce(records); + jest.spyOn(util, 'promisify').mockReturnValueOnce(resolveSrv); + + const redisMock = createMock(); + jest.spyOn(ioredisModule, 'Redis').mockReturnValueOnce(redisMock); + + return { resolveSrv, sentinelServiceName }; + }; + + it('calls resolveSrv', async () => { + const { resolveSrv, sentinelServiceName } = setup(); + + await service.createRedisInstance(); + + expect(resolveSrv).toHaveBeenLastCalledWith(sentinelServiceName); + }); + }); + }); + }); +}); From f3636c99245935ba3d013e328606d6dcd77747fb Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Mon, 16 Sep 2024 09:55:30 +0200 Subject: [PATCH 12/20] Fix test --- src/infra/redis/redis.service.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/infra/redis/redis.service.spec.ts b/src/infra/redis/redis.service.spec.ts index 28c8a7b..79636c1 100644 --- a/src/infra/redis/redis.service.spec.ts +++ b/src/infra/redis/redis.service.spec.ts @@ -58,8 +58,8 @@ describe('Redis Service', () => { const resolveSrv = jest.fn().mockResolvedValueOnce(records); jest.spyOn(util, 'promisify').mockReturnValueOnce(resolveSrv); - const redisMock = createMock(); - jest.spyOn(ioredisModule, 'Redis').mockReturnValueOnce(redisMock); + //const redisMock = createMock(); + //jest.spyOn(ioredisModule, 'Redis').mockReturnValueOnce(redisMock); return { resolveSrv, sentinelServiceName }; }; From 18d8ddbab90f947da72f3c15f729a7c27912bc09 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Mon, 16 Sep 2024 09:55:42 +0200 Subject: [PATCH 13/20] Change delete message --- src/modules/server/service/tldraw-document.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/server/service/tldraw-document.service.ts b/src/modules/server/service/tldraw-document.service.ts index 52c2a5b..b79f951 100644 --- a/src/modules/server/service/tldraw-document.service.ts +++ b/src/modules/server/service/tldraw-document.service.ts @@ -15,7 +15,7 @@ export class TldrawDocumentService { async deleteByDocName(parentId: string) { const docName = `y:room:${parentId}:index`; - this.webSocketServer.publish(docName, 'deleted'); + this.webSocketServer.publish(docName, 'action:delete'); await this.redisService.deleteDocument(docName); From a665c88333a0363af8cfa744ff95671c149cf9ba Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Mon, 16 Sep 2024 10:00:02 +0200 Subject: [PATCH 14/20] remove commented out code --- .../api/test/tldraw-document.api.spec.ts | 160 +----------------- 1 file changed, 1 insertion(+), 159 deletions(-) diff --git a/src/modules/server/api/test/tldraw-document.api.spec.ts b/src/modules/server/api/test/tldraw-document.api.spec.ts index bbacf4c..ffab9ff 100644 --- a/src/modules/server/api/test/tldraw-document.api.spec.ts +++ b/src/modules/server/api/test/tldraw-document.api.spec.ts @@ -1,6 +1,6 @@ jest.mock('@y/redis'); -import { HttpStatus, INestApplication } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { StorageService } from '../../../../infra/storage/storage.service.js'; import { RedisService } from '../../../../infra/redis/redis.service.js'; @@ -38,163 +38,5 @@ describe('Tldraw-Document Api Test', () => { it('true to be true', () => { expect(true).toBe(true); }); - /* describe('when no api key is provided', () => { - it('should return 401', async () => { - const someId = '123'; - - const response = await testApiClient.get(someId); - - expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); - }); - }); */ - - /* describe('when id in params is not a mongo id', () => { - const setup = async () => { - const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - - await em.persistAndFlush([studentAccount, studentUser]); - em.clear(); - - const loggedInClient = await testApiClient.login(studentAccount); - - return { loggedInClient }; - }; - - it('should return 400', async () => { - const { loggedInClient } = await setup(); - - const response = await loggedInClient.get(`id/123`); - - expect(response.status).toEqual(HttpStatus.BAD_REQUEST); - expect(response.body).toEqual( - expect.objectContaining({ - validationErrors: [{ errors: ['schoolId must be a mongodb id'], field: ['schoolId'] }], - }), - ); - }); - }); - - describe('when requested school is not found', () => { - const setup = async () => { - const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - - await em.persistAndFlush([studentAccount, studentUser]); - em.clear(); - - const loggedInClient = await testApiClient.login(studentAccount); - - return { loggedInClient }; - }; - - it('should return 404', async () => { - const { loggedInClient } = await setup(); - const someId = new ObjectId().toHexString(); - - const response = await loggedInClient.get(`id/${someId}`); - - expect(response.status).toEqual(HttpStatus.NOT_FOUND); - }); - }); - - describe('when user is not in requested school', () => { - const setup = async () => { - const school = schoolEntityFactory.build(); - const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); - - await em.persistAndFlush([school, studentAccount, studentUser]); - em.clear(); - - const loggedInClient = await testApiClient.login(studentAccount); - - return { schoolId: school.id, loggedInClient }; - }; - - it('should return 403', async () => { - const { schoolId, loggedInClient } = await setup(); - - const response = await loggedInClient.get(`id/${schoolId}`); - - expect(response.status).toEqual(HttpStatus.FORBIDDEN); - }); - }); - - describe('when user is in requested school', () => { - const setup = async () => { - const schoolYears = schoolYearFactory.withStartYear(2002).buildList(3); - const currentYear = schoolYears[1]; - const federalState = federalStateFactory.build(); - const county = countyEmbeddableFactory.build(); - const systems = systemEntityFactory.buildList(3); - const school = schoolEntityFactory.build({ currentYear, federalState, systems, county }); - const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({ school }); - - await em.persistAndFlush([...schoolYears, federalState, school, studentAccount, studentUser]); - em.clear(); - - const schoolYearResponses = schoolYears.map((schoolYear) => { - return { - id: schoolYear.id, - name: schoolYear.name, - startDate: schoolYear.startDate.toISOString(), - endDate: schoolYear.endDate.toISOString(), - }; - }); - - const expectedResponse = { - id: school.id, - createdAt: school.createdAt.toISOString(), - updatedAt: school.updatedAt.toISOString(), - name: school.name, - federalState: { - id: federalState.id, - name: federalState.name, - abbreviation: federalState.abbreviation, - logoUrl: federalState.logoUrl, - counties: federalState.counties?.map((item) => { - return { - id: item._id.toHexString(), - name: item.name, - countyId: item.countyId, - antaresKey: item.antaresKey, - }; - }), - }, - county: { - id: county._id.toHexString(), - name: county.name, - countyId: county.countyId, - antaresKey: county.antaresKey, - }, - inUserMigration: undefined, - inMaintenance: false, - isExternal: false, - currentYear: schoolYearResponses[1], - years: { - schoolYears: schoolYearResponses, - activeYear: schoolYearResponses[1], - lastYear: schoolYearResponses[0], - nextYear: schoolYearResponses[2], - }, - features: [], - systemIds: systems.map((system) => system.id), - // TODO: The feature isTeamCreationByStudentsEnabled is set based on the config value STUDENT_TEAM_CREATION. - // We need to discuss how to go about the config in API tests! - instanceFeatures: ['isTeamCreationByStudentsEnabled'], - }; - - const loggedInClient = await testApiClient.login(studentAccount); - - return { schoolId: school.id, loggedInClient, expectedResponse }; - }; - - it('should return school', async () => { - const { schoolId, loggedInClient, expectedResponse } = await setup(); - - const response = await loggedInClient.get(`id/${schoolId}`); - - expect(response.status).toEqual(HttpStatus.OK); - expect(response.body).toEqual(expectedResponse); - }); - }); */ }); }); From 7c4323efcb9fed625b626bf4e030f7c2c6230354 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Mon, 16 Sep 2024 10:15:06 +0200 Subject: [PATCH 15/20] Add global api prefix --- src/apps/tldraw-server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apps/tldraw-server.ts b/src/apps/tldraw-server.ts index d7b8501..3619d02 100644 --- a/src/apps/tldraw-server.ts +++ b/src/apps/tldraw-server.ts @@ -6,6 +6,7 @@ import { ServerModule } from '../modules/server/server.module.js'; async function bootstrap() { const httpPort = 3349; const nestApp = await NestFactory.create(ServerModule); + nestApp.setGlobalPrefix('api'); nestApp.enableCors(); await nestApp.init(); From 2a61d5537117a2b92169e9643e36efab36884bdd Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Mon, 16 Sep 2024 15:24:35 +0200 Subject: [PATCH 16/20] Remove hardcoded bucketname --- src/infra/storage/storage.service.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/infra/storage/storage.service.ts b/src/infra/storage/storage.service.ts index 601aeef..61ce758 100644 --- a/src/infra/storage/storage.service.ts +++ b/src/infra/storage/storage.service.ts @@ -5,29 +5,30 @@ import { Logger } from '../logging/logger.js'; @Injectable() export class StorageService { private internalStorageInstance?: any; + private s3Endpoint: string; + private bucketName: string; constructor( private configService: ConfigService, private logger: Logger, ) { this.logger.setContext(StorageService.name); + this.s3Endpoint = this.configService.getOrThrow('S3_ENDPOINT'); + this.bucketName = this.configService.get('S3_BUCKET') || 'ydocs'; } public async get() { - const s3Endpoint = this.configService.get('S3_ENDPOINT'); - const bucketName = this.configService.get('S3_BUCKET') || 'ydocs'; - let store; - if (s3Endpoint) { + if (this.s3Endpoint) { this.logger.log('using s3 store'); // @ts-expect-error - @y/redis is only having jsdoc types const { createS3Storage } = await import('@y/redis/storage/s3'); - store = createS3Storage(bucketName); + store = createS3Storage(this.bucketName); try { // make sure the bucket exists - await store.client.makeBucket(bucketName); + await store.client.makeBucket(this.bucketName); } catch (e) {} } else { this.logger.log('ATTENTION! using in-memory store'); @@ -43,13 +44,13 @@ export class StorageService { const store = await this.getInternalStorageInstance(); const objectsList = []; - const stream = store.client.listObjectsV2('ydocs', parentId, true); + const stream = store.client.listObjectsV2(this.bucketName, parentId, true); for await (const obj of stream) { objectsList.push(obj.name); } - await store.client.removeObjects('ydocs', objectsList); + await store.client.removeObjects(this.bucketName, objectsList); } private async getInternalStorageInstance(): Promise { From 75c111d347dd7b3dc07ba52c169fea9592102a1c Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Wed, 18 Sep 2024 15:28:37 +0200 Subject: [PATCH 17/20] Publish delete message over redi pub --- src/infra/redis/redis.service.ts | 13 ++++++++-- src/modules/server/api/websocket.gateway.ts | 5 ++++ .../service/tldraw-document.service.spec.ts | 25 ------------------- .../server/service/tldraw-document.service.ts | 6 +++-- 4 files changed, 20 insertions(+), 29 deletions(-) diff --git a/src/infra/redis/redis.service.ts b/src/infra/redis/redis.service.ts index 2ff27e9..ceffef1 100644 --- a/src/infra/redis/redis.service.ts +++ b/src/infra/redis/redis.service.ts @@ -30,10 +30,19 @@ export class RedisService { return redisInstance; } - public async deleteDocument(docName: string): Promise { + public async addDeleteDocument(docName: string): Promise { const redisInstance = await this.getInternalRedisInstance(); - await redisInstance.del(docName); + await redisInstance.xadd('delete', '*', 'docName', docName); + await redisInstance.publish('delete', docName); + } + + public subscribeToDeleteChannel(callback: (message: string) => void): void { + const redisInstance = this.createNewRedisInstance(); + redisInstance.subscribe('delete'); + redisInstance.on('message', (chan, message) => { + callback(message); + }); } private async getInternalRedisInstance(): Promise { diff --git a/src/modules/server/api/websocket.gateway.ts b/src/modules/server/api/websocket.gateway.ts index 4299f4c..44205f9 100644 --- a/src/modules/server/api/websocket.gateway.ts +++ b/src/modules/server/api/websocket.gateway.ts @@ -50,6 +50,11 @@ export class WebsocketGateway implements OnModuleInit, OnModuleDestroy { this.logger.log(`Websocket Server is running on port ${wsPort}`); } }); + + this.redisService.subscribeToDeleteChannel(async (message: string) => { + console.log('Received message in delete channel:', message); + this.webSocketServer.publish(message, 'action:delete'); + }); } private incOpenConnectionsGauge() { diff --git a/src/modules/server/service/tldraw-document.service.spec.ts b/src/modules/server/service/tldraw-document.service.spec.ts index 1f63dc4..368111d 100644 --- a/src/modules/server/service/tldraw-document.service.spec.ts +++ b/src/modules/server/service/tldraw-document.service.spec.ts @@ -52,14 +52,6 @@ describe('Tldraw-Document Service', () => { expect(webSocketServer.publish).toHaveBeenCalledWith(docName, expectedMessage); }); - it('should call redisService deleteDocument', async () => { - const { parentId, docName, expectedMessage } = setup(); - - await service.deleteByDocName(parentId); - - expect(redisService.deleteDocument).toHaveBeenCalledWith(docName); - }); - it('should call storageService deleteDocument', async () => { const { parentId, docName, expectedMessage } = setup(); @@ -69,23 +61,6 @@ describe('Tldraw-Document Service', () => { }); }); - describe('when redis service throws error', () => { - const setup = () => { - const error = new Error('error'); - const parentId = '123'; - - redisService.deleteDocument.mockRejectedValueOnce(error); - - return { error, parentId }; - }; - - it('should return error', () => { - const { error, parentId } = setup(); - - expect(service.deleteByDocName(parentId)).rejects.toThrow(error); - }); - }); - describe('when storage service throws error', () => { const setup = () => { const error = new Error('error'); diff --git a/src/modules/server/service/tldraw-document.service.ts b/src/modules/server/service/tldraw-document.service.ts index b79f951..043ec09 100644 --- a/src/modules/server/service/tldraw-document.service.ts +++ b/src/modules/server/service/tldraw-document.service.ts @@ -1,7 +1,9 @@ import { Injectable, Inject } from '@nestjs/common'; import { RedisService } from '../../../infra/redis/index.js'; import { StorageService } from '../../../infra/storage/index.js'; -import { TemplatedApp } from 'uws'; +import { TemplatedApp, WebSocketBehavior } from 'uws'; +// @ts-expect-error - @y/redis is only having jsdoc types +import { Api } from '@y/redis'; const UWS = 'UWS'; @Injectable() @@ -17,7 +19,7 @@ export class TldrawDocumentService { this.webSocketServer.publish(docName, 'action:delete'); - await this.redisService.deleteDocument(docName); + await this.redisService.addDeleteDocument(docName); await this.storageService.deleteDocument(parentId); } From 1bb95e2a5aafcbe5013e5f85a272ed6202ccc561 Mon Sep 17 00:00:00 2001 From: Max Bischof Date: Wed, 18 Sep 2024 15:33:45 +0200 Subject: [PATCH 18/20] Add new y-redis form version to package json --- package-lock.json | 7 ++++--- package.json | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa8d78a..6dc4d9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@nestjs/core": "^10.4.1", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.4.1", - "@y/redis": "github:hpi-schul-cloud/y-redis#a5e141466a759a1d4b2876d5d5af52bc9ec4930d", + "@y/redis": "github:hpi-schul-cloud/y-redis#9896f03907f2a1c1dc26b785a2a10ea45ca493b1", "ioredis": "^5.4.1", "passport": "^0.7.0", "passport-headerapikey": "^1.2.2", @@ -2597,8 +2597,9 @@ }, "node_modules/@y/redis": { "version": "0.1.6", - "resolved": "git+ssh://git@github.com/hpi-schul-cloud/y-redis.git#a5e141466a759a1d4b2876d5d5af52bc9ec4930d", - "integrity": "sha512-wtrbVIf5ifCkzdX3EV5G5ZCwYf3Xl+0Td2gfvNh0qgGrIXxjRaqQFCTzniPRrYy0gzOR2s/OHe70dlNZSUbNYg==", + "resolved": "git+ssh://git@github.com/hpi-schul-cloud/y-redis.git#9896f03907f2a1c1dc26b785a2a10ea45ca493b1", + "integrity": "sha512-0bfJI1hPiJZu+/U/54rxrKdZucoQSs7QZ34nrw4WZbo91uJYq14OWKLhENfLl0QIkuildKU05lv6pyML9Ogbkg==", + "license": "AGPL-3.0 OR PROPRIETARY", "dependencies": { "ioredis": "^5.4.1", "lib0": "^0.2.95", diff --git a/package.json b/package.json index 34c8a8a..5ced5b7 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@nestjs/core": "^10.4.1", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.4.1", - "@y/redis": "github:hpi-schul-cloud/y-redis#a5e141466a759a1d4b2876d5d5af52bc9ec4930d", + "@y/redis": "github:hpi-schul-cloud/y-redis#9896f03907f2a1c1dc26b785a2a10ea45ca493b1", "ioredis": "^5.4.1", "passport": "^0.7.0", "passport-headerapikey": "^1.2.2", From 47e8f8c4e4027e87cc2329800a2f45004ea2f31e Mon Sep 17 00:00:00 2001 From: SevenWaysDP Date: Thu, 19 Sep 2024 14:32:48 +0200 Subject: [PATCH 19/20] BC-7851 - move logic to y-redis --- package-lock.json | 7 +++--- package.json | 2 +- src/infra/redis/redis.service.ts | 16 +++++++++---- src/infra/storage/storage.service.ts | 23 ------------------- .../service/tldraw-document.service.spec.ts | 16 +++---------- .../server/service/tldraw-document.service.ts | 14 ++++------- 6 files changed, 22 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index 138f52b..c88406a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@nestjs/core": "^10.4.1", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.4.1", - "@y/redis": "github:hpi-schul-cloud/y-redis#9896f03907f2a1c1dc26b785a2a10ea45ca493b1", + "@y/redis": "github:hpi-schul-cloud/y-redis#7d48e08d18ec78c9ab90063a7d867ec7f191319c", "ioredis": "^5.4.1", "passport": "^0.7.0", "passport-headerapikey": "^1.2.2", @@ -2695,9 +2695,8 @@ }, "node_modules/@y/redis": { "version": "0.1.6", - "resolved": "git+ssh://git@github.com/hpi-schul-cloud/y-redis.git#9896f03907f2a1c1dc26b785a2a10ea45ca493b1", - "integrity": "sha512-0bfJI1hPiJZu+/U/54rxrKdZucoQSs7QZ34nrw4WZbo91uJYq14OWKLhENfLl0QIkuildKU05lv6pyML9Ogbkg==", - "license": "AGPL-3.0 OR PROPRIETARY", + "resolved": "git+ssh://git@github.com/hpi-schul-cloud/y-redis.git#7d48e08d18ec78c9ab90063a7d867ec7f191319c", + "integrity": "sha512-usbDXO9o25+clgkXPwUGq6qnhhNa6lFJBxcTkEZy+9hQT5fOonnjfshnFdOW1huECKWPPWQmqB0gMB46aqdMpw==", "dependencies": { "ioredis": "^5.4.1", "lib0": "^0.2.95", diff --git a/package.json b/package.json index a7f61e9..28e525c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@nestjs/core": "^10.4.1", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.4.1", - "@y/redis": "github:hpi-schul-cloud/y-redis#9896f03907f2a1c1dc26b785a2a10ea45ca493b1", + "@y/redis": "github:hpi-schul-cloud/y-redis#7d48e08d18ec78c9ab90063a7d867ec7f191319c", "ioredis": "^5.4.1", "passport": "^0.7.0", "passport-headerapikey": "^1.2.2", diff --git a/src/infra/redis/redis.service.ts b/src/infra/redis/redis.service.ts index 239bedc..9524493 100644 --- a/src/infra/redis/redis.service.ts +++ b/src/infra/redis/redis.service.ts @@ -9,12 +9,18 @@ import { Logger } from '../logging/logger.js'; export class RedisService { private sentinelServiceName: string; private internalRedisInstance?: Redis; + private redisDeletionKey: string; + private redisDeletionActionKey: string; public constructor( private configService: ConfigService, private logger: Logger, ) { this.sentinelServiceName = this.configService.get('REDIS_SENTINEL_SERVICE_NAME') ?? ''; + const redisPrefix = this.configService.get('REDIS_PREFIX') ?? 'y'; + + this.redisDeletionKey = `${redisPrefix}:delete`; + this.redisDeletionActionKey = `${redisPrefix}:delete:action`; this.logger.setContext(RedisService.name); } @@ -33,14 +39,14 @@ export class RedisService { public async addDeleteDocument(docName: string): Promise { const redisInstance = await this.getInternalRedisInstance(); - await redisInstance.xadd('delete', '*', 'docName', docName); - await redisInstance.publish('delete', docName); + await redisInstance.xadd(this.redisDeletionKey, '*', 'docName', docName); + await redisInstance.publish(this.redisDeletionActionKey, docName); } public async subscribeToDeleteChannel(callback: (message: string) => void): Promise { - const redisInstance = await this.getInternalRedisInstance(); - redisInstance.subscribe('delete'); - redisInstance.on('message', (chan, message) => { + const redisSubscriberInstance = await this.createRedisInstance(); + redisSubscriberInstance.subscribe(this.redisDeletionActionKey); + redisSubscriberInstance.on('message', (chan, message) => { callback(message); }); } diff --git a/src/infra/storage/storage.service.ts b/src/infra/storage/storage.service.ts index 8cdbe97..ad9e639 100644 --- a/src/infra/storage/storage.service.ts +++ b/src/infra/storage/storage.service.ts @@ -4,7 +4,6 @@ import { Logger } from '../logging/logger.js'; @Injectable() export class StorageService { - private internalStorageInstance?: any; private s3Endpoint: string; private bucketName: string; @@ -39,26 +38,4 @@ export class StorageService { return store; } - - public async deleteDocument(parentId: string): Promise { - const store = await this.getInternalStorageInstance(); - - const objectsList = []; - const stream = store.client.listObjectsV2(this.bucketName, parentId, true); - - for await (const obj of stream) { - objectsList.push(obj.name); - } - - await store.client.removeObjects(this.bucketName, objectsList); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private async getInternalStorageInstance(): Promise { - if (!this.internalStorageInstance) { - this.internalStorageInstance = await this.get(); - } - - return this.internalStorageInstance; - } } diff --git a/src/modules/server/service/tldraw-document.service.spec.ts b/src/modules/server/service/tldraw-document.service.spec.ts index 368111d..a31f107 100644 --- a/src/modules/server/service/tldraw-document.service.spec.ts +++ b/src/modules/server/service/tldraw-document.service.spec.ts @@ -1,10 +1,10 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { TldrawDocumentService } from './tldraw-document.service.js'; +import { TemplatedApp } from 'uws'; import { RedisService } from '../../../infra/redis/index.js'; import { StorageService } from '../../../infra/storage/index.js'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { TemplatedApp } from 'uws'; +import { TldrawDocumentService } from './tldraw-document.service.js'; describe('Tldraw-Document Service', () => { let app: INestApplication; @@ -51,14 +51,6 @@ describe('Tldraw-Document Service', () => { expect(webSocketServer.publish).toHaveBeenCalledWith(docName, expectedMessage); }); - - it('should call storageService deleteDocument', async () => { - const { parentId, docName, expectedMessage } = setup(); - - await service.deleteByDocName(parentId); - - expect(storageService.deleteDocument).toHaveBeenCalledWith(parentId); - }); }); describe('when storage service throws error', () => { @@ -66,8 +58,6 @@ describe('Tldraw-Document Service', () => { const error = new Error('error'); const parentId = '123'; - storageService.deleteDocument.mockRejectedValueOnce(error); - return { error, parentId }; }; diff --git a/src/modules/server/service/tldraw-document.service.ts b/src/modules/server/service/tldraw-document.service.ts index 043ec09..ac52c2d 100644 --- a/src/modules/server/service/tldraw-document.service.ts +++ b/src/modules/server/service/tldraw-document.service.ts @@ -1,26 +1,20 @@ -import { Injectable, Inject } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { TemplatedApp } from 'uws'; import { RedisService } from '../../../infra/redis/index.js'; -import { StorageService } from '../../../infra/storage/index.js'; -import { TemplatedApp, WebSocketBehavior } from 'uws'; -// @ts-expect-error - @y/redis is only having jsdoc types -import { Api } from '@y/redis'; const UWS = 'UWS'; @Injectable() export class TldrawDocumentService { - constructor( - private readonly storageService: StorageService, + public constructor( @Inject(UWS) private webSocketServer: TemplatedApp, private readonly redisService: RedisService, ) {} - async deleteByDocName(parentId: string) { + public async deleteByDocName(parentId: string): Promise { const docName = `y:room:${parentId}:index`; this.webSocketServer.publish(docName, 'action:delete'); await this.redisService.addDeleteDocument(docName); - - await this.storageService.deleteDocument(parentId); } } From fad1da0c2f2843ff26e676001e9c682892c4b123 Mon Sep 17 00:00:00 2001 From: SevenWaysDP Date: Thu, 19 Sep 2024 16:12:08 +0200 Subject: [PATCH 20/20] fix linter issues --- .../strategy/x-api-key.strategy.spec.ts | 9 +++---- .../auth-guard/strategy/x-api-key.strategy.ts | 8 +++--- src/infra/redis/redis.service.spec.ts | 12 ++++----- .../api/dto/tldraw-document-delete.params.ts | 2 +- .../server/api/test/test-api-client.ts | 4 +-- .../server/api/tldraw-document.controller.ts | 8 +++--- src/modules/server/api/websocket.gateway.ts | 2 +- .../service/tldraw-document.service.spec.ts | 26 ++++--------------- 8 files changed, 27 insertions(+), 44 deletions(-) diff --git a/src/infra/auth-guard/strategy/x-api-key.strategy.spec.ts b/src/infra/auth-guard/strategy/x-api-key.strategy.spec.ts index 7bc0050..b666415 100644 --- a/src/infra/auth-guard/strategy/x-api-key.strategy.spec.ts +++ b/src/infra/auth-guard/strategy/x-api-key.strategy.spec.ts @@ -1,9 +1,9 @@ +import { createMock } from '@golevelup/ts-jest'; import { UnauthorizedException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; import { ConfigService } from '@nestjs/config'; -import { createMock } from '@golevelup/ts-jest'; -import { XApiKeyStrategy } from './x-api-key.strategy.js'; +import { Test, TestingModule } from '@nestjs/testing'; import { XApiKeyConfig } from '../config/x-api-key.config.js'; +import { XApiKeyStrategy } from './x-api-key.strategy.js'; describe('XApiKeyStrategy', () => { let module: TestingModule; @@ -37,8 +37,7 @@ describe('XApiKeyStrategy', () => { }); describe('validate', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const done = jest.fn((error: Error | null, data: boolean | null) => {}); + const done = jest.fn(() => null); describe('when a valid api key is provided', () => { const setup = () => { const CORRECT_API_KEY = '7ccd4e11-c6f6-48b0-81eb-cccf7922e7a4'; diff --git a/src/infra/auth-guard/strategy/x-api-key.strategy.ts b/src/infra/auth-guard/strategy/x-api-key.strategy.ts index 8df6b6b..7c6e2e1 100644 --- a/src/infra/auth-guard/strategy/x-api-key.strategy.ts +++ b/src/infra/auth-guard/strategy/x-api-key.strategy.ts @@ -1,23 +1,23 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-headerapikey/lib/Strategy.js'; import { XApiKeyConfig } from '../config/index.js'; import { StrategyType } from '../interface/index.js'; -import { Strategy } from 'passport-headerapikey/lib/Strategy.js'; @Injectable() export class XApiKeyStrategy extends PassportStrategy(Strategy, StrategyType.API_KEY) { private readonly allowedApiKeys: string[]; - constructor(private readonly configService: ConfigService) { + public constructor(private readonly configService: ConfigService) { super({ header: 'X-API-KEY' }, false); this.allowedApiKeys = this.configService.get('ADMIN_API__ALLOWED_API_KEYS'); } - public validate = (apiKey: string, done: (error: Error | null, data: boolean | null) => void) => { + public validate(apiKey: string, done: (error: Error | null, data: boolean | null) => void): void { if (this.allowedApiKeys.includes(apiKey)) { done(null, true); } done(new UnauthorizedException(), null); - }; + } } diff --git a/src/infra/redis/redis.service.spec.ts b/src/infra/redis/redis.service.spec.ts index 79636c1..e45e9a5 100644 --- a/src/infra/redis/redis.service.spec.ts +++ b/src/infra/redis/redis.service.spec.ts @@ -1,12 +1,9 @@ -import { ConfigService } from '@nestjs/config'; -import { RedisService } from './redis.service.js'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; import * as util from 'util'; -import * as ioredisModule from 'ioredis'; import { Logger } from '../logging/logger.js'; - -const Redis = ioredisModule.Redis; +import { RedisService } from './redis.service.js'; describe('Redis Service', () => { let service: RedisService; @@ -42,10 +39,13 @@ describe('Redis Service', () => { const sentinelServiceName = 'serviceName'; const sentinelName = 'sentinelName'; const sentinelPassword = 'sentinelPassword'; + const redisPrefix = 'y'; configService.get.mockReturnValueOnce(sentinelServiceName); + configService.get.mockReturnValueOnce(redisPrefix); configService.get.mockReturnValueOnce(sentinelName); configService.get.mockReturnValueOnce(sentinelPassword); + configService.get.mockReturnValueOnce(sentinelPassword); const name1 = 'name1'; const name2 = 'name2'; @@ -64,7 +64,7 @@ describe('Redis Service', () => { return { resolveSrv, sentinelServiceName }; }; - it('calls resolveSrv', async () => { + it.only('calls resolveSrv', async () => { const { resolveSrv, sentinelServiceName } = setup(); await service.createRedisInstance(); diff --git a/src/modules/server/api/dto/tldraw-document-delete.params.ts b/src/modules/server/api/dto/tldraw-document-delete.params.ts index 2d392c9..001c7da 100644 --- a/src/modules/server/api/dto/tldraw-document-delete.params.ts +++ b/src/modules/server/api/dto/tldraw-document-delete.params.ts @@ -1,3 +1,3 @@ export class TldrawDocumentDeleteParams { - parentId!: string; + public parentId!: string; } diff --git a/src/modules/server/api/test/test-api-client.ts b/src/modules/server/api/test/test-api-client.ts index 9024421..b72aa5e 100644 --- a/src/modules/server/api/test/test-api-client.ts +++ b/src/modules/server/api/test/test-api-client.ts @@ -25,10 +25,10 @@ export class TestApiClient { private readonly kindOfAuth: string; - constructor(app: INestApplication, baseRoute: string, authValue?: string, useAsApiKey = false) { + public constructor(app: INestApplication, baseRoute: string, authValue?: string, useAsApiKey = false) { this.app = app; this.baseRoute = this.checkAndAddPrefix(baseRoute); - this.authHeader = useAsApiKey ? `${authValue || ''}` : `${testReqestConst.prefix} ${authValue || ''}`; + this.authHeader = useAsApiKey ? `${authValue ?? ''}` : `${testReqestConst.prefix} ${authValue ?? ''}`; this.kindOfAuth = useAsApiKey ? 'X-API-KEY' : 'authorization'; } diff --git a/src/modules/server/api/tldraw-document.controller.ts b/src/modules/server/api/tldraw-document.controller.ts index e56c5be..5c31e0a 100644 --- a/src/modules/server/api/tldraw-document.controller.ts +++ b/src/modules/server/api/tldraw-document.controller.ts @@ -1,16 +1,16 @@ import { Controller, Delete, HttpCode, Param, UseGuards } from '@nestjs/common'; -import { TldrawDocumentDeleteParams } from './dto/index.js'; import { ApiKeyGuard } from '../../../infra/auth-guard/guard/index.js'; import { TldrawDocumentService } from '../service/tldraw-document.service.js'; +import { TldrawDocumentDeleteParams } from './dto/index.js'; @UseGuards(ApiKeyGuard) @Controller('tldraw-document') export class TldrawDocumentController { - constructor(private readonly tldrawDocumentService: TldrawDocumentService) {} + public constructor(private readonly tldrawDocumentService: TldrawDocumentService) {} @HttpCode(204) @Delete(':parentId') - async deleteByDocName(@Param() urlParams: TldrawDocumentDeleteParams) { - this.tldrawDocumentService.deleteByDocName(urlParams.parentId); + public async deleteByDocName(@Param() urlParams: TldrawDocumentDeleteParams): Promise { + await this.tldrawDocumentService.deleteByDocName(urlParams.parentId); } } diff --git a/src/modules/server/api/websocket.gateway.ts b/src/modules/server/api/websocket.gateway.ts index 181055b..10dfabd 100644 --- a/src/modules/server/api/websocket.gateway.ts +++ b/src/modules/server/api/websocket.gateway.ts @@ -51,7 +51,7 @@ export class WebsocketGateway implements OnModuleInit, OnModuleDestroy { } }); - this.redisService.subscribeToDeleteChannel(async (message: string) => { + this.redisService.subscribeToDeleteChannel((message: string) => { console.log('Received message in delete channel:', message); this.webSocketServer.publish(message, 'action:delete'); }); diff --git a/src/modules/server/service/tldraw-document.service.spec.ts b/src/modules/server/service/tldraw-document.service.spec.ts index a31f107..41ef4fe 100644 --- a/src/modules/server/service/tldraw-document.service.spec.ts +++ b/src/modules/server/service/tldraw-document.service.spec.ts @@ -1,17 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { TemplatedApp } from 'uws'; import { RedisService } from '../../../infra/redis/index.js'; -import { StorageService } from '../../../infra/storage/index.js'; import { TldrawDocumentService } from './tldraw-document.service.js'; describe('Tldraw-Document Service', () => { - let app: INestApplication; let service: TldrawDocumentService; let webSocketServer: TemplatedApp; let redisService: DeepMocked; - let storageService: DeepMocked; beforeAll(async () => { const moduleFixture = await Test.createTestingModule({ @@ -21,10 +17,6 @@ describe('Tldraw-Document Service', () => { provide: RedisService, useValue: createMock(), }, - { - provide: StorageService, - useValue: createMock(), - }, { provide: 'UWS', useValue: createMock() }, ], }).compile(); @@ -32,14 +24,13 @@ describe('Tldraw-Document Service', () => { service = moduleFixture.get(TldrawDocumentService); webSocketServer = moduleFixture.get('UWS'); redisService = moduleFixture.get(RedisService); - storageService = moduleFixture.get(StorageService); }); describe('when redis and storage service returns successfully', () => { const setup = () => { const parentId = '123'; const docName = `y:room:${parentId}:index`; - const expectedMessage = 'deleted'; + const expectedMessage = 'action:delete'; return { parentId, docName, expectedMessage }; }; @@ -51,20 +42,13 @@ describe('Tldraw-Document Service', () => { expect(webSocketServer.publish).toHaveBeenCalledWith(docName, expectedMessage); }); - }); - - describe('when storage service throws error', () => { - const setup = () => { - const error = new Error('error'); - const parentId = '123'; - return { error, parentId }; - }; + it('should call addDeleteDocument', async () => { + const { parentId, docName } = setup(); - it('should return error', () => { - const { error, parentId } = setup(); + await service.deleteByDocName(parentId); - expect(service.deleteByDocName(parentId)).rejects.toThrow(error); + expect(redisService.addDeleteDocument).toHaveBeenCalledWith(docName); }); }); });