diff --git a/.env.test b/.env.test index e66d043b..0fa52b85 100644 --- a/.env.test +++ b/.env.test @@ -1,4 +1,3 @@ -REDIS=redis://localhost:6379 API_HOST=http://localhost:3030 ADMIN_API__ALLOWED_API_KEYS=randomString diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d437ee04..f6605b7a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,11 +18,6 @@ jobs: strategy: matrix: shard: [1] - services: - rabbitmq: - image: rabbitmq:3 - ports: - - 5672:5672 steps: - name: checkout uses: actions/checkout@v4 @@ -32,6 +27,13 @@ jobs: node-version: ${{ env.NODE_VERSION }} - name: npm ci run: npm ci --prefer-offline --no-audit + - name: Start UP MinIO + uses: infleet/minio-action@v0.0.1 + with: + port: "9000" + version: "latest" + username: "miniouser" + password: "miniouser" - name: test:cov - test all with coverage timeout-minutes: 15 run: export RUN_WITHOUT_JEST_COVERAGE='true' && export NODE_OPTIONS='--max_old_space_size=4096' && ./node_modules/.bin/jest --shard=${{ matrix.shard }}/${{ strategy.job-total }} --coverage --force-exit diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/Dockerfile b/Dockerfile index 6428045e..7933c8df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM docker.io/node:22-alpine -RUN apk add gcompat +RUN apk add --no-cache gcompat build-base ENV TZ=Europe/Berlin RUN mkdir /app && chown -R node:node /app diff --git a/jest.config.cjs b/jest.config.cjs index 7ee9428a..99dbccdc 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -18,4 +18,6 @@ module.exports = { collectCoverageFrom: ['**/*.(t|j)s'], coverageDirectory: '../coverage', testEnvironment: 'node', + globalSetup: '/../scripts/testing/globalSetup.ts', + globalTeardown: '/../scripts/testing/globalTeardown.ts', }; diff --git a/package-lock.json b/package-lock.json index a3042347..bb3e8c57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@types/passport": "^1.0.17", "@types/passport-strategy": "^0.2.38", "@types/supertest": "^6.0.2", + "@types/ws": "^8.5.13", "@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/parser": "^8.13.0", "arg": "^5.0.2", @@ -56,13 +57,16 @@ "globals": "^15.12.0", "jest": "^29.7.0", "prettier": "^3.3.3", + "redis-memory-server": "^0.11.0", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.2.5", "ts-loader": "^9.5.1", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.6.3" + "typescript": "^5.6.3", + "ws": "^8.18.0", + "y-websocket": "^2.0.4" }, "engines": { "node": ">=22.0.0", @@ -2583,6 +2587,16 @@ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==" }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -2598,6 +2612,17 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.13.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.13.0.tgz", @@ -2944,6 +2969,24 @@ "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", "optional": true }, + "node_modules/abstract-leveldown": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "immediate": "^3.2.3", + "level-concat-iterator": "~2.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3169,6 +3212,14 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3695,6 +3746,16 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -3950,6 +4011,13 @@ "node": ">= 6" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, "node_modules/compare-versions": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.4.tgz", @@ -4341,6 +4409,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deferred-leveldown": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", + "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.1", + "inherits": "^2.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -4535,6 +4618,33 @@ "node": ">= 0.8" } }, + "node_modules/encoding-down": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", + "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "abstract-leveldown": "^6.2.1", + "inherits": "^2.0.3", + "level-codec": "^9.0.0", + "level-errors": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -4548,6 +4658,20 @@ "node": ">=10.13.0" } }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5095,6 +5219,43 @@ "node": ">=4" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5179,6 +5340,16 @@ "bser": "2.1.1" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -5291,6 +5462,57 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/find-package-json": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/find-package-json/-/find-package-json-1.2.0.tgz", + "integrity": "sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==", + "dev": true, + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5494,6 +5716,39 @@ "node": ">=12" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/fs-monkey": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", @@ -5559,6 +5814,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -5856,6 +6124,14 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -7102,6 +7378,175 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/level": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/level/-/level-6.0.1.tgz", + "integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "level-js": "^5.0.0", + "level-packager": "^5.1.0", + "leveldown": "^5.4.0" + }, + "engines": { + "node": ">=8.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/level" + } + }, + "node_modules/level-codec": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", + "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-concat-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", + "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "errno": "~0.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-iterator-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", + "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.4.0", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-iterator-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/level-js": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/level-js/-/level-js-5.0.2.tgz", + "integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.3", + "buffer": "^5.5.0", + "inherits": "^2.0.3", + "ltgt": "^2.1.2" + } + }, + "node_modules/level-packager": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz", + "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "encoding-down": "^6.3.0", + "levelup": "^4.3.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/level-supports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "xtend": "^4.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leveldown": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz", + "integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "abstract-leveldown": "~6.2.1", + "napi-macros": "~2.0.0", + "node-gyp-build": "~4.1.0" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/levelup": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", + "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "deferred-leveldown": "~5.3.0", + "level-errors": "~2.0.0", + "level-iterator-stream": "~4.0.0", + "level-supports": "~1.0.0", + "xtend": "~4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -7179,16 +7624,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lockfile": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", + "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", + "dev": true, + "license": "ISC", + "dependencies": { + "signal-exit": "^3.0.2" + } + }, + "node_modules/lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, + "node_modules/lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -7262,6 +7738,14 @@ "yallist": "^3.0.2" } }, + "node_modules/ltgt": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", + "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/magic-string": { "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", @@ -7483,6 +7967,40 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -7522,6 +8040,14 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "node_modules/napi-macros": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", + "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -7597,6 +8123,19 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz", + "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7978,6 +8517,13 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8224,6 +8770,25 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8388,6 +8953,60 @@ "node": ">=4" } }, + "node_modules/redis-memory-server": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/redis-memory-server/-/redis-memory-server-0.11.0.tgz", + "integrity": "sha512-yDE4/cGPUE/xpgZkv+krBMS5jQ0vpPc+P5BOSDlJfevxNEBx7+8aQw+bNvIlVXzDcg7M+Z+eh2wR2ZYL/cebdQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.3.0", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "extract-zip": "^2.0.1", + "find-cache-dir": "^3.3.2", + "find-package-json": "^1.2.0", + "get-port": "^5.1.1", + "https-proxy-agent": "^7.0.0", + "lockfile": "^1.0.4", + "lodash.defaultsdeep": "^4.6.1", + "rimraf": "^5.0.1", + "semver": "^7.5.3", + "tar": "^6.1.15", + "tmp": "^0.2.1", + "uuid": "^8.3.2" + }, + "bin": { + "redis-memory-server": "bin/index.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/redis-memory-server/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/redis-memory-server/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/redis-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", @@ -8523,6 +9142,22 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -9217,6 +9852,54 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/tdigest": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", @@ -9798,6 +10481,16 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/uWebSockets.js": { "version": "20.49.0", "resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#442087c0a01bf146acb7386910739ec81df06700", @@ -10149,6 +10842,28 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -10177,6 +10892,25 @@ "node": ">=0.4" } }, + "node_modules/y-leveldb": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/y-leveldb/-/y-leveldb-0.1.2.tgz", + "integrity": "sha512-6ulEn5AXfXJYi89rXPEg2mMHAyyw8+ZfeMMdOtBbV8FJpQ1NOrcgi6DTAcXof0dap84NjHPT2+9d0rb6cFsjEg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "level": "^6.0.1", + "lib0": "^0.2.31" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, "node_modules/y-protocols": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", @@ -10196,6 +10930,48 @@ "yjs": "^13.0.0" } }, + "node_modules/y-websocket": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/y-websocket/-/y-websocket-2.0.4.tgz", + "integrity": "sha512-UbrkOU4GPNFFTDlJYAxAmzZhia8EPxHkngZ6qjrxgIYCN3gI2l+zzLzA9p4LQJ0IswzpioeIgmzekWe7HoBBjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lib0": "^0.2.52", + "lodash.debounce": "^4.0.8", + "y-protocols": "^1.0.5" + }, + "bin": { + "y-websocket": "bin/server.cjs", + "y-websocket-server": "bin/server.cjs" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "optionalDependencies": { + "ws": "^6.2.1", + "y-leveldb": "^0.1.0" + }, + "peerDependencies": { + "yjs": "^13.5.6" + } + }, + "node_modules/y-websocket/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -10238,6 +11014,27 @@ "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yauzl/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/yjs": { "version": "13.6.20", "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.20.tgz", diff --git a/package.json b/package.json index 8f8ac620..6e40fd06 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@types/passport": "^1.0.17", "@types/passport-strategy": "^0.2.38", "@types/supertest": "^6.0.2", + "@types/ws": "^8.5.13", "@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/parser": "^8.13.0", "arg": "^5.0.2", @@ -76,12 +77,15 @@ "globals": "^15.12.0", "jest": "^29.7.0", "prettier": "^3.3.3", + "redis-memory-server": "^0.11.0", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.2.5", "ts-loader": "^9.5.1", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.6.3" + "typescript": "^5.6.3", + "ws": "^8.18.0", + "y-websocket": "^2.0.4" } } diff --git a/scripts/testing/globalSetup.ts b/scripts/testing/globalSetup.ts new file mode 100644 index 00000000..c72cf24b --- /dev/null +++ b/scripts/testing/globalSetup.ts @@ -0,0 +1,11 @@ +import { RedisMemoryServer } from 'redis-memory-server'; + +export default async function globalSetup(): Promise { + const redisMemoryServer = new RedisMemoryServer(); + + const host = await redisMemoryServer.getHost(); + const port = await redisMemoryServer.getPort(); + process.env.REDIS = `redis://${host}:${port}`; + // @ts-ignore + global.__REDISINSTANCE = redisMemoryServer; +} diff --git a/scripts/testing/globalTeardown.ts b/scripts/testing/globalTeardown.ts new file mode 100644 index 00000000..f0235011 --- /dev/null +++ b/scripts/testing/globalTeardown.ts @@ -0,0 +1,7 @@ +import { RedisMemoryServer } from 'redis-memory-server'; + +export default async function globalTeardown(): Promise { + // @ts-ignore + const instance: RedisMemoryServer = global.__REDISINSTANCE; + await instance.stop(); +} diff --git a/sonar-project.properties b/sonar-project.properties index 2c231dcd..395cde6d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,8 +3,8 @@ sonar.projectKey=hpi-schul-cloud_tldraw-server sonar.sources=. sonar.tests=. sonar.test.inclusions=**/*.spec.ts -sonar.exclusions=**/*.app.ts,**/authorization-api-client/**/*.ts -sonar.coverage.exclusions=**/*.factory.ts,**/authorization-api-client/**/*.ts,eslint.config.mjs +sonar.exclusions=**/*.app.ts,**/authorization-api-client/**/*.ts,scripts/**/* +sonar.coverage.exclusions=**/*.factory.ts,**/authorization-api-client/**/*.ts,scripts/**/*,eslint.config.mjs sonar.cpd.exclusions=**/controller/dto/*.ts sonar.javascript.lcov.reportPaths=merged-lcov.info sonar.typescript.tsconfigPaths=tsconfig.json diff --git a/src/infra/metrics/api/metrics.api.spec.ts b/src/infra/metrics/api/metrics.api.spec.ts new file mode 100644 index 00000000..f0344441 --- /dev/null +++ b/src/infra/metrics/api/metrics.api.spec.ts @@ -0,0 +1,35 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { TestApiClient } from '../../testing/test-api-client.js'; +import { MetricsModule } from '../metrics.module.js'; + +describe('Tldraw-Document Api Test', () => { + let app: INestApplication; + const baseRoute = 'metrics'; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [MetricsModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('getMetrics', () => { + const setup = () => { + const testApiClient = new TestApiClient(app, baseRoute); + + return { testApiClient }; + }; + it('returns ok 200', async () => { + const { testApiClient } = setup(); + + await testApiClient.get().expect(200); + }); + }); +}); diff --git a/src/infra/redis/redis.config.ts b/src/infra/redis/redis.config.ts index 40c7fd05..f42995a7 100644 --- a/src/infra/redis/redis.config.ts +++ b/src/infra/redis/redis.config.ts @@ -8,11 +8,11 @@ export class RedisConfig { public REDIS_CLUSTER_ENABLED!: boolean; @IsUrl({ protocols: ['redis'], require_tld: false }) - @ValidateIf((o: RedisConfig) => o.REDIS_CLUSTER_ENABLED === false) + @ValidateIf((o: RedisConfig) => !o.REDIS_CLUSTER_ENABLED) public REDIS!: string; @IsString() - @ValidateIf((o: RedisConfig) => o.REDIS_CLUSTER_ENABLED === true) + @ValidateIf((o: RedisConfig) => o.REDIS_CLUSTER_ENABLED) public REDIS_SENTINEL_SERVICE_NAME!: string; @IsString() @@ -22,6 +22,6 @@ export class RedisConfig { public REDIS_SENTINEL_NAME = 'mymaster'; @IsString() - @ValidateIf((o: RedisConfig) => o.REDIS_CLUSTER_ENABLED === true) + @ValidateIf((o: RedisConfig) => o.REDIS_CLUSTER_ENABLED) public REDIS_SENTINEL_PASSWORD!: string; } diff --git a/src/infra/testing/test-api-client.spec.ts b/src/infra/testing/test-api-client.spec.ts new file mode 100644 index 00000000..0b39a72a --- /dev/null +++ b/src/infra/testing/test-api-client.spec.ts @@ -0,0 +1,149 @@ +import { Controller, Delete, Get, Headers, HttpStatus, INestApplication, Patch, Post, Put } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { XApiKeyConfig } from '../auth-guard/x-api-key.config.js'; +import { TestApiClient } from './test-api-client.js'; + +@Controller('') +class TestXApiKeyController { + @Delete(':id') + public delete(@Headers('X-API-KEY') authorization: string) { + return Promise.resolve({ method: 'delete', authorization }); + } + + @Post() + public post(@Headers('X-API-KEY') authorization: string) { + return Promise.resolve({ method: 'post', authorization }); + } + + @Get(':id') + public get(@Headers('X-API-KEY') authorization: string) { + return Promise.resolve({ method: 'get', authorization }); + } + + @Put() + public put(@Headers('X-API-KEY') authorization: string) { + return Promise.resolve({ method: 'put', authorization }); + } + + @Patch(':id') + public patch(@Headers('X-API-KEY') authorization: string) { + return Promise.resolve({ method: 'patch', authorization }); + } +} + +describe(TestApiClient.name, () => { + let app: INestApplication; + const baseRoute = ''; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + controllers: [TestXApiKeyController], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('when apiKey is defined', () => { + const setup = () => { + const id = '60f1b9b3b3b3b3b3b3b3b3b3'; + const useAsApiKey = true; + const validApiKey: XApiKeyConfig['ADMIN_API__ALLOWED_API_KEYS'][0] = 'randomString'; + const testApiClient = new TestApiClient(app, baseRoute, validApiKey, useAsApiKey); + + return { testApiClient, id }; + }; + + describe('get', () => { + it('should resolve requests', async () => { + const { testApiClient, id } = setup(); + + const result = await testApiClient.get(id); + + expect(result.statusCode).toEqual(HttpStatus.OK); + expect(result.body).toEqual(expect.objectContaining({ method: 'get' })); + }); + }); + + describe('delete', () => { + it('should resolve requests', async () => { + const { testApiClient, id } = setup(); + + const result = await testApiClient.delete(id); + + expect(result.statusCode).toEqual(HttpStatus.OK); + expect(result.body).toEqual(expect.objectContaining({ method: 'delete' })); + }); + }); + + describe('put', () => { + it('should resolve requests', async () => { + const { testApiClient } = setup(); + + const result = await testApiClient.put(); + + expect(result.statusCode).toEqual(HttpStatus.OK); + expect(result.body).toEqual(expect.objectContaining({ method: 'put' })); + }); + }); + + describe('patch', () => { + it('should resolve requests', async () => { + const { testApiClient, id } = setup(); + + const result = await testApiClient.patch(id); + + expect(result.statusCode).toEqual(HttpStatus.OK); + expect(result.body).toEqual(expect.objectContaining({ method: 'patch' })); + }); + }); + + describe('post', () => { + it('should resolve requests', async () => { + const { testApiClient } = setup(); + + const result = await testApiClient.post(); + + expect(result.statusCode).toEqual(HttpStatus.CREATED); + expect(result.body).toEqual(expect.objectContaining({ method: 'post' })); + }); + }); + + describe('postWithAttachment', () => { + it('should resolve requests', async () => { + const { testApiClient } = setup(); + + const result = await testApiClient.postWithAttachment('', 'file', Buffer.from('some test data'), 'file.txt'); + + expect(result.statusCode).toEqual(HttpStatus.CREATED); + expect(result.body).toEqual(expect.objectContaining({ method: 'post' })); + }); + }); + }); + + describe('when apiKey is undefined', () => { + const setup = () => { + const id = '60f1b9b3b3b3b3b3b3b3b3b3'; + const useAsApiKey = true; + const invalidApiKey = undefined; + const testApiClient = new TestApiClient(app, baseRoute, invalidApiKey, useAsApiKey); + + return { testApiClient, id }; + }; + + describe('get', () => { + it('should resolve requests', async () => { + const { testApiClient, id } = setup(); + + const result = await testApiClient.get(id); + + expect(result.statusCode).toEqual(HttpStatus.OK); + expect(result.body).toEqual(expect.objectContaining({ authorization: '', method: 'get' })); + }); + }); + }); +}); diff --git a/src/modules/server/api/test/test-api-client.ts b/src/infra/testing/test-api-client.ts similarity index 100% rename from src/modules/server/api/test/test-api-client.ts rename to src/infra/testing/test-api-client.ts diff --git a/src/infra/y-redis/ws.service.spec.ts b/src/infra/y-redis/ws.service.spec.ts index 29ed043e..a5a2cb2d 100644 --- a/src/infra/y-redis/ws.service.spec.ts +++ b/src/infra/y-redis/ws.service.spec.ts @@ -116,34 +116,53 @@ describe('ws service', () => { }); describe('when checkAuth resolves ', () => { - it('should upgrade the connection', async () => { - const res = createMock(); - const req = createMock(); - const context = createMock(); - const checkAuth = jest.fn().mockResolvedValue({ hasWriteAccess: true, room: 'room', userid: 'userid' }); + describe('when connection is not aborted', () => { + it('should upgrade the connection', async () => { + const res = createMock(); + const req = createMock(); + const context = createMock(); + const checkAuth = jest.fn().mockResolvedValue({ hasWriteAccess: true, room: 'room', userid: 'userid' }); + + await upgradeCallback(res, req, context, checkAuth); + + expect(res.cork).toHaveBeenCalledTimes(1); + expect(res.cork).toHaveBeenCalledWith(expect.any(Function)); + res.cork.mock.calls[0][0](); + expect(res.upgrade).toHaveBeenCalledWith( + expect.objectContaining({ + awarenessId: null, + awarenessLastClock: 0, + error: null, + hasWriteAccess: true, + id: 0, + initialRedisSubId: '0', + isClosed: false, + room: 'room', + userid: 'userid', + }), + req.getHeader('sec-websocket-key'), + req.getHeader('sec-websocket-protocol'), + req.getHeader('sec-websocket-extensions'), + context, + ); + }); + }); - await upgradeCallback(res, req, context, checkAuth); + describe('when connection is aborted', () => { + it('should not upgrade the connection', async () => { + const res = createMock(); + const req = createMock(); + const context = createMock(); + const checkAuth = jest.fn().mockImplementationOnce(async () => { + res.onAborted.mock.calls[0][0](); - expect(res.cork).toHaveBeenCalledTimes(1); - expect(res.cork).toHaveBeenCalledWith(expect.any(Function)); - res.cork.mock.calls[0][0](); - expect(res.upgrade).toHaveBeenCalledWith( - expect.objectContaining({ - awarenessId: null, - awarenessLastClock: 0, - error: null, - hasWriteAccess: true, - id: 0, - initialRedisSubId: '0', - isClosed: false, - room: 'room', - userid: 'userid', - }), - req.getHeader('sec-websocket-key'), - req.getHeader('sec-websocket-protocol'), - req.getHeader('sec-websocket-extensions'), - context, - ); + return await Promise.resolve({ hasWriteAccess: true, room: 'room', userid: 'userid' }); + }); + + await upgradeCallback(res, req, context, checkAuth); + + expect(res.cork).not.toHaveBeenCalled(); + }); }); }); }); @@ -1009,5 +1028,48 @@ describe('ws service', () => { }); }); }); + + describe('when user has no room', () => { + const setup = () => { + const { ws, client, app, subscriber } = buildParams(); + app.numSubscribers.mockReturnValue(0); + + const user = createMock({ + room: null, + awarenessId: 22, + awarenessLastClock: 1, + subs: new Set(['topic1', 'topic2']), + }); + ws.getUserData.mockReturnValueOnce(user); + const code = 0; + const message = buildUpdate({ + messageType: protocol.messageAwareness, + length: 0, + numberOfUpdates: 1, + awarenessId: 75, + lastClock: 76, + }); + const redisMessageSubscriber = jest.fn(); + const closeWsCallback = jest.fn(); + + return { ws, client, app, code, subscriber, message, redisMessageSubscriber, user, closeWsCallback }; + }; + + it('should not call addMessage', () => { + const { app, ws, client, subscriber, code, message, redisMessageSubscriber } = setup(); + + closeCallback(app, ws, client, subscriber, code, message, redisMessageSubscriber); + + expect(client.addMessage).not.toHaveBeenCalled(); + }); + + it('should not call closeWsCallback', () => { + const { app, ws, client, subscriber, code, message, redisMessageSubscriber, closeWsCallback } = setup(); + + closeCallback(app, ws, client, subscriber, code, message, redisMessageSubscriber, closeWsCallback); + + expect(closeWsCallback).not.toHaveBeenCalled(); + }); + }); }); }); 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 001c7da7..8df421ba 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,8 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsMongoId } from 'class-validator'; + export class TldrawDocumentDeleteParams { + @IsMongoId() + @ApiProperty() public parentId!: string; } diff --git a/src/modules/server/api/test/tldraw-config.api.spec.ts b/src/modules/server/api/test/tldraw-config.api.spec.ts index 9454844b..3d93769b 100644 --- a/src/modules/server/api/test/tldraw-config.api.spec.ts +++ b/src/modules/server/api/test/tldraw-config.api.spec.ts @@ -1,12 +1,7 @@ -import { createMock } from '@golevelup/ts-jest'; import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { App } from 'uWebSockets.js'; -import { RedisService } from '../../../../infra/redis/redis.service.js'; -import { StorageService } from '../../../../infra/storage/storage.service.js'; +import { TestApiClient } from '../../../../infra/testing/test-api-client.js'; import { ServerModule } from '../../server.module.js'; -import { WebsocketGateway } from '../websocket.gateway.js'; -import { TestApiClient } from './test-api-client.js'; describe('Tldraw-Config Api Test', () => { let app: INestApplication; @@ -15,16 +10,7 @@ describe('Tldraw-Config Api Test', () => { 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(); + }).compile(); app = moduleFixture.createNestApplication(); await app.init(); 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 199a68a9..50ad91ac 100644 --- a/src/modules/server/api/test/tldraw-document.api.spec.ts +++ b/src/modules/server/api/test/tldraw-document.api.spec.ts @@ -1,28 +1,17 @@ -import { createMock } from '@golevelup/ts-jest'; import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { App } from 'uWebSockets.js'; -import { RedisService } from '../../../../infra/redis/redis.service.js'; -import { StorageService } from '../../../../infra/storage/storage.service.js'; +import { TestApiClient } from '../../../../infra/testing/test-api-client.js'; import { ServerModule } from '../../server.module.js'; -import { WebsocketGateway } from '../websocket.gateway.js'; describe('Tldraw-Document Api Test', () => { let app: INestApplication; + const baseRoute = 'tldraw-document'; + const xApiKey = 'randomString'; 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(); + }).compile(); app = moduleFixture.createNestApplication(); await app.init(); @@ -33,8 +22,47 @@ describe('Tldraw-Document Api Test', () => { }); describe('deleteByDocName', () => { - it('true to be true', () => { - expect(true).toBe(true); + describe('when apiKey is not valid', () => { + const setup = () => { + const parentId = '60f1b9b3b3b3b3b3b3b3b3b3'; + const useAsApiKey = true; + const invalidApiKey = 'invalid'; + const testApiClient = new TestApiClient(app, baseRoute, invalidApiKey, useAsApiKey); + + return { testApiClient, parentId }; + }; + + it('returns unauthorized ', async () => { + const { testApiClient, parentId } = setup(); + + await testApiClient.delete(parentId).expect(401); + }); + }); + + describe('when apiKey is valid', () => { + const setup = () => { + const useAsApiKey = true; + const testApiClient = new TestApiClient(app, baseRoute, xApiKey, useAsApiKey); + + return { testApiClient }; + }; + + describe('when parentId is not a mongoId', () => { + it('returns bad request 400', async () => { + const { testApiClient } = setup(); + + await testApiClient.delete('/asas').expect(400); + }); + }); + + describe('when parentId is a mongoId', () => { + it('returns no content 204', async () => { + const { testApiClient } = setup(); + const parentId = '60f1b9b3b3b3b3b3b3b3b3b3'; + + await testApiClient.delete(parentId).expect(204); + }); + }); }); }); }); diff --git a/src/modules/server/api/test/websocket.api.spec.ts b/src/modules/server/api/test/websocket.api.spec.ts new file mode 100644 index 00000000..a809b8f8 --- /dev/null +++ b/src/modules/server/api/test/websocket.api.spec.ts @@ -0,0 +1,229 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import * as array from 'lib0/array'; +import * as promise from 'lib0/promise'; +import { WebSocket } from 'ws'; +import { WebsocketProvider } from 'y-websocket'; +import { Doc, encodeStateAsUpdateV2 } from 'yjs'; +import { ResponsePayloadBuilder } from '../../../../infra//authorization/response.builder.js'; +import { AuthorizationService } from '../../../../infra/authorization/authorization.service.js'; +import { ServerModule } from '../../server.module.js'; + +describe('Websocket Api Test', () => { + let app: INestApplication; + let authorizationService: DeepMocked; + const prefix = 'y'; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [ServerModule], + }) + .overrideProvider(AuthorizationService) + .useValue(createMock()) + .compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + + authorizationService = await app.resolve(AuthorizationService); + }); + + afterAll(async () => { + await app.close(); + }); + + const createWsClient = (room: string) => { + const ydoc = new Doc(); + const serverUrl = 'ws://localhost:3345'; + const provider = new WebsocketProvider(serverUrl, prefix + '-' + room, ydoc, { + // @ts-ignore + WebSocketPolyfill: WebSocket, + connect: true, + disableBc: true, + }); + + return { ydoc, provider }; + }; + + const waitUntilDocsEqual = (ydoc1: Doc, ydoc2: Doc): Promise => + promise.until(0, () => { + const e1 = encodeStateAsUpdateV2(ydoc1); + const e2 = encodeStateAsUpdateV2(ydoc2); + const isSynced = array.equalFlat(e1, e2); + + return isSynced; + }); + + /* const waitUntilDocValueMatches = (ydoc: Doc, key: string, value: number): Promise => + promise.until(0, () => { + const result = ydoc.getMap().get(key); + const isMatch = result === value; + + return isMatch; + }); */ + + describe('when clients have permission for room', () => { + describe('when two clients connect to the same doc before any changes', () => { + const setup = () => { + const randomString = Math.random().toString(36).substring(7); + const room = randomString; + + authorizationService.hasPermission.mockResolvedValueOnce({ + hasWriteAccess: true, + room, + userid: 'userId1', + error: null, + }); + authorizationService.hasPermission.mockResolvedValueOnce({ + hasWriteAccess: true, + room, + userid: 'userId2', + error: null, + }); + + const { ydoc: client1Doc } = createWsClient(room); + const { ydoc: client2Doc } = createWsClient(room); + + return { client1Doc, client2Doc }; + }; + + it('syncs doc changes of first client to second client', async () => { + const { client1Doc, client2Doc } = setup(); + + client1Doc.getMap().set('a', 1); + + await waitUntilDocsEqual(client1Doc, client2Doc); + + const result = client2Doc.getMap().get('a'); + expect(result).toBe(1); + }); + + it('syncs subsequent doc changes of second client to first client', async () => { + const { client1Doc, client2Doc } = setup(); + + client1Doc.getMap().set('a', 1); + await waitUntilDocsEqual(client1Doc, client2Doc); + + client2Doc.getMap().set('a', 2); + await waitUntilDocsEqual(client1Doc, client2Doc); + + const result = client1Doc.getMap().get('a'); + expect(result).toBe(2); + }); + + /* it('syncs nearly parallel doc changes of second client to first client', async () => { + // This test is instable + const { client1Doc, client2Doc } = setup(); + + client1Doc.getMap().set('a', 1); + client2Doc.getMap().set('a', 2); + + await waitUntilDocValueMatches(client1Doc, 'a', 2); + + const result = client1Doc.getMap().get('a'); + expect(result).toBe(2); + }); */ + }); + + describe('when two clients connect to the same doc one before and one after the changes', () => { + const setup = () => { + const randomString = Math.random().toString(36).substring(7); + const room = randomString; + + authorizationService.hasPermission.mockResolvedValue({ + hasWriteAccess: true, + room: randomString, + userid: 'userId', + error: null, + }); + + const { ydoc: client1Doc } = createWsClient(room); + + return { client1Doc, room }; + }; + + it('syncs doc changes of first client to second client', async () => { + const { client1Doc, room } = setup(); + + client1Doc.getMap().set('a', 1); + + const { ydoc: client2Doc } = createWsClient(room); + await waitUntilDocsEqual(client1Doc, client2Doc); + + const result = client2Doc.getMap().get('a'); + expect(result).toBe(1); + }); + + it('syncs subsequent doc changes of second client to first client', async () => { + const { client1Doc, room } = setup(); + + client1Doc.getMap().set('a', 1); + + const { ydoc: client2Doc } = createWsClient(room); + await waitUntilDocsEqual(client1Doc, client2Doc); + + client2Doc.getMap().set('a', 2); + await waitUntilDocsEqual(client1Doc, client2Doc); + + const result = client1Doc.getMap().get('a'); + expect(result).toBe(2); + }); + + /* it('syncs nearly parallel doc changes of second client to first client', async () => { + // This test is instable + const { client1Doc, room } = setup(); + + client1Doc.getMap().set('a', 1); + + const { ydoc: client2Doc } = createWsClient(room); + client2Doc.getMap().set('a', 2); + + await waitUntilDocValueMatches(client1Doc, 'a', 2); + + const result = client1Doc.getMap().get('a'); + expect(result).toBe(2); + }); */ + }); + + /* describe('when doc is only pesisted in storage and not in redis', () => { + // Need to implement this test + }); */ + }); + + describe('when client has no permission for room', () => { + describe('when client connects and updates', () => { + const setup = () => { + const randomString = Math.random().toString(36).substring(7); + const room = randomString; + + const errorResponse = ResponsePayloadBuilder.buildWithError(4401, 'Unauthorized'); + authorizationService.hasPermission.mockResolvedValue(errorResponse); + + const { ydoc: client1Doc, provider } = createWsClient(room); + + return { client1Doc, provider }; + }; + + it('syncs doc changes of first client to second client', async () => { + const { provider } = setup(); + + let error: CloseEvent; + if (provider.ws) { + provider.ws.onclose = (event: Event) => { + error = event as CloseEvent; + }; + } + + await promise.until(0, () => { + return error as unknown as boolean; + }); + + // @ts-ignore + expect(error.reason).toBe('Unauthorized'); + // @ts-ignore + expect(error.code).toBe(4401); + }); + }); + }); +}); diff --git a/src/modules/server/server.module.ts b/src/modules/server/server.module.ts index 776ef704..7f928a1d 100644 --- a/src/modules/server/server.module.ts +++ b/src/modules/server/server.module.ts @@ -1,4 +1,5 @@ -import { Module } from '@nestjs/common'; +import { Module, ValidationPipe } from '@nestjs/common'; +import { APP_PIPE } from '@nestjs/core'; import { App } from 'uWebSockets.js'; import { AuthGuardModule } from '../../infra/auth-guard/auth-guard.module.js'; import { AuthorizationModule } from '../../infra/authorization/authorization.module.js'; @@ -28,6 +29,27 @@ import { TldrawDocumentService } from './service/tldraw-document.service.js'; provide: UWS, useValue: App({}), }, + { + provide: APP_PIPE, + useValue: new ValidationPipe({ + // enable DTO instance creation for incoming data + transform: true, + transformOptions: { + // enable type coersion, requires transform:true + enableImplicitConversion: true, + }, + whitelist: true, // only pass valid @ApiProperty-decorated DTO properties, remove others + forbidNonWhitelisted: false, // additional params are just skipped (required when extracting multiple DTO from single query) + forbidUnknownValues: true, + validationError: { + // make sure target (DTO) is set on validation error + // we need this to be able to get DTO metadata for checking if a value has to be the obfuscated on output + // see e.g. ErrorLoggable + target: true, + value: true, + }, + }), + }, ], controllers: [TldrawDocumentController, TldrawConfigController], })