From 4fefaa3524cbe118f1ab6cf92979495ed65d45b9 Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Fri, 27 Feb 2026 16:37:02 +0530 Subject: [PATCH 01/24] feat: adds unit tests for terminate session handler --- package-lock.json | 1105 ++++++++++++++++- package.json | 4 +- src/services/socket-handlers.deps.ts | 8 + .../socket-handlers.terminateSession.test.ts | 220 ++++ src/services/socket-handlers.ts | 14 +- vitest.config.ts | 10 + 6 files changed, 1354 insertions(+), 7 deletions(-) create mode 100644 src/services/socket-handlers.deps.ts create mode 100644 src/services/socket-handlers.terminateSession.test.ts create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json index 8af1a39..09aeb2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,8 @@ "prettier": "^3.3.3", "tsup": "^8.2.4", "tsx": "^4.7.1", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "vitest": "^1.6.0" }, "engines": { "node": "23.x" @@ -882,6 +883,19 @@ "node": ">=12" } }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1731,6 +1745,13 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/fnv1a": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@sindresorhus/fnv1a/-/fnv1a-3.1.0.tgz", @@ -2304,6 +2325,123 @@ "node": ">=15" } }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@waku/core": { "version": "0.0.38", "resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.38.tgz", @@ -2565,6 +2703,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3206,6 +3357,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dns-over-http-resolver": { "version": "3.0.16", "resolved": "https://registry.npmjs.org/dns-over-http-resolver/-/dns-over-http-resolver-3.0.16.tgz", @@ -3629,6 +3790,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3660,6 +3831,30 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -4036,6 +4231,19 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", @@ -4185,6 +4393,16 @@ "node": ">= 0.8" } }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4387,6 +4605,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4682,6 +4913,13 @@ "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz", "integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==" }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4835,6 +5073,23 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4946,6 +5201,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5021,6 +5283,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -5193,6 +5468,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5274,6 +5568,35 @@ "integrity": "sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg==", "license": "MIT" }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5322,6 +5645,22 @@ "integrity": "sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q==", "license": "MIT" }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5602,6 +5941,35 @@ "pathe": "^2.0.1" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postcss-load-config": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", @@ -5671,6 +6039,34 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/progress-events": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/progress-events/-/progress-events-1.0.1.tgz", @@ -5819,6 +6215,13 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -6167,6 +6570,13 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -6235,9 +6645,19 @@ "node": ">= 8" } }, - "node_modules/source-map/node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", "dev": true, "license": "MIT", @@ -6273,6 +6693,13 @@ "memory-pager": "^1.0.2" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -6288,6 +6715,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -6392,6 +6826,19 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6405,6 +6852,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -6464,6 +6924,13 @@ "node": ">=0.8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -6519,6 +6986,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6858,6 +7345,599 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/weald": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/weald/-/weald-1.0.6.tgz", @@ -6949,6 +8029,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 1082faf..e158811 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "clean": "rm -rf dist", "format": "prettier --write .", "lint": "eslint src/**/*.ts", + "test": "vitest", "postinstall": "npm run build", "heroku-postbuild": "npm run build" }, @@ -69,7 +70,8 @@ "prettier": "^3.3.3", "tsup": "^8.2.4", "tsx": "^4.7.1", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "vitest": "^1.6.0" }, "overrides": { "uint8arrays": "^5.1.0" diff --git a/src/services/socket-handlers.deps.ts b/src/services/socket-handlers.deps.ts new file mode 100644 index 0000000..6f13168 --- /dev/null +++ b/src/services/socket-handlers.deps.ts @@ -0,0 +1,8 @@ +import type { AuthService } from "./auth"; +import type { SessionManager } from "./session-manager"; + +export interface SocketHandlerDeps { + authService: AuthService; + sessionManager: SessionManager; +} + diff --git a/src/services/socket-handlers.terminateSession.test.ts b/src/services/socket-handlers.terminateSession.test.ts new file mode 100644 index 0000000..8f4dd31 --- /dev/null +++ b/src/services/socket-handlers.terminateSession.test.ts @@ -0,0 +1,220 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { handleTerminateSession } from "./socket-handlers"; +import type { AppServer, AppSocket } from "../types/index"; +import type { SocketHandlerDeps } from "./socket-handlers.deps"; + +function createFakeIO(fetchSocketsResponse: any[] = []): AppServer { + const fetchSocketsMock = vi.fn().mockResolvedValue(fetchSocketsResponse); + + return { + in: vi.fn(() => ({ + fetchSockets: fetchSocketsMock, + })), + } as unknown as AppServer; +} + +/** + * Fake socket for handler tests. + * If you pass a broadcastOperator, socket.to(room) will return it so you can assert + * on broadcastOperator.emit(event, payload) in the test. + */ +function createFakeSocket(broadcastOperator?: { emit: ReturnType }) { + const toReturn = broadcastOperator ?? { emit: vi.fn() }; + const socket = { + id: "socket-1", + data: { + authenticated: true, + documentId: "doc-1", + sessionDid: "session-1", + role: "owner" as const, + }, + to: vi.fn(() => toReturn), + } as unknown as AppSocket; + return socket; +} + +describe("handleTerminateSession", () => { + const fakeAuthService = { + verifyOwnerToken: vi.fn<[], Promise>(), + }; + const fakeSessionManager = { + getSession: vi.fn(), + terminateSession: vi.fn(), + }; + const deps: SocketHandlerDeps = { + authService: fakeAuthService as any, + sessionManager: fakeSessionManager as any, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns 400 when sessionDid is empty", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs = { + documentId: "test-document-id", + sessionDid: "", + ownerToken: "test-owner-token", + ownerAddress: "test-owner-address", + contractAddress: "test-contract-address", + }; + const callback = vi.fn(); + const callbackResponse = { + status: false, + statusCode: 400, + error: "Session DID is required", + } + await handleTerminateSession(deps, fakeIO, fakeSocket, fakeArgs, callback); + expect(callback).toHaveBeenCalledWith(callbackResponse); + expect(fakeSessionManager.getSession).not.toHaveBeenCalled(); + }); + + it("returns 404 when session is not found", async() => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs = { + documentId: "test-document-id", + sessionDid: "test-session-did", + ownerToken: "test-owner-token", + ownerAddress: "test-owner-address", + contractAddress: "test-contract-address", + }; + const callback = vi.fn(); + + fakeSessionManager.getSession.mockResolvedValue(undefined); + await handleTerminateSession(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeSessionManager.getSession).toHaveBeenCalledOnce(); + expect(fakeSessionManager.getSession).toHaveBeenCalledWith(fakeArgs.documentId, fakeArgs.sessionDid); + + const callbackResponse = { + status: false, + statusCode: 404, + error: "Session not found", + }; + expect(callback).toHaveBeenCalledWith(callbackResponse); + }); + + it("returns 401 when ownerDid does not match session owner", async() => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs = { + documentId: "test-document-id", + sessionDid: "test-session-did", + ownerToken: "test-owner-token", + ownerAddress: "test-owner-address", + contractAddress: "test-contract-address", + }; + const callback = vi.fn(); + + const fakeSessionResponse = { ownerDid: "fake-owner-did" }; + const ownerDidResponse = "different-owner-did"; + fakeSessionManager.getSession.mockResolvedValue(fakeSessionResponse); + fakeAuthService.verifyOwnerToken.mockResolvedValue(ownerDidResponse); + + await handleTerminateSession(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeSessionManager.getSession).toHaveBeenCalledOnce(); + expect(fakeSessionManager.getSession).toHaveBeenCalledWith(fakeArgs.documentId, fakeArgs.sessionDid); + + expect(fakeAuthService.verifyOwnerToken).toHaveBeenCalledOnce(); + expect(fakeAuthService.verifyOwnerToken).toHaveBeenCalledWith( + fakeArgs.ownerToken, + fakeArgs.contractAddress, + fakeArgs.ownerAddress, + ); + + const callbackResponse = { + status: false, + statusCode: 401, + error: "Unauthorized", + }; + expect(callback).toHaveBeenCalledWith(callbackResponse); + }); + + it("returns 200 when session is terminated", async () => { + // Mock: io.in(roomName).fetchSockets() will resolve to this array + const fetchSocketsResponse = [ + { + id: "peer-1", + data: { authenticated: true }, + leave: vi.fn(), + }, + ]; + /** + * To mock this + * const sockets = await io.in(roomName).fetchSockets(); + * such that, the fetchSockets() returns a mock value that we set. + */ + const fakeIO = createFakeIO(fetchSocketsResponse); + + /** + * Likewise, we want to mock + * socket.to(roomName).emit() calls + * socket.to(roomName) returns a broadcast operator, which has an emit function on itself. + * So, we want to mock all of that. + */ + const fakeBroadcastOperator = { emit: vi.fn() }; + const fakeSocket = createFakeSocket(fakeBroadcastOperator); + + const fakeArgs = { + documentId: "test-document-id", + sessionDid: "test-session-did", + ownerToken: "test-owner-token", + ownerAddress: "test-owner-address", + contractAddress: "test-contract-address", + }; + const callback = vi.fn(); + + const fakeSessionResponse = { + sessionDid: fakeArgs.sessionDid, + ownerDid: "match-owner-did", + }; + const ownerDidResponse = "match-owner-did"; + + // set the mock return values + fakeSessionManager.getSession.mockResolvedValue(fakeSessionResponse); + fakeAuthService.verifyOwnerToken.mockResolvedValue(ownerDidResponse); + fakeSessionManager.terminateSession.mockResolvedValue(undefined); + + // now actually call the function + await handleTerminateSession(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeSessionManager.getSession).toHaveBeenCalledOnce(); + expect(fakeSessionManager.getSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeArgs.sessionDid + ); + + expect(fakeAuthService.verifyOwnerToken).toHaveBeenCalledOnce(); + expect(fakeAuthService.verifyOwnerToken).toHaveBeenCalledWith( + fakeArgs.ownerToken, + fakeArgs.contractAddress, + fakeArgs.ownerAddress, + ); + + const roomName = `session::${fakeArgs.documentId}__${fakeSessionResponse.sessionDid}`; + + expect(fakeSocket.to).toHaveBeenCalledWith(roomName); + expect(fakeBroadcastOperator.emit).toHaveBeenCalledWith("/session/terminated", { + roomId: fakeArgs.documentId, + }); + + expect(fetchSocketsResponse[0].leave).toHaveBeenCalledWith(roomName); + expect(fetchSocketsResponse[0].data.authenticated).toBe(false); + expect(fakeSessionManager.terminateSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeSessionResponse.sessionDid + ); + + const callbackResponse = { + status: true, + statusCode: 200, + data: { message: "Session terminated" }, + }; + expect(callback).toHaveBeenCalledWith(callbackResponse); + }); +}); + diff --git a/src/services/socket-handlers.ts b/src/services/socket-handlers.ts index 197429d..da6108d 100644 --- a/src/services/socket-handlers.ts +++ b/src/services/socket-handlers.ts @@ -24,7 +24,12 @@ import { mongodbStore } from "./mongodb-store"; import { sessionManager } from "./session-manager"; import { Hex, isAddress } from "viem"; import type { BroadcastBridge } from "./broadcast-bridge"; +import type { SocketHandlerDeps } from "./socket-handlers.deps"; +const defaultDeps: SocketHandlerDeps = { + authService, + sessionManager, +}; let bridge: BroadcastBridge | null = null; function validateHexAddress(address: string | undefined, fieldName: string): address is Hex { @@ -42,6 +47,7 @@ export function registerEventHandlers(io: AppServer, broadcastBridge?: Broadcast if (broadcastBridge) { bridge = broadcastBridge; } + io.on("connection", (socket: AppSocket) => { console.log(`New Socket.IO connection: ${socket.id}`); @@ -59,7 +65,9 @@ export function registerEventHandlers(io: AppServer, broadcastBridge?: Broadcast socket.on("/documents/update/history", (args, callback) => handleUpdateHistory(socket, args, callback)); socket.on("/documents/peers/list", (args, callback) => handlePeersList(io, socket, args, callback)); socket.on("/documents/awareness", (args) => handleAwareness(io, socket, args)); - socket.on("/documents/terminate", (args, callback) => handleTerminateSession(io, socket, args, callback)); + socket.on("/documents/terminate", (args, callback) => + handleTerminateSession(defaultDeps, io, socket, args, callback) + ); // Disconnection handling socket.on("disconnecting", () => handleDisconnecting(socket)); @@ -674,13 +682,15 @@ async function handleAwareness( } } -async function handleTerminateSession( +export async function handleTerminateSession( + deps: SocketHandlerDeps, io: AppServer, socket: AppSocket, args: TerminateSessionArgs, callback: (response: AckResponse<{ message: string }>) => void ): Promise { try { + const { authService, sessionManager } = deps; const { documentId, sessionDid, ownerToken, ownerAddress, contractAddress } = args; console.log("TERMINATING SESSION", documentId); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..e1d99b1 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts", "tests/**/*.test.ts"], + globals: true, + }, +}); + From f88f0c030b12e578876e9e0b70e008a320c7af81 Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Sun, 1 Mar 2026 01:11:23 +0530 Subject: [PATCH 02/24] adds unit test for awareness handler --- .../socket-handlers.handleAwareness.test.ts | 70 +++++++++++++++++++ src/services/socket-handlers.ts | 2 +- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/services/socket-handlers.handleAwareness.test.ts diff --git a/src/services/socket-handlers.handleAwareness.test.ts b/src/services/socket-handlers.handleAwareness.test.ts new file mode 100644 index 0000000..14df10d --- /dev/null +++ b/src/services/socket-handlers.handleAwareness.test.ts @@ -0,0 +1,70 @@ +import { beforeEach, it, describe, vi, expect } from "vitest"; +import { handleAwareness } from "./socket-handlers"; +import type { AppServer, AppSocket } from "../types"; +import type { SocketData } from "../types"; + +const defaultSocketData: SocketData = { + documentId: "test-document-id", + sessionDid: "test-session-did", + role: "owner", + authenticated: true, +}; + +function createFakeIO(): AppServer { + return {} as unknown as AppServer; +} + +function createFakeSocket( + fakeBroadcastOperator?: { emit: ReturnType }, + dataOverrides?: Partial +): AppSocket { + const op = fakeBroadcastOperator ?? { emit: vi.fn() }; + const data: SocketData = { ...defaultSocketData, ...dataOverrides }; + + return { + id: "socket-1", + data, + to: vi.fn(() => op), + } as unknown as AppSocket; +} + +describe('handleAwareness', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns if socket is not authenticated", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(undefined, { authenticated: false }); + const fakeArgs = { + documentId: "", + data: {}, + collaborationToken: "", + }; + + await handleAwareness(fakeIO, fakeSocket, fakeArgs); + expect(fakeSocket.to).not.toHaveBeenCalled(); + }); + + it("broadcasts awareness update to all participants in room", async () => { + const fakeIO = createFakeIO(); + const fakeBroadcastOperator = { emit: vi.fn() }; + const fakeSocket = createFakeSocket(fakeBroadcastOperator); + const fakeArgs = { + documentId: "test-document-id", + data: { + "position": "AnOj8HhKtKwIwoMhASL3k9y7OKz1t8OLOxBhGgLobtL3__n__ZergaGXSI4+831Mn__n__mjnKm/6GcoUByss9zPvU6hMYQ4hcesVBcSluOAUctFSFNshQak+GHWAzptk4j4NmIIPtihoEmm0XBS3Sa7whQ+tIThoX9J4UGlb5MYk4oAuWgy0zfwU4vjfgqo+NyzcF/mlMDYOvdfnlLKWE/H7jI3V61Ddll6I+3d6oIRfSS2jruzvZn2slDC1Esg7S+a6Uw0LGUxOyY2dXEaaocB9qmuJG8OGw8D4u23mA+IiBfaqKggmt9OOkGiO3xVLr70XNqYfUpJbs8u5kPMuxWX5trT7L+asNitrsBplUsA0Kf4KaJBIQLmVSIWHtwyAaWNSxAPQPP7zW0Gm4VnuY4eTCAjU/iYlx3A==__n__5pXP2B5Nmt+xlOuzGOW9wA==" + }, + collaborationToken: "test-collaborator-token", + }; + + await handleAwareness(fakeIO, fakeSocket, fakeArgs); + + const roomName = `session::${fakeArgs.documentId}__${fakeSocket.data.sessionDid}`; + expect(fakeSocket.to).toHaveBeenCalledWith(roomName); + expect(fakeBroadcastOperator.emit).toHaveBeenCalledWith("/document/awareness_update", { + data: fakeArgs.data, + roomId: fakeArgs.documentId, + }); + }); +}); \ No newline at end of file diff --git a/src/services/socket-handlers.ts b/src/services/socket-handlers.ts index da6108d..95c2aab 100644 --- a/src/services/socket-handlers.ts +++ b/src/services/socket-handlers.ts @@ -655,7 +655,7 @@ async function handlePeersList( } } -async function handleAwareness( +export async function handleAwareness( io: AppServer, socket: AppSocket, args: AwarenessArgs, From 92e12e1c09ba5cb994b0a2ffbe60d1e61a1a86fb Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Tue, 3 Mar 2026 11:47:29 +0530 Subject: [PATCH 03/24] chore: adds unit-tests for disconnecting handler --- ...ocket-handlers.handleDisconnecting.test.ts | 105 ++++++++++++++++++ src/services/socket-handlers.ts | 6 +- 2 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/services/socket-handlers.handleDisconnecting.test.ts diff --git a/src/services/socket-handlers.handleDisconnecting.test.ts b/src/services/socket-handlers.handleDisconnecting.test.ts new file mode 100644 index 0000000..fb674ff --- /dev/null +++ b/src/services/socket-handlers.handleDisconnecting.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { handleDisconnecting } from "./socket-handlers"; +import type { AppSocket } from "../types/index"; +import type { SocketHandlerDeps } from "./socket-handlers.deps"; + +/** + * Fake socket for handler tests. + * If you pass a broadcastOperator, socket.to(room) will return it so you can assert + * on broadcastOperator.emit(event, payload) in the test. + * dataOverrides can be used to set authenticated, documentId, sessionDid, or role for early-return tests. + */ +function createFakeSocket( + broadcastOperator?: { emit: ReturnType }, + dataOverrides?: Partial<{ + authenticated: boolean; + documentId: string; + sessionDid: string; + role: "owner" | "editor"; + }> +): AppSocket { + const toReturn = broadcastOperator ?? { emit: vi.fn() }; + const defaultData = { + authenticated: true, + documentId: "test-document-id", + sessionDid: "test-session-did", + role: "owner" as const, + }; + const data = { ...defaultData, ...dataOverrides }; + const socket = { + id: "socket-1", + data, + to: vi.fn(() => toReturn), + } as unknown as AppSocket; + return socket; +} + +describe("handleDisconnecting", () => { + const fakeAuthService = { + verifyOwnerToken: vi.fn<[], Promise>(), + }; + const fakeSessionManager = { + removeClientFromSession: vi.fn(), + }; + const deps: SocketHandlerDeps = { + authService: fakeAuthService as any, + sessionManager: fakeSessionManager as any, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns early when socket is not authenticated", async () => { + const fakeSocket = createFakeSocket(undefined, { authenticated: false }); + + await handleDisconnecting(deps, fakeSocket); + + expect(fakeSocket.to).not.toHaveBeenCalled(); + expect(fakeSessionManager.removeClientFromSession).not.toHaveBeenCalled(); + }); + + it("returns early when documentId is missing", async () => { + const fakeSocket = createFakeSocket(undefined, { documentId: "" }); + + await handleDisconnecting(deps, fakeSocket); + + expect(fakeSocket.to).not.toHaveBeenCalled(); + expect(fakeSessionManager.removeClientFromSession).not.toHaveBeenCalled(); + }); + + it("returns early when sessionDid is missing", async () => { + const fakeSocket = createFakeSocket(undefined, { sessionDid: "" }); + + await handleDisconnecting(deps, fakeSocket); + + expect(fakeSocket.to).not.toHaveBeenCalled(); + expect(fakeSessionManager.removeClientFromSession).not.toHaveBeenCalled(); + }); + + it("broadcasts membership_change and calls removeClientFromSession when socket has full data", async () => { + const fakeBroadcastOperator = { emit: vi.fn() }; + const fakeSocket = createFakeSocket(fakeBroadcastOperator); + + fakeSessionManager.removeClientFromSession.mockResolvedValue(undefined); + + await handleDisconnecting(deps, fakeSocket); + + const roomName = `session::${fakeSocket.data.documentId}__${fakeSocket.data.sessionDid}`; + + expect(fakeSocket.to).toHaveBeenCalledOnce(); + expect(fakeSocket.to).toHaveBeenCalledWith(roomName); + expect(fakeBroadcastOperator.emit).toHaveBeenCalledWith("/room/membership_change", { + action: "user_left", + user: { role: fakeSocket.data.role }, + roomId: fakeSocket.data.documentId, + }); + + expect(fakeSessionManager.removeClientFromSession).toHaveBeenCalledOnce(); + expect(fakeSessionManager.removeClientFromSession).toHaveBeenCalledWith( + fakeSocket.data.documentId, + fakeSocket.data.sessionDid, + fakeSocket.id + ); + }); +}); diff --git a/src/services/socket-handlers.ts b/src/services/socket-handlers.ts index 95c2aab..98adc7a 100644 --- a/src/services/socket-handlers.ts +++ b/src/services/socket-handlers.ts @@ -70,7 +70,7 @@ export function registerEventHandlers(io: AppServer, broadcastBridge?: Broadcast ); // Disconnection handling - socket.on("disconnecting", () => handleDisconnecting(socket)); + socket.on("disconnecting", () => handleDisconnecting(defaultDeps, socket)); socket.on("disconnect", (reason) => { console.log(`Socket disconnected: ${socket.id}, reason: ${reason}`); }); @@ -782,10 +782,12 @@ export async function handleTerminateSession( } } -async function handleDisconnecting( +export async function handleDisconnecting( + deps: SocketHandlerDeps, socket: AppSocket ): Promise { try { + const { sessionManager } = deps; if (!socket.data.authenticated || !socket.data.documentId || !socket.data.sessionDid) { return; } From a0b60577c1ecaa854aaace6dd0a2bbe1a347d8ca Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Tue, 3 Mar 2026 15:01:30 +0530 Subject: [PATCH 04/24] chore: adds unit test for peers list handler --- .../socket-handlers.handlePeersList.test.ts | 136 ++++++++++++++++++ src/services/socket-handlers.ts | 4 +- 2 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/services/socket-handlers.handlePeersList.test.ts diff --git a/src/services/socket-handlers.handlePeersList.test.ts b/src/services/socket-handlers.handlePeersList.test.ts new file mode 100644 index 0000000..03dc0c2 --- /dev/null +++ b/src/services/socket-handlers.handlePeersList.test.ts @@ -0,0 +1,136 @@ +import { describe, expect, it, vi } from "vitest"; +import { handlePeersList, getRoomName } from "./socket-handlers"; +// import { PeersListArgs } from "../types"; +import type { AppServer, AppSocket, PeersListArgs } from "../types"; + +function createFakeIO(fetchSocketsResponse: any[] = []): AppServer { + const fetchSocketsMock = vi.fn().mockResolvedValue(fetchSocketsResponse); + + return { + in: vi.fn(() => ({ + fetchSockets: fetchSocketsMock, + })), + } as unknown as AppServer; +} + +function createFakeSocket( + broadcastOperator?: { emit: ReturnType}, + dataOverrides?: Partial<{ + authenticated: boolean; + documentId: string; + sessionDid: string; + role: "owner" | "editor"; + }> +): AppSocket { + const toReturn = broadcastOperator ?? { emit: vi.fn() }; + const defaultData = { + authenticated: true, + documentId: "test-document-id", + sessionDid: "test-session-did", + role: "owner" as const, + }; + const data = { ...defaultData, ...dataOverrides }; + + return { + id: "socket-1", + data, + to: vi.fn(() => toReturn), + } as unknown as AppSocket; +} + +describe("tests peers list handler", () => { + it("returns early when socket is not authenticated", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(undefined, { authenticated: false }); + const fakeArgs: PeersListArgs = {}; + const fakeCallback= vi.fn(); + + await handlePeersList(fakeIO, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeIO.in).not.toHaveBeenCalled(); + expect(fakeCallback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Not authenticated or session not found", + }); + }); + + it("returns early when documentId is missing in socket data", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(undefined, { documentId: "" }); + const fakeArgs: PeersListArgs = {}; + const fakeCallback= vi.fn(); + + await handlePeersList(fakeIO, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeIO.in).not.toHaveBeenCalled(); + expect(fakeCallback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Not authenticated or session not found", + }); + }); + + it("returns early when sessionDid is missing in socket data", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(undefined, { sessionDid: "" }); + const fakeArgs: PeersListArgs = {}; + const fakeCallback = vi.fn(); + + await handlePeersList(fakeIO, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeIO.in).not.toHaveBeenCalled(); + expect(fakeCallback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Not authenticated or session not found", + }); + }); + + it("returns 500 when fetchSockets throws", async () => { + const fetchSocketsMock = vi.fn().mockRejectedValue(new Error("fetch failed")); + const fakeIO = { + in: vi.fn(() => ({ + fetchSockets: fetchSocketsMock, + })), + } as unknown as AppServer; + const fakeSocket = createFakeSocket(); + const fakeArgs: PeersListArgs = {}; + const fakeCallback = vi.fn(); + + await handlePeersList(fakeIO, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: false, + statusCode: 500, + error: "Internal server error", + }); + }); + + it("returns peers list successfully", async () => { + const fetchSocketsResponse = [ + { id: "socket-id-1" }, + { id: "socket-id-2" } + ]; + const fakeIO = createFakeIO(fetchSocketsResponse); + const fakeSocket = createFakeSocket(); + const fakeArgs: PeersListArgs = {}; + const fakeCallback = vi.fn(); + + await handlePeersList(fakeIO, fakeSocket, fakeArgs, fakeCallback); + + const documentId = fakeArgs.documentId || fakeSocket.data.documentId; + const roomName = getRoomName(documentId, fakeSocket.data.sessionDid); + + expect(fakeIO.in).toHaveBeenCalledWith(roomName); + + const peersData = ["socket-id-1", "socket-id-2"]; + expect(fakeCallback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + peers: peersData, + }, + }) + }); +}); \ No newline at end of file diff --git a/src/services/socket-handlers.ts b/src/services/socket-handlers.ts index 98adc7a..ed54147 100644 --- a/src/services/socket-handlers.ts +++ b/src/services/socket-handlers.ts @@ -39,7 +39,7 @@ function validateHexAddress(address: string | undefined, fieldName: string): add return true; } -function getRoomName(documentId: string, sessionDid: string): string { +export function getRoomName(documentId: string, sessionDid: string): string { return `session::${documentId}__${sessionDid}`; } @@ -611,7 +611,7 @@ async function handleUpdateHistory( } } -async function handlePeersList( +export async function handlePeersList( io: AppServer, socket: AppSocket, args: PeersListArgs, From b3a4ad684f353ee8907f8b3c7d7d468ce4fcc67a Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Wed, 4 Mar 2026 12:59:48 +0530 Subject: [PATCH 05/24] chore: injects mongostore as a dependency in handler --- src/services/socket-handlers.deps.ts | 3 ++- .../socket-handlers.handleDisconnecting.test.ts | 2 ++ .../socket-handlers.handleUpdateHistory.test.ts | 6 ++++++ src/services/socket-handlers.terminateSession.test.ts | 2 ++ src/services/socket-handlers.ts | 10 ++++++++-- 5 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/services/socket-handlers.handleUpdateHistory.test.ts diff --git a/src/services/socket-handlers.deps.ts b/src/services/socket-handlers.deps.ts index 6f13168..5a09831 100644 --- a/src/services/socket-handlers.deps.ts +++ b/src/services/socket-handlers.deps.ts @@ -1,8 +1,9 @@ import type { AuthService } from "./auth"; import type { SessionManager } from "./session-manager"; +import type { MongoDBStore } from "./mongodb-store"; export interface SocketHandlerDeps { authService: AuthService; sessionManager: SessionManager; + mongodbStore: MongoDBStore; } - diff --git a/src/services/socket-handlers.handleDisconnecting.test.ts b/src/services/socket-handlers.handleDisconnecting.test.ts index fb674ff..f2a555d 100644 --- a/src/services/socket-handlers.handleDisconnecting.test.ts +++ b/src/services/socket-handlers.handleDisconnecting.test.ts @@ -41,9 +41,11 @@ describe("handleDisconnecting", () => { const fakeSessionManager = { removeClientFromSession: vi.fn(), }; + const fakeMongoDBStore = {} as any; const deps: SocketHandlerDeps = { authService: fakeAuthService as any, sessionManager: fakeSessionManager as any, + mongodbStore: fakeMongoDBStore, }; beforeEach(() => { diff --git a/src/services/socket-handlers.handleUpdateHistory.test.ts b/src/services/socket-handlers.handleUpdateHistory.test.ts new file mode 100644 index 0000000..f291115 --- /dev/null +++ b/src/services/socket-handlers.handleUpdateHistory.test.ts @@ -0,0 +1,6 @@ +import { describe } from "vitest"; + +// describe("updateHistory", () => { + + +// }); \ No newline at end of file diff --git a/src/services/socket-handlers.terminateSession.test.ts b/src/services/socket-handlers.terminateSession.test.ts index 8f4dd31..44f30dc 100644 --- a/src/services/socket-handlers.terminateSession.test.ts +++ b/src/services/socket-handlers.terminateSession.test.ts @@ -41,9 +41,11 @@ describe("handleTerminateSession", () => { getSession: vi.fn(), terminateSession: vi.fn(), }; + const fakeMongoDBStore = {} as any; const deps: SocketHandlerDeps = { authService: fakeAuthService as any, sessionManager: fakeSessionManager as any, + mongodbStore: fakeMongoDBStore, }; beforeEach(() => { diff --git a/src/services/socket-handlers.ts b/src/services/socket-handlers.ts index ed54147..d221cbb 100644 --- a/src/services/socket-handlers.ts +++ b/src/services/socket-handlers.ts @@ -29,7 +29,9 @@ import type { SocketHandlerDeps } from "./socket-handlers.deps"; const defaultDeps: SocketHandlerDeps = { authService, sessionManager, + mongodbStore, }; + let bridge: BroadcastBridge | null = null; function validateHexAddress(address: string | undefined, fieldName: string): address is Hex { @@ -62,7 +64,9 @@ export function registerEventHandlers(io: AppServer, broadcastBridge?: Broadcast socket.on("/documents/update", (args, callback) => handleDocumentUpdate(io, socket, args, callback)); socket.on("/documents/commit", (args, callback) => handleDocumentCommit(socket, args, callback)); socket.on("/documents/commit/history", (args, callback) => handleCommitHistory(socket, args, callback)); - socket.on("/documents/update/history", (args, callback) => handleUpdateHistory(socket, args, callback)); + socket.on("/documents/update/history", (args, callback) => + handleUpdateHistory(defaultDeps, socket, args, callback) + ); socket.on("/documents/peers/list", (args, callback) => handlePeersList(io, socket, args, callback)); socket.on("/documents/awareness", (args) => handleAwareness(io, socket, args)); socket.on("/documents/terminate", (args, callback) => @@ -568,12 +572,14 @@ async function handleCommitHistory( } } -async function handleUpdateHistory( +export async function handleUpdateHistory( + deps: SocketHandlerDeps, socket: AppSocket, args: UpdateHistoryArgs, callback: (response: AckResponse<{ history: DocumentUpdate[]; total: number }>) => void ): Promise { try { + const { mongodbStore } = deps; if (!requireAuth(socket)) { return callback({ status: false, From bb22aa6993566cfd58a64fcca50f846a66213a0a Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Wed, 4 Mar 2026 14:01:39 +0530 Subject: [PATCH 06/24] feat: adds unit tests for update history handler --- .../socket-handlers.handlePeersList.test.ts | 6 +- ...ocket-handlers.handleUpdateHistory.test.ts | 208 +++++++++++++++++- 2 files changed, 210 insertions(+), 4 deletions(-) diff --git a/src/services/socket-handlers.handlePeersList.test.ts b/src/services/socket-handlers.handlePeersList.test.ts index 03dc0c2..49e7bfe 100644 --- a/src/services/socket-handlers.handlePeersList.test.ts +++ b/src/services/socket-handlers.handlePeersList.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi, beforeEach } from "vitest"; import { handlePeersList, getRoomName } from "./socket-handlers"; // import { PeersListArgs } from "../types"; import type { AppServer, AppSocket, PeersListArgs } from "../types"; @@ -39,6 +39,10 @@ function createFakeSocket( } describe("tests peers list handler", () => { + beforeEach(() => { + vi.clearAllMocks(); + }) + it("returns early when socket is not authenticated", async () => { const fakeIO = createFakeIO(); const fakeSocket = createFakeSocket(undefined, { authenticated: false }); diff --git a/src/services/socket-handlers.handleUpdateHistory.test.ts b/src/services/socket-handlers.handleUpdateHistory.test.ts index f291115..2412e3a 100644 --- a/src/services/socket-handlers.handleUpdateHistory.test.ts +++ b/src/services/socket-handlers.handleUpdateHistory.test.ts @@ -1,6 +1,208 @@ -import { describe } from "vitest"; +import { describe, vi, it, expect, beforeEach } from "vitest"; +import { handleUpdateHistory } from "./socket-handlers"; +// TODO: does it make any difference if I mention/don't-mention type +import type { SocketHandlerDeps } from "./socket-handlers.deps"; +import type { AppServer, AppSocket, DocumentUpdate, UpdateHistoryArgs } from "../types"; -// describe("updateHistory", () => { +function createFakeSocket( + broadcastOperator?: { emit: ReturnType}, + dataOverrides?: Partial<{ + authenticated: boolean; + documentId: string; + sessionDid: string; + role: "owner" | "editor"; + }> +): AppSocket { + const toReturn = broadcastOperator ?? { emit: vi.fn() }; + const defaultData = { + authenticated: true, + documentId: "test-document-id", + sessionDid: "test-session-did", + role: "owner" as const, + }; + const data = { ...defaultData, ...dataOverrides }; + return { + id: "socket-1", + data, + to: vi.fn(() => toReturn), + } as unknown as AppSocket; +} -// }); \ No newline at end of file +describe("updateHistory", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const fakeMongodbStore = { + getUpdatesByDocument: vi.fn(), + }; + + const deps: SocketHandlerDeps = { + authService: {} as any, + sessionManager: {} as any, + mongodbStore: fakeMongodbStore as any, + }; + + it('returns early when not authenticated', async () => { + const fakeSocket: AppSocket = createFakeSocket(undefined, { authenticated: false }); + const fakeArgs: UpdateHistoryArgs = { + documentId: "test-document-id", + }; + const fakeCallback = vi.fn(); + + await handleUpdateHistory(deps, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Not authenticated", + }); + }); + + it('returns early when documentId is empty in socket data', async () => { + const fakeSocket: AppSocket = createFakeSocket(undefined, { documentId: "" }); + const fakeArgs: UpdateHistoryArgs = {}; + const fakeCallback = vi.fn(); + + await handleUpdateHistory(deps, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Not authenticated", + }); + }); + + it('returns early when sessionDid is empty in socket data', async () => { + const fakeSocket: AppSocket = createFakeSocket(undefined, { sessionDid: "" }); + const fakeArgs: UpdateHistoryArgs = {}; + const fakeCallback = vi.fn(); + + await handleUpdateHistory(deps, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Not authenticated", + }); + }); + + it('returns update history successfully, with fallback argument values', async () => { + const fakeSocket: AppSocket = createFakeSocket(undefined, { + documentId: "test-document-id" + }); + const fakeArgs: UpdateHistoryArgs = { + documentId: "test-document-id", + }; + const fakeCallback = vi.fn(); + const fakeResponse: DocumentUpdate[] = []; + const documentId = fakeArgs.documentId || fakeSocket.data.documentId; + + fakeMongodbStore.getUpdatesByDocument.mockResolvedValue(fakeResponse); + + await handleUpdateHistory(deps, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalled(); + expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalledWith({ + documentId, + sessionDid: fakeSocket.data.sessionDid, + }, { + offset: 0, limit: 100, sort: "desc", committed: undefined, + }); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + history: [], + total: 0, + }, + }); + }); + + it('returns update history successfully with proper argument values set', async () => { + const fakeSocket: AppSocket = createFakeSocket(undefined, { + documentId: "test-document-id" + }); + const fakeArgs: UpdateHistoryArgs = { + documentId: "test-document-id", + limit: 1000, + offset: 0, + filters: { + committed: false, + }, + sort: "desc", + }; + const fakeCallback = vi.fn(); + const fakeResponse: DocumentUpdate[] = [ + { + "id": "test-id", + "documentId": "test-document-id", + "data": "test-encrypted-data", + "updateType": "yjs_update", + "committed": false, + "commitCid": null, + "createdAt": 1772181495470, + "sessionDid": "test-session-did" + } + ]; + const documentId = fakeArgs.documentId || fakeSocket.data.documentId; + + fakeMongodbStore.getUpdatesByDocument.mockResolvedValue(fakeResponse); + + await handleUpdateHistory(deps, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalled(); + expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalledWith({ + documentId, + sessionDid: fakeSocket.data.sessionDid, + }, { + offset: 0, limit: 1000, sort: "desc", committed: false, + }); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + history: fakeResponse, + total: fakeResponse.length, + }, + }); + }); + + it('returns 500 due to db operation error', async () => { + const fakeSocket: AppSocket = createFakeSocket(undefined, { + documentId: "test-document-id" + }); + const fakeArgs: UpdateHistoryArgs = { + documentId: "test-document-id", + limit: 1000, + offset: 0, + filters: { + committed: false, + }, + sort: "desc", + }; + const fakeCallback = vi.fn(); + const documentId = fakeArgs.documentId || fakeSocket.data.documentId; + + fakeMongodbStore.getUpdatesByDocument.mockRejectedValue(new Error("db error")); + + await handleUpdateHistory(deps, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalled(); + expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalledWith({ + documentId, + sessionDid: fakeSocket.data.sessionDid, + }, { + offset: 0, limit: 1000, sort: "desc", committed: false, + }); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: false, + statusCode: 500, + error: "Internal server error", + }); + }); +}); \ No newline at end of file From 5657865fee8692f7b495be2285f8d47b0bcd2adc Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Wed, 4 Mar 2026 19:21:36 +0530 Subject: [PATCH 07/24] chore: adds unit tests for update commit handler --- ...ocket-handlers.handleCommitHistory.test.ts | 186 ++++++++++++++++++ ...ocket-handlers.handleUpdateHistory.test.ts | 14 +- src/services/socket-handlers.ts | 8 +- 3 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 src/services/socket-handlers.handleCommitHistory.test.ts diff --git a/src/services/socket-handlers.handleCommitHistory.test.ts b/src/services/socket-handlers.handleCommitHistory.test.ts new file mode 100644 index 0000000..2dd8917 --- /dev/null +++ b/src/services/socket-handlers.handleCommitHistory.test.ts @@ -0,0 +1,186 @@ +import { describe, vi, it, expect, beforeEach } from "vitest"; +import { handleCommitHistory } from "./socket-handlers"; +import type { SocketHandlerDeps } from "./socket-handlers.deps"; +import type { AppSocket, DocumentCommit, CommitHistoryArgs } from "../types"; + +function createFakeSocket( + broadcastOperator?: { emit: ReturnType}, + dataOverrides?: Partial<{ + authenticated: boolean; + documentId: string; + sessionDid: string; + role: "owner" | "editor"; + }> +): AppSocket { + const toReturn = broadcastOperator ?? { emit: vi.fn() }; + const defaultData = { + authenticated: true, + documentId: "test-document-id", + sessionDid: "test-session-did", + role: "owner" as const, + }; + const data = { ...defaultData, ...dataOverrides }; + + return { + id: "socket-1", + data, + to: vi.fn(() => toReturn), + } as unknown as AppSocket; +} + +describe("commitHistory", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const fakeMongodbStore = { + getCommitsByDocument: vi.fn(), + }; + + const deps: SocketHandlerDeps = { + authService: {} as any, + sessionManager: {} as any, + mongodbStore: fakeMongodbStore as any, + }; + + it('returns early when not authenticated', async () => { + const fakeSocket: AppSocket = createFakeSocket(undefined, { authenticated: false }); + const fakeArgs: CommitHistoryArgs = { + documentId: "test-document-id", + }; + const fakeCallback = vi.fn(); + + await handleCommitHistory(deps, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Not authenticated", + }); + }); + + it('returns early when documentId is empty in socket data', async () => { + const fakeSocket: AppSocket = createFakeSocket(undefined, { documentId: "" }); + const fakeArgs: CommitHistoryArgs = {}; + const fakeCallback = vi.fn(); + + await handleCommitHistory(deps, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Not authenticated", + }); + }); + + it('returns early when sessionDid is empty in socket data', async () => { + const fakeSocket: AppSocket = createFakeSocket(undefined, { sessionDid: "" }); + const fakeArgs: CommitHistoryArgs = { + documentId: "test-document-id", + }; + const fakeCallback = vi.fn(); + + await handleCommitHistory(deps, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Not authenticated", + }); + }); + + it('returns commit history successfully, with fallback argument values', async () => { + const fakeSocket: AppSocket = createFakeSocket(); + const fakeArgs: CommitHistoryArgs = {}; + const fakeCallback = vi.fn(); + const fakeResponse: DocumentCommit[] = []; + const documentId = fakeArgs.documentId || fakeSocket.data.documentId; + + fakeMongodbStore.getCommitsByDocument.mockResolvedValue(fakeResponse); + + await handleCommitHistory(deps, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalled(); + expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalledWith({ + documentId, + sessionDid: fakeSocket.data.sessionDid, + }, { + offset: 0, limit: 10, sort: "desc" + }); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + history: fakeResponse, + total: fakeResponse.length, + }, + }); + }); + + it('returns update history successfully with proper argument values set', async () => { + const fakeSocket: AppSocket = createFakeSocket(); + const fakeArgs: CommitHistoryArgs = { + documentId: "test-document-id", + limit: 15, + offset: 0, + sort: "desc", + }; + const fakeCallback = vi.fn(); + const fakeResponse: DocumentCommit[] = [ + // {} + ]; + const documentId = fakeArgs.documentId || fakeSocket.data.documentId; + + fakeMongodbStore.getCommitsByDocument.mockResolvedValue(fakeResponse); + + await handleCommitHistory(deps, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalled(); + expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalledWith({ + documentId, + sessionDid: fakeSocket.data.sessionDid, + }, { + offset: fakeArgs.offset, limit: fakeArgs.limit, sort: fakeArgs.sort, + }); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + history: fakeResponse, + total: fakeResponse.length, + }, + }); + }); + + it('returns 500 due to db operation error', async () => { + const fakeSocket: AppSocket = createFakeSocket(); + const fakeArgs: CommitHistoryArgs = { + documentId: "test-document-id", + limit: 15, + offset: 0, + sort: "desc", + }; + const fakeCallback = vi.fn(); + const documentId = fakeArgs.documentId || fakeSocket.data.documentId; + + fakeMongodbStore.getCommitsByDocument.mockRejectedValue(new Error("db error")); + + await handleCommitHistory(deps, fakeSocket, fakeArgs, fakeCallback); + + expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalled(); + expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalledWith({ + documentId, + sessionDid: fakeSocket.data.sessionDid, + }, { + offset: fakeArgs.offset, limit: fakeArgs.limit, sort: fakeArgs.sort, + }); + + expect(fakeCallback).toHaveBeenCalledWith({ + status: false, + statusCode: 500, + error: "Internal server error", + }); + }); +}); \ No newline at end of file diff --git a/src/services/socket-handlers.handleUpdateHistory.test.ts b/src/services/socket-handlers.handleUpdateHistory.test.ts index 2412e3a..8ac463e 100644 --- a/src/services/socket-handlers.handleUpdateHistory.test.ts +++ b/src/services/socket-handlers.handleUpdateHistory.test.ts @@ -76,7 +76,9 @@ describe("updateHistory", () => { it('returns early when sessionDid is empty in socket data', async () => { const fakeSocket: AppSocket = createFakeSocket(undefined, { sessionDid: "" }); - const fakeArgs: UpdateHistoryArgs = {}; + const fakeArgs: UpdateHistoryArgs = { + documentId: "test-document-id", + }; const fakeCallback = vi.fn(); await handleUpdateHistory(deps, fakeSocket, fakeArgs, fakeCallback); @@ -122,9 +124,7 @@ describe("updateHistory", () => { }); it('returns update history successfully with proper argument values set', async () => { - const fakeSocket: AppSocket = createFakeSocket(undefined, { - documentId: "test-document-id" - }); + const fakeSocket: AppSocket = createFakeSocket(); const fakeArgs: UpdateHistoryArgs = { documentId: "test-document-id", limit: 1000, @@ -158,7 +158,7 @@ describe("updateHistory", () => { documentId, sessionDid: fakeSocket.data.sessionDid, }, { - offset: 0, limit: 1000, sort: "desc", committed: false, + offset: fakeArgs.offset, limit: fakeArgs.limit, sort: fakeArgs.sort, committed: fakeArgs.filters?.committed, }); expect(fakeCallback).toHaveBeenCalledWith({ @@ -172,9 +172,7 @@ describe("updateHistory", () => { }); it('returns 500 due to db operation error', async () => { - const fakeSocket: AppSocket = createFakeSocket(undefined, { - documentId: "test-document-id" - }); + const fakeSocket: AppSocket = createFakeSocket(); const fakeArgs: UpdateHistoryArgs = { documentId: "test-document-id", limit: 1000, diff --git a/src/services/socket-handlers.ts b/src/services/socket-handlers.ts index d221cbb..f4f1245 100644 --- a/src/services/socket-handlers.ts +++ b/src/services/socket-handlers.ts @@ -63,7 +63,9 @@ export function registerEventHandlers(io: AppServer, broadcastBridge?: Broadcast socket.on("/auth", (args, callback) => handleAuth(io, socket, args, callback)); socket.on("/documents/update", (args, callback) => handleDocumentUpdate(io, socket, args, callback)); socket.on("/documents/commit", (args, callback) => handleDocumentCommit(socket, args, callback)); - socket.on("/documents/commit/history", (args, callback) => handleCommitHistory(socket, args, callback)); + socket.on("/documents/commit/history", (args, callback) => + handleCommitHistory(defaultDeps, socket, args, callback) + ); socket.on("/documents/update/history", (args, callback) => handleUpdateHistory(defaultDeps, socket, args, callback) ); @@ -529,12 +531,14 @@ async function handleDocumentCommit( } } -async function handleCommitHistory( +export async function handleCommitHistory( + deps: SocketHandlerDeps, socket: AppSocket, args: CommitHistoryArgs, callback: (response: AckResponse<{ history: DocumentCommit[]; total: number }>) => void ): Promise { try { + const { mongodbStore } = deps; if (!requireAuth(socket)) { return callback({ status: false, From bfc464c987219eff6123b1661892413667f33857 Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Thu, 5 Mar 2026 00:37:03 +0530 Subject: [PATCH 08/24] chore: injecting dependencies on remaining handlers --- src/services/socket-handlers.ts | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/services/socket-handlers.ts b/src/services/socket-handlers.ts index f4f1245..2c4b8ab 100644 --- a/src/services/socket-handlers.ts +++ b/src/services/socket-handlers.ts @@ -60,9 +60,13 @@ export function registerEventHandlers(io: AppServer, broadcastBridge?: Broadcast }); // Register event handlers - socket.on("/auth", (args, callback) => handleAuth(io, socket, args, callback)); - socket.on("/documents/update", (args, callback) => handleDocumentUpdate(io, socket, args, callback)); - socket.on("/documents/commit", (args, callback) => handleDocumentCommit(socket, args, callback)); + socket.on("/auth", (args, callback) => handleAuth(defaultDeps, io, socket, args, callback)); + socket.on("/documents/update", (args, callback) => + handleDocumentUpdate(defaultDeps, io, socket, args, callback) + ); + socket.on("/documents/commit", (args, callback) => + handleDocumentCommit(defaultDeps, socket, args, callback) + ); socket.on("/documents/commit/history", (args, callback) => handleCommitHistory(defaultDeps, socket, args, callback) ); @@ -86,13 +90,15 @@ export function registerEventHandlers(io: AppServer, broadcastBridge?: Broadcast }); } -async function handleAuth( +export async function handleAuth( + deps: SocketHandlerDeps, io: AppServer, socket: AppSocket, args: AuthArgs, callback: (response: AckResponse) => void ): Promise { try { + const { authService, sessionManager } = deps; const { documentId, collaborationToken, sessionDid } = args; if (!collaborationToken) { @@ -129,7 +135,7 @@ async function handleAuth( let roomInfo: string | undefined; if (!existingSession && args.ownerToken) { - // - Setup new session (owner flow) - + // - Set up a new session (owner flow) if (!args.ownerToken || !sessionDid) { return callback({ status: false, @@ -214,7 +220,7 @@ async function handleAuth( sessionType = "new"; roomInfo = args.roomInfo; } else if (existingSession) { - // - Join existing session - + // Join an existing session const userDid = await authService.verifyCollaborationToken( collaborationToken, existingSession.sessionDid, @@ -319,13 +325,15 @@ async function handleAuth( } } -async function handleDocumentUpdate( +export async function handleDocumentUpdate( + deps: SocketHandlerDeps, io: AppServer, socket: AppSocket, args: DocumentUpdateArgs, callback: (response: AckResponse) => void ): Promise { try { + const { authService, sessionManager, mongodbStore } = deps; if (!requireAuth(socket)) { return callback({ status: false, @@ -427,12 +435,14 @@ async function handleDocumentUpdate( } } -async function handleDocumentCommit( +export async function handleDocumentCommit( + deps: SocketHandlerDeps, socket: AppSocket, args: DocumentCommitArgs, callback: (response: AckResponse) => void ): Promise { try { + const { authService, sessionManager, mongodbStore } = deps; if (!requireAuth(socket)) { return callback({ status: false, From 34c99072fd6c3f21773f9881f25463e45de86983 Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Thu, 5 Mar 2026 11:06:23 +0530 Subject: [PATCH 09/24] feat: adds unit test for auth handler --- .../socket-handlers.handleAuth.test.ts | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 src/services/socket-handlers.handleAuth.test.ts diff --git a/src/services/socket-handlers.handleAuth.test.ts b/src/services/socket-handlers.handleAuth.test.ts new file mode 100644 index 0000000..5b45808 --- /dev/null +++ b/src/services/socket-handlers.handleAuth.test.ts @@ -0,0 +1,264 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { handleAuth, getRoomName } from "./socket-handlers"; +import type { AppServer, AppSocket, AuthArgs } from "../types"; +import type { SocketHandlerDeps } from "./socket-handlers.deps"; + +function createFakeIO(): AppServer { + return { + in: vi.fn(), + } as unknown as AppServer; +} + +function createFakeSocket( + broadcastOperator?: { emit: ReturnType }, + dataOverrides?: Partial<{ + authenticated: boolean; + documentId: string; + sessionDid: string; + role: "owner" | "editor"; + }> +): AppSocket { + const toReturn = broadcastOperator ?? { emit: vi.fn() }; + const defaultData = { + authenticated: false, + documentId: "", + sessionDid: "", + role: "editor" as const, + }; + const data = { ...defaultData, ...dataOverrides }; + + return { + id: "socket-1", + data, + to: vi.fn(() => toReturn), + join: vi.fn(), + } as unknown as AppSocket; +} + +describe("handleAuth", () => { + const fakeAuthService = { + verifyOwnerToken: vi.fn(), + verifyCollaborationToken: vi.fn(), + getServerDid: vi.fn(), + }; + const fakeSessionManager = { + getSession: vi.fn(), + terminateOtherExistingSessions: vi.fn(), + createSession: vi.fn(), + updateRoomInfo: vi.fn(), + addClientToSession: vi.fn(), + }; + const fakeMongoDBStore = {} as any; + + const deps: SocketHandlerDeps = { + authService: fakeAuthService as any, + sessionManager: fakeSessionManager as any, + mongodbStore: fakeMongoDBStore, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns 400 when collaborationToken is missing", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "" as any, + }; + const callback = vi.fn(); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 400, + error: "Collaboration token is required", + }); + }); + + it("returns 400 when documentId is missing", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: AuthArgs = { + documentId: "" as any, + sessionDid: "session-1", + collaborationToken: "collab-token", + }; + const callback = vi.fn(); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 400, + error: "Document ID is required", + }); + }); + + it("returns 400 when sessionDid is missing", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "" as any, + collaborationToken: "collab-token", + }; + const callback = vi.fn(); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 400, + error: "Session DID is required", + }); + }); + + it("creates a new owner session when no existing session and ownerToken is provided", async () => { + const fakeIO = createFakeIO(); + const fakeBroadcastOperator = { emit: vi.fn() }; + const fakeSocket = createFakeSocket(fakeBroadcastOperator); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + ownerToken: "owner-token", + ownerAddress: "0xowner", + contractAddress: "0xcontract", + roomInfo: "room-info", + }; + const callback = vi.fn(); + + fakeSessionManager.getSession.mockResolvedValue(undefined); + fakeAuthService.verifyOwnerToken.mockResolvedValue("owner-did"); + fakeSessionManager.terminateOtherExistingSessions.mockResolvedValue(undefined); + fakeSessionManager.createSession.mockResolvedValue(undefined); + fakeSessionManager.addClientToSession.mockResolvedValue(undefined); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeSessionManager.getSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeArgs.sessionDid + ); + expect(fakeAuthService.verifyOwnerToken).toHaveBeenCalledWith( + fakeArgs.ownerToken, + fakeArgs.contractAddress, + fakeArgs.ownerAddress + ); + expect(fakeSessionManager.terminateOtherExistingSessions).toHaveBeenCalledWith( + fakeArgs.documentId, + "owner-did" + ); + expect(fakeSessionManager.createSession).toHaveBeenCalledWith({ + documentId: fakeArgs.documentId, + sessionDid: fakeArgs.sessionDid, + ownerDid: "owner-did", + roomInfo: fakeArgs.roomInfo, + }); + + expect(fakeSocket.data.authenticated).toBe(true); + expect(fakeSocket.data.documentId).toBe(fakeArgs.documentId); + expect(fakeSocket.data.sessionDid).toBe(fakeArgs.sessionDid); + expect(fakeSocket.data.role).toBe("owner"); + + const roomName = getRoomName(fakeArgs.documentId, fakeArgs.sessionDid); + expect(fakeSocket.join).toHaveBeenCalledWith(roomName); + expect(fakeSessionManager.addClientToSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeArgs.sessionDid, + fakeSocket.id + ); + expect(fakeSocket.to).toHaveBeenCalledWith(roomName); + expect(fakeBroadcastOperator.emit).toHaveBeenCalledWith("/room/membership_change", { + action: "user_joined", + user: { role: "owner" }, + roomId: fakeArgs.documentId, + }); + + expect(callback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + message: "Authentication successful", + role: "owner", + sessionType: "new", + roomInfo: fakeArgs.roomInfo, + }, + }); + }); + + it("joins an existing session as editor when collaboration token is valid", async () => { + const fakeIO = createFakeIO(); + const fakeBroadcastOperator = { emit: vi.fn() }; + const fakeSocket = createFakeSocket(fakeBroadcastOperator); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + }; + const callback = vi.fn(); + + const existingSession = { + sessionDid: fakeArgs.sessionDid, + ownerDid: "owner-did", + roomInfo: "existing-room-info", + }; + + fakeSessionManager.getSession.mockResolvedValue(existingSession); + fakeAuthService.verifyCollaborationToken.mockResolvedValue("user-did"); + fakeSessionManager.addClientToSession.mockResolvedValue(undefined); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeAuthService.verifyCollaborationToken).toHaveBeenCalledWith( + fakeArgs.collaborationToken, + existingSession.sessionDid, + fakeArgs.documentId + ); + + const roomName = getRoomName(fakeArgs.documentId, fakeArgs.sessionDid); + expect(fakeSocket.join).toHaveBeenCalledWith(roomName); + expect(fakeBroadcastOperator.emit).toHaveBeenCalledWith("/room/membership_change", { + action: "user_joined", + user: { role: "editor" }, + roomId: fakeArgs.documentId, + }); + + expect(callback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + message: "Authentication successful", + role: "editor", + sessionType: "existing", + roomInfo: existingSession.roomInfo, + }, + }); + }); + + it("returns 404 when existing session is not found and no ownerToken is provided", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + }; + const callback = vi.fn(); + + fakeSessionManager.getSession.mockResolvedValue(undefined); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 404, + error: "Session not found", + }); + }); +}); + From f52fd1b2ae8e4288095e89e1394c383013fbfe2c Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Thu, 5 Mar 2026 11:32:17 +0530 Subject: [PATCH 10/24] chore: adds unit tests for document commit handler --- ...cket-handlers.handleDocumentUpdate.test.ts | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 src/services/socket-handlers.handleDocumentUpdate.test.ts diff --git a/src/services/socket-handlers.handleDocumentUpdate.test.ts b/src/services/socket-handlers.handleDocumentUpdate.test.ts new file mode 100644 index 0000000..510142f --- /dev/null +++ b/src/services/socket-handlers.handleDocumentUpdate.test.ts @@ -0,0 +1,202 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { handleDocumentUpdate, getRoomName } from "./socket-handlers"; +import type { AppServer, AppSocket, DocumentUpdateArgs } from "../types"; +import type { SocketHandlerDeps } from "./socket-handlers.deps"; + +function createFakeIO(): AppServer { + return {} as unknown as AppServer; +} + +function createFakeSocket( + broadcastOperator?: { emit: ReturnType }, + dataOverrides?: Partial<{ + authenticated: boolean; + documentId: string; + sessionDid: string; + role: "owner" | "editor"; + }> +): AppSocket { + const toReturn = broadcastOperator ?? { emit: vi.fn() }; + const defaultData = { + authenticated: true, + documentId: "test-document-id", + sessionDid: "test-session-did", + role: "owner" as const, + }; + const data = { ...defaultData, ...dataOverrides }; + + return { + id: "socket-1", + data, + to: vi.fn(() => toReturn), + } as unknown as AppSocket; +} + +describe("handleDocumentUpdate", () => { + const fakeAuthService = { + verifyCollaborationToken: vi.fn(), + }; + const fakeSessionManager = { + getRuntimeSession: vi.fn(), + }; + const fakeMongoDBStore = { + createUpdate: vi.fn(), + }; + + const deps: SocketHandlerDeps = { + authService: fakeAuthService as any, + sessionManager: fakeSessionManager as any, + mongodbStore: fakeMongoDBStore as any, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns 401 when socket is not authenticated", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(undefined, { authenticated: false }); + const fakeArgs: DocumentUpdateArgs = { + documentId: "doc-1", + data: "update-data", + collaborationToken: "token", + }; + const callback = vi.fn(); + + await handleDocumentUpdate(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Not authenticated or session not found", + }); + }); + + it("returns 400 when data is missing", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: DocumentUpdateArgs = { + documentId: "doc-1", + data: "" as any, + collaborationToken: "token", + }; + const callback = vi.fn(); + + await handleDocumentUpdate(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 400, + error: "Update data is required", + }); + }); + + it("returns 404 when runtime session is not found", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: DocumentUpdateArgs = { + documentId: "doc-1", + data: "update-data", + collaborationToken: "token", + }; + const callback = vi.fn(); + + fakeSessionManager.getRuntimeSession.mockResolvedValue(undefined); + + await handleDocumentUpdate(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeSessionManager.getRuntimeSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeSocket.data.sessionDid + ); + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 404, + error: "Session not found", + }); + }); + + it("returns 401 when collaboration token verification fails", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: DocumentUpdateArgs = { + documentId: "doc-1", + data: "update-data", + collaborationToken: "token", + }; + const callback = vi.fn(); + + const runtimeSession = { sessionDid: fakeSocket.data.sessionDid }; + fakeSessionManager.getRuntimeSession.mockResolvedValue(runtimeSession); + fakeAuthService.verifyCollaborationToken.mockResolvedValue(false); + + await handleDocumentUpdate(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeAuthService.verifyCollaborationToken).toHaveBeenCalledWith( + fakeArgs.collaborationToken, + runtimeSession.sessionDid, + fakeArgs.documentId + ); + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Authentication failed", + }); + }); + + it("creates update and broadcasts when all checks pass", async () => { + const fakeIO = createFakeIO(); + const fakeBroadcastOperator = { emit: vi.fn() }; + const fakeSocket = createFakeSocket(fakeBroadcastOperator); + const fakeArgs: DocumentUpdateArgs = { + documentId: "doc-1", + data: "update-data", + collaborationToken: "token", + }; + const callback = vi.fn(); + + const runtimeSession = { sessionDid: fakeSocket.data.sessionDid }; + fakeSessionManager.getRuntimeSession.mockResolvedValue(runtimeSession); + fakeAuthService.verifyCollaborationToken.mockResolvedValue(true); + + const fakeUpdate = { + id: "update-id", + documentId: fakeArgs.documentId, + data: fakeArgs.data, + updateType: "yjs_update", + committed: false, + commitCid: null, + createdAt: 123456, + sessionDid: runtimeSession.sessionDid, + }; + fakeMongoDBStore.createUpdate.mockResolvedValue(fakeUpdate); + + await handleDocumentUpdate(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeMongoDBStore.createUpdate).toHaveBeenCalled(); + + const roomName = getRoomName(fakeArgs.documentId!, fakeSocket.data.sessionDid); + expect(fakeSocket.to).toHaveBeenCalledWith(roomName); + expect(fakeBroadcastOperator.emit).toHaveBeenCalledWith("/document/content_update", { + id: fakeUpdate.id, + data: fakeUpdate.data, + createdAt: fakeUpdate.createdAt, + roomId: fakeArgs.documentId, + }); + + expect(callback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + id: fakeUpdate.id, + documentId: fakeUpdate.documentId, + data: fakeUpdate.data, + updateType: fakeUpdate.updateType, + commitCid: fakeUpdate.commitCid, + createdAt: fakeUpdate.createdAt, + }, + }); + }); +} +); + From b38822ec86b6ec7c30f6a161dd744f7ede4d95ed Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Thu, 5 Mar 2026 12:13:45 +0530 Subject: [PATCH 11/24] chore: adds test for document commit handler --- ...cket-handlers.handleDocumentCommit.test.ts | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 src/services/socket-handlers.handleDocumentCommit.test.ts diff --git a/src/services/socket-handlers.handleDocumentCommit.test.ts b/src/services/socket-handlers.handleDocumentCommit.test.ts new file mode 100644 index 0000000..d7fdcdb --- /dev/null +++ b/src/services/socket-handlers.handleDocumentCommit.test.ts @@ -0,0 +1,234 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { handleDocumentCommit } from "./socket-handlers"; +import type { AppSocket, DocumentCommitArgs } from "../types"; +import type { SocketHandlerDeps } from "./socket-handlers.deps"; + +function createFakeSocket( + dataOverrides?: Partial<{ + authenticated: boolean; + documentId: string; + sessionDid: string; + role: "owner" | "editor"; + }> +): AppSocket { + const defaultData = { + authenticated: true, + documentId: "test-document-id", + sessionDid: "test-session-did", + role: "owner" as const, + }; + const data = { ...defaultData, ...dataOverrides }; + + return { + id: "socket-1", + data, + } as unknown as AppSocket; +} + +describe("handleDocumentCommit", () => { + const fakeAuthService = { + verifyOwnerToken: vi.fn(), + }; + const fakeSessionManager = { + getRuntimeSession: vi.fn(), + }; + const fakeMongoDBStore = { + createCommit: vi.fn(), + }; + + const deps: SocketHandlerDeps = { + authService: fakeAuthService as any, + sessionManager: fakeSessionManager as any, + mongodbStore: fakeMongoDBStore as any, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns 401 when socket is not authenticated", async () => { + const fakeSocket = createFakeSocket({ authenticated: false }); + const fakeArgs: DocumentCommitArgs = { + documentId: "doc-1", + updates: ["u1"], + cid: "cid-1", + ownerToken: "owner-token", + ownerAddress: "0xowner", + contractAddress: "0xcontract", + }; + const callback = vi.fn(); + + await handleDocumentCommit(deps, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Not authenticated or session not found", + }); + }); + + it("returns 403 when socket role is not owner", async () => { + const fakeSocket = createFakeSocket({ role: "editor" }); + const fakeArgs: DocumentCommitArgs = { + documentId: "doc-1", + updates: ["u1"], + cid: "cid-1", + ownerToken: "owner-token", + ownerAddress: "0xowner", + contractAddress: "0xcontract", + }; + const callback = vi.fn(); + + await handleDocumentCommit(deps, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 403, + error: "Only owners can create commits", + }); + }); + + it("returns 404 when runtime session is not found", async () => { + const fakeSocket = createFakeSocket(); + const fakeArgs: DocumentCommitArgs = { + documentId: "doc-1", + updates: ["u1"], + cid: "cid-1", + ownerToken: "owner-token", + ownerAddress: "0xowner", + contractAddress: "0xcontract", + }; + const callback = vi.fn(); + + fakeSessionManager.getRuntimeSession.mockResolvedValue(undefined); + + await handleDocumentCommit(deps, fakeSocket, fakeArgs, callback); + + expect(fakeSessionManager.getRuntimeSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeSocket.data.sessionDid + ); + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 404, + error: "Session not found", + }); + }); + + it("returns 400 when updates or cid are missing or invalid", async () => { + const fakeSocket = createFakeSocket(); + const callback = vi.fn(); + + const badArgsList: DocumentCommitArgs[] = [ + // @ts-expect-error testing missing required fields (updates, cid, owner fields) + { + documentId: "doc-1", + }, + { + documentId: "doc-1", + // deliberately non-array to test validation + updates: null as any, + cid: "cid-1", + ownerToken: "owner-token", + ownerAddress: "0xowner", + contractAddress: "0xcontract", + }, + { + documentId: "doc-1", + updates: ["u1"], + cid: "" as any, + ownerToken: "owner-token", + ownerAddress: "0xowner", + contractAddress: "0xcontract", + }, + ]; + + for (const badArgs of badArgsList) { + const runtimeSession = { sessionDid: fakeSocket.data.sessionDid }; + fakeSessionManager.getRuntimeSession.mockResolvedValue(runtimeSession); + fakeAuthService.verifyOwnerToken.mockResolvedValue(true); + + await handleDocumentCommit(deps, fakeSocket, badArgs as any, callback); + + expect(callback).toHaveBeenLastCalledWith({ + status: false, + statusCode: 400, + error: "Updates array and CID are required", + }); + } + }); + + it("returns 401 when owner token verification fails", async () => { + const fakeSocket = createFakeSocket(); + const fakeArgs: DocumentCommitArgs = { + documentId: "doc-1", + updates: ["u1"], + cid: "cid-1", + ownerToken: "owner-token", + ownerAddress: "0xowner", + contractAddress: "0xcontract", + }; + const callback = vi.fn(); + + const runtimeSession = { sessionDid: fakeSocket.data.sessionDid }; + fakeSessionManager.getRuntimeSession.mockResolvedValue(runtimeSession); + fakeAuthService.verifyOwnerToken.mockResolvedValue(false); + + await handleDocumentCommit(deps, fakeSocket, fakeArgs, callback); + + expect(fakeAuthService.verifyOwnerToken).toHaveBeenCalledWith( + fakeArgs.ownerToken, + fakeArgs.contractAddress, + fakeArgs.ownerAddress + ); + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Authentication failed", + }); + }); + + it("creates commit when all checks pass", async () => { + const fakeSocket = createFakeSocket(); + const fakeArgs: DocumentCommitArgs = { + documentId: "doc-1", + updates: ["u1", "u2"], + cid: "cid-1", + ownerToken: "owner-token", + ownerAddress: "0xowner", + contractAddress: "0xcontract", + }; + const callback = vi.fn(); + + const runtimeSession = { sessionDid: fakeSocket.data.sessionDid }; + fakeSessionManager.getRuntimeSession.mockResolvedValue(runtimeSession); + fakeAuthService.verifyOwnerToken.mockResolvedValue(true); + + const fakeCommit = { + id: "commit-id", + documentId: fakeArgs.documentId, + cid: fakeArgs.cid, + updates: fakeArgs.updates, + createdAt: 123456, + sessionDid: runtimeSession.sessionDid, + }; + fakeMongoDBStore.createCommit.mockResolvedValue(fakeCommit); + + await handleDocumentCommit(deps, fakeSocket, fakeArgs, callback); + + expect(fakeMongoDBStore.createCommit).toHaveBeenCalled(); + + expect(callback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + cid: fakeCommit.cid, + createdAt: fakeCommit.createdAt, + documentId: fakeCommit.documentId, + updates: fakeCommit.updates, + }, + }); + }); +} +); + From 27750cfb47a5300ed86ddd45953898db02fb1133 Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Thu, 5 Mar 2026 12:20:03 +0530 Subject: [PATCH 12/24] chore: adds missing dependency for generating coverage --- package-lock.json | 306 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + 2 files changed, 308 insertions(+) diff --git a/package-lock.json b/package-lock.json index 09aeb2c..5a74967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^8.6.0", "@typescript-eslint/parser": "^8.6.0", + "@vitest/coverage-v8": "^1.6.1", "eslint": "^9.11.0", "eslint-config-prettier": "^9.1.0", "prettier": "^3.3.3", @@ -61,6 +62,77 @@ "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", "license": "MIT" }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, "node_modules/@chainsafe/as-chacha20poly1305": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@chainsafe/as-chacha20poly1305/-/as-chacha20poly1305-0.1.0.tgz", @@ -883,6 +955,16 @@ "node": ">=12" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -2325,6 +2407,34 @@ "node": ">=15" } }, + "node_modules/@vitest/coverage-v8": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz", + "integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.4", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.3", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "test-exclude": "^6.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "1.6.1" + } + }, "node_modules/@vitest/expect": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", @@ -4156,6 +4266,13 @@ "node": ">= 0.6" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4377,6 +4494,13 @@ "resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz", "integrity": "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==" }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -4472,6 +4596,18 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4650,6 +4786,60 @@ "ws": "*" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/it-all": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/it-all/-/it-all-3.0.9.tgz", @@ -5162,12 +5352,40 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, "node_modules/main-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/main-event/-/main-event-1.0.1.tgz", "integrity": "sha512-NWtdGrAca/69fm6DIVd8T9rtfDII4Q8NQbIbsKQq2VzS9eqOGYs8uaNQjcuaCq/d9H/o625aOTJX2Qoxzqw0Pw==", "license": "Apache-2.0 OR MIT" }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5639,6 +5857,16 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/one-webcrypto": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/one-webcrypto/-/one-webcrypto-1.0.3.tgz", @@ -5851,6 +6079,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6901,6 +7139,67 @@ "node": ">=8" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -8151,6 +8450,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", diff --git a/package.json b/package.json index e158811..d407d14 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "format": "prettier --write .", "lint": "eslint src/**/*.ts", "test": "vitest", + "test:coverage": "vitest run --coverage", "postinstall": "npm run build", "heroku-postbuild": "npm run build" }, @@ -65,6 +66,7 @@ "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^8.6.0", "@typescript-eslint/parser": "^8.6.0", + "@vitest/coverage-v8": "^1.6.1", "eslint": "^9.11.0", "eslint-config-prettier": "^9.1.0", "prettier": "^3.3.3", From 2b12c5436cd1ae8d0970acfec7be765eefbf8f0d Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Thu, 5 Mar 2026 13:04:09 +0530 Subject: [PATCH 13/24] chore: adds additional test in auth handler to cover few failure cases --- .../socket-handlers.handleAuth.test.ts | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/src/services/socket-handlers.handleAuth.test.ts b/src/services/socket-handlers.handleAuth.test.ts index 5b45808..0aa1088 100644 --- a/src/services/socket-handlers.handleAuth.test.ts +++ b/src/services/socket-handlers.handleAuth.test.ts @@ -260,5 +260,156 @@ describe("handleAuth", () => { error: "Session not found", }); }); + + it("returns 401 when owner token verification fails in owner flow", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + ownerToken: "owner-token", + ownerAddress: "0xowner", + contractAddress: "0xcontract", + }; + const callback = vi.fn(); + + fakeSessionManager.getSession.mockResolvedValue(undefined); + fakeAuthService.verifyOwnerToken.mockResolvedValue(null); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeSessionManager.getSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeArgs.sessionDid + ); + expect(fakeAuthService.verifyOwnerToken).toHaveBeenCalledWith( + fakeArgs.ownerToken, + fakeArgs.contractAddress, + fakeArgs.ownerAddress + ); + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Authentication failed", + }); + }); + + it("returns 401 when collaboration token verification fails for existing session", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + }; + const callback = vi.fn(); + + const existingSession = { + sessionDid: fakeArgs.sessionDid, + ownerDid: "owner-did", + roomInfo: "existing-room-info", + }; + + fakeSessionManager.getSession.mockResolvedValue(existingSession); + fakeAuthService.verifyCollaborationToken.mockResolvedValue(null); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeAuthService.verifyCollaborationToken).toHaveBeenCalledWith( + fakeArgs.collaborationToken, + existingSession.sessionDid, + fakeArgs.documentId + ); + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 401, + error: "Authentication failed", + }); + }); + + it("joins existing session as owner and updates room info when owner token matches", async () => { + const fakeIO = createFakeIO(); + const fakeBroadcastOperator = { emit: vi.fn() }; + const fakeSocket = createFakeSocket(fakeBroadcastOperator); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + ownerToken: "owner-token", + ownerAddress: "0xowner", + contractAddress: "0xcontract", + roomInfo: "new-room-info", + }; + const callback = vi.fn(); + + const existingSession = { + sessionDid: fakeArgs.sessionDid, + ownerDid: "owner-did", + roomInfo: "existing-room-info", + }; + + fakeSessionManager.getSession.mockResolvedValue(existingSession); + fakeAuthService.verifyCollaborationToken.mockResolvedValue("user-did"); + fakeAuthService.verifyOwnerToken.mockResolvedValue("owner-did"); + fakeSessionManager.updateRoomInfo.mockResolvedValue(undefined); + fakeSessionManager.addClientToSession.mockResolvedValue(undefined); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + const roomName = getRoomName(fakeArgs.documentId, fakeArgs.sessionDid); + + expect(fakeAuthService.verifyOwnerToken).toHaveBeenCalledWith( + fakeArgs.ownerToken, + fakeArgs.contractAddress, + fakeArgs.ownerAddress + ); + expect(fakeSessionManager.updateRoomInfo).toHaveBeenCalledWith( + fakeArgs.documentId, + existingSession.sessionDid, + existingSession.ownerDid, + fakeArgs.roomInfo + ); + + expect(fakeSocket.data.role).toBe("owner"); + expect(fakeSocket.join).toHaveBeenCalledWith(roomName); + expect(fakeBroadcastOperator.emit).toHaveBeenCalledWith("/room/membership_change", { + action: "user_joined", + user: { role: "owner" }, + roomId: fakeArgs.documentId, + }); + + expect(callback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + message: "Authentication successful", + role: "owner", + sessionType: "existing", + roomInfo: existingSession.roomInfo, + }, + }); + }); + + it("returns 500 when an unexpected error occurs in auth handler", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + }; + const callback = vi.fn(); + + fakeSessionManager.getSession.mockRejectedValue(new Error("db error")); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 500, + error: "Internal server error", + }); + }); }); From 8b7fba5b3e128f5e17ce496e39d63c8ed4ce6ddd Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Mon, 9 Mar 2026 12:56:54 +0530 Subject: [PATCH 14/24] chore(unit-tests): fixes breaking unit-tests --- src/services/session-manager.ts | 14 -- .../socket-handlers.handleAuth.test.ts | 177 ++++++++++++++++-- ...ocket-handlers.handleCommitHistory.test.ts | 62 +++--- ...cket-handlers.handleDocumentCommit.test.ts | 25 ++- ...cket-handlers.handleDocumentUpdate.test.ts | 4 + .../socket-handlers.handlePeersList.test.ts | 4 + ...ocket-handlers.handleUpdateHistory.test.ts | 118 ++++++++---- .../socket-handlers.terminateSession.test.ts | 36 ++-- 8 files changed, 322 insertions(+), 118 deletions(-) diff --git a/src/services/session-manager.ts b/src/services/session-manager.ts index 5174d56..d9a0485 100644 --- a/src/services/session-manager.ts +++ b/src/services/session-manager.ts @@ -191,20 +191,6 @@ export class SessionManager { } } - async terminateOtherExistingSessions(documentId: string, ownerDid: string): Promise { - try { - const existingSessions = await SessionModel.find({ documentId, ownerDid, state: "active" }); - for (const session of existingSessions) { - await this.terminateSession(documentId, session.sessionDid); - console.log( - `[SessionManager] Terminated session: ${session.sessionDid} for document: ${documentId}` - ); - } - } catch (error) { - console.error("Error terminating existing sessions:", error); - } - } - async getActiveSessionsCount(): Promise { const inMemoryCount = this.inMemorySessions.size; if (inMemoryCount > 0) { diff --git a/src/services/socket-handlers.handleAuth.test.ts b/src/services/socket-handlers.handleAuth.test.ts index 0aa1088..ac7fd1b 100644 --- a/src/services/socket-handlers.handleAuth.test.ts +++ b/src/services/socket-handlers.handleAuth.test.ts @@ -1,11 +1,18 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { handleAuth, getRoomName } from "./socket-handlers"; -import type { AppServer, AppSocket, AuthArgs } from "../types"; +import { AppServer, AppSocket, AuthArgs, ErrorCode } from "../types"; import type { SocketHandlerDeps } from "./socket-handlers.deps"; -function createFakeIO(): AppServer { +function createFakeIO(options?: { + broadcastOperator?: { emit: ReturnType }; + fetchSockets?: ReturnType; +}): AppServer { + const roomBroadcastOperator = options?.broadcastOperator ?? { emit: vi.fn() }; + const fetchSockets = options?.fetchSockets ?? vi.fn().mockResolvedValue([]); + return { - in: vi.fn(), + to: vi.fn(() => roomBroadcastOperator), + in: vi.fn(() => ({ fetchSockets })), } as unknown as AppServer; } @@ -43,7 +50,8 @@ describe("handleAuth", () => { }; const fakeSessionManager = { getSession: vi.fn(), - terminateOtherExistingSessions: vi.fn(), + getOtherActiveSessions: vi.fn(), + terminateSession: vi.fn(), createSession: vi.fn(), updateRoomInfo: vi.fn(), addClientToSession: vi.fn(), @@ -76,6 +84,7 @@ describe("handleAuth", () => { status: false, statusCode: 400, error: "Collaboration token is required", + errorCode: ErrorCode.AUTH_TOKEN_MISSING, }); }); @@ -95,6 +104,7 @@ describe("handleAuth", () => { status: false, statusCode: 400, error: "Document ID is required", + errorCode: ErrorCode.DOCUMENT_ID_MISSING, }); }); @@ -114,6 +124,7 @@ describe("handleAuth", () => { status: false, statusCode: 400, error: "Session DID is required", + errorCode: ErrorCode.SESSION_DID_MISSING, }); }); @@ -126,15 +137,15 @@ describe("handleAuth", () => { sessionDid: "session-1", collaborationToken: "collab-token", ownerToken: "owner-token", - ownerAddress: "0xowner", - contractAddress: "0xcontract", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", roomInfo: "room-info", }; const callback = vi.fn(); fakeSessionManager.getSession.mockResolvedValue(undefined); fakeAuthService.verifyOwnerToken.mockResolvedValue("owner-did"); - fakeSessionManager.terminateOtherExistingSessions.mockResolvedValue(undefined); + fakeSessionManager.getOtherActiveSessions.mockResolvedValue([]); fakeSessionManager.createSession.mockResolvedValue(undefined); fakeSessionManager.addClientToSession.mockResolvedValue(undefined); @@ -149,9 +160,10 @@ describe("handleAuth", () => { fakeArgs.contractAddress, fakeArgs.ownerAddress ); - expect(fakeSessionManager.terminateOtherExistingSessions).toHaveBeenCalledWith( + expect(fakeSessionManager.getOtherActiveSessions).toHaveBeenCalledWith( fakeArgs.documentId, - "owner-did" + "owner-did", + fakeArgs.sessionDid, ); expect(fakeSessionManager.createSession).toHaveBeenCalledWith({ documentId: fakeArgs.documentId, @@ -191,6 +203,141 @@ describe("handleAuth", () => { }); }); + it("creates a new owner session and terminates other active sessions when they exist", async () => { + const fakeRoomBroadcastOperator = { emit: vi.fn() }; + const fetchSockets = vi.fn(); + const fakeIO = createFakeIO({ broadcastOperator: fakeRoomBroadcastOperator, fetchSockets }); + + const fakeBroadcastOperator = { emit: vi.fn() }; + const fakeSocket = createFakeSocket(fakeBroadcastOperator); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + ownerToken: "owner-token", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", + roomInfo: "room-info", + }; + const callback = vi.fn(); + + const otherSessions = [ + { documentId: fakeArgs.documentId, sessionDid: "old-session-1" }, + { documentId: fakeArgs.documentId, sessionDid: "old-session-2" }, + ]; + + const oldRoomName1 = getRoomName(fakeArgs.documentId, otherSessions[0].sessionDid); + const oldRoomName2 = getRoomName(fakeArgs.documentId, otherSessions[1].sessionDid); + const oldSocket1 = createFakeSocket(undefined, { authenticated: true }) as any; + const oldSocket2 = createFakeSocket(undefined, { authenticated: true }) as any; + oldSocket1.leave = vi.fn(); + oldSocket2.leave = vi.fn(); + + fetchSockets + .mockResolvedValueOnce([oldSocket1]) + .mockResolvedValueOnce([oldSocket2]); + + fakeSessionManager.getSession.mockResolvedValue(undefined); + fakeAuthService.verifyOwnerToken.mockResolvedValue("owner-did"); + fakeSessionManager.getOtherActiveSessions.mockResolvedValue(otherSessions); + fakeSessionManager.terminateSession.mockResolvedValue(undefined); + fakeSessionManager.createSession.mockResolvedValue(undefined); + fakeSessionManager.addClientToSession.mockResolvedValue(undefined); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + // Pre-loop checks (sequence before termination loop) + expect(fakeSessionManager.getSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeArgs.sessionDid + ); + expect(fakeAuthService.verifyOwnerToken).toHaveBeenCalledWith( + fakeArgs.ownerToken, + fakeArgs.contractAddress, + fakeArgs.ownerAddress + ); + expect(fakeSessionManager.getOtherActiveSessions).toHaveBeenCalledWith( + fakeArgs.documentId, + "owner-did", + fakeArgs.sessionDid, + ); + + // First other session (old-session-1) + expect(fakeIO.to).toHaveBeenCalledWith(oldRoomName1); + expect(fakeRoomBroadcastOperator.emit).toHaveBeenCalledWith("/server/error", { + errorCode: ErrorCode.SESSION_TERMINATED, + message: "Session terminated by owner creating a new session", + roomId: otherSessions[0].documentId, + }); + expect(fakeIO.to).toHaveBeenCalledWith(oldRoomName1); + expect(fakeRoomBroadcastOperator.emit).toHaveBeenCalledWith("/session/terminated", { + roomId: otherSessions[0].documentId, + }); + expect(fakeIO.in).toHaveBeenCalledWith(oldRoomName1); + expect(fetchSockets).toHaveBeenCalledWith(); + expect(oldSocket1.data.authenticated).toBe(false); + expect(oldSocket1.leave).toHaveBeenCalledWith(oldRoomName1); + expect(fakeSessionManager.terminateSession).toHaveBeenCalledWith( + otherSessions[0].documentId, + otherSessions[0].sessionDid + ); + + // Second other session (old-session-2) + expect(fakeIO.to).toHaveBeenCalledWith(oldRoomName2); + expect(fakeRoomBroadcastOperator.emit).toHaveBeenCalledWith("/server/error", { + errorCode: ErrorCode.SESSION_TERMINATED, + message: "Session terminated by owner creating a new session", + roomId: otherSessions[1].documentId, + }); + expect(fakeIO.to).toHaveBeenCalledWith(oldRoomName2); + expect(fakeRoomBroadcastOperator.emit).toHaveBeenCalledWith("/session/terminated", { + roomId: otherSessions[1].documentId, + }); + expect(fakeIO.in).toHaveBeenCalledWith(oldRoomName2); + expect(fetchSockets).toHaveBeenCalledWith(); + expect(oldSocket2.data.authenticated).toBe(false); + expect(oldSocket2.leave).toHaveBeenCalledWith(oldRoomName2); + expect(fakeSessionManager.terminateSession).toHaveBeenCalledWith( + otherSessions[1].documentId, + otherSessions[1].sessionDid + ); + + // Aggregate call-count assertions after verifying per-iteration sequence + // Outside if-else block + expect(fetchSockets).toHaveBeenCalledTimes(otherSessions.length); + expect(fakeSessionManager.terminateSession).toHaveBeenCalledTimes(otherSessions.length); + + expect(fakeSocket.data.authenticated).toBe(true); + expect(fakeSocket.data.documentId).toBe(fakeArgs.documentId); + expect(fakeSocket.data.sessionDid).toBe(fakeArgs.sessionDid); + expect(fakeSocket.data.role).toBe("owner"); + + const roomName = getRoomName(fakeArgs.documentId, fakeArgs.sessionDid); + expect(fakeSocket.join).toHaveBeenCalledWith(roomName); + expect(fakeSessionManager.addClientToSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeArgs.sessionDid, + fakeSocket.id + ); + expect(fakeSocket.to).toHaveBeenCalledWith(roomName); + expect(fakeBroadcastOperator.emit).toHaveBeenCalledWith("/room/membership_change", { + action: "user_joined", + user: { role: "owner" }, + roomId: fakeArgs.documentId, + }); + + expect(callback).toHaveBeenCalledWith({ + status: true, + statusCode: 200, + data: { + message: "Authentication successful", + role: "owner", + sessionType: "new", + roomInfo: fakeArgs.roomInfo, + }, + }); + }); + it("joins an existing session as editor when collaboration token is valid", async () => { const fakeIO = createFakeIO(); const fakeBroadcastOperator = { emit: vi.fn() }; @@ -258,6 +405,7 @@ describe("handleAuth", () => { status: false, statusCode: 404, error: "Session not found", + errorCode: ErrorCode.SESSION_NOT_FOUND, }); }); @@ -269,8 +417,8 @@ describe("handleAuth", () => { sessionDid: "session-1", collaborationToken: "collab-token", ownerToken: "owner-token", - ownerAddress: "0xowner", - contractAddress: "0xcontract", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", }; const callback = vi.fn(); @@ -292,6 +440,7 @@ describe("handleAuth", () => { status: false, statusCode: 401, error: "Authentication failed", + errorCode: ErrorCode.AUTH_TOKEN_INVALID, }); }); @@ -325,6 +474,7 @@ describe("handleAuth", () => { status: false, statusCode: 401, error: "Authentication failed", + errorCode: ErrorCode.AUTH_TOKEN_INVALID, }); }); @@ -337,8 +487,8 @@ describe("handleAuth", () => { sessionDid: "session-1", collaborationToken: "collab-token", ownerToken: "owner-token", - ownerAddress: "0xowner", - contractAddress: "0xcontract", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", roomInfo: "new-room-info", }; const callback = vi.fn(); @@ -409,6 +559,7 @@ describe("handleAuth", () => { status: false, statusCode: 500, error: "Internal server error", + errorCode: ErrorCode.INTERNAL_ERROR, }); }); }); diff --git a/src/services/socket-handlers.handleCommitHistory.test.ts b/src/services/socket-handlers.handleCommitHistory.test.ts index 2dd8917..c51135e 100644 --- a/src/services/socket-handlers.handleCommitHistory.test.ts +++ b/src/services/socket-handlers.handleCommitHistory.test.ts @@ -4,7 +4,7 @@ import type { SocketHandlerDeps } from "./socket-handlers.deps"; import type { AppSocket, DocumentCommit, CommitHistoryArgs } from "../types"; function createFakeSocket( - broadcastOperator?: { emit: ReturnType}, + broadcastOperator?: { emit: ReturnType }, dataOverrides?: Partial<{ authenticated: boolean; documentId: string; @@ -35,6 +35,7 @@ describe("commitHistory", () => { const fakeMongodbStore = { getCommitsByDocument: vi.fn(), + countCommitsByDocument: vi.fn(), }; const deps: SocketHandlerDeps = { @@ -43,7 +44,7 @@ describe("commitHistory", () => { mongodbStore: fakeMongodbStore as any, }; - it('returns early when not authenticated', async () => { + it("returns early when not authenticated", async () => { const fakeSocket: AppSocket = createFakeSocket(undefined, { authenticated: false }); const fakeArgs: CommitHistoryArgs = { documentId: "test-document-id", @@ -56,10 +57,11 @@ describe("commitHistory", () => { status: false, statusCode: 401, error: "Not authenticated", + errorCode: "NOT_AUTHENTICATED", }); }); - it('returns early when documentId is empty in socket data', async () => { + it("returns early when documentId is empty in socket data", async () => { const fakeSocket: AppSocket = createFakeSocket(undefined, { documentId: "" }); const fakeArgs: CommitHistoryArgs = {}; const fakeCallback = vi.fn(); @@ -70,10 +72,11 @@ describe("commitHistory", () => { status: false, statusCode: 401, error: "Not authenticated", + errorCode: "NOT_AUTHENTICATED", }); }); - it('returns early when sessionDid is empty in socket data', async () => { + it("returns early when sessionDid is empty in socket data", async () => { const fakeSocket: AppSocket = createFakeSocket(undefined, { sessionDid: "" }); const fakeArgs: CommitHistoryArgs = { documentId: "test-document-id", @@ -86,10 +89,11 @@ describe("commitHistory", () => { status: false, statusCode: 401, error: "Not authenticated", + errorCode: "NOT_AUTHENTICATED", }); }); - it('returns commit history successfully, with fallback argument values', async () => { + it("returns commit history successfully, with fallback argument values", async () => { const fakeSocket: AppSocket = createFakeSocket(); const fakeArgs: CommitHistoryArgs = {}; const fakeCallback = vi.fn(); @@ -97,15 +101,17 @@ describe("commitHistory", () => { const documentId = fakeArgs.documentId || fakeSocket.data.documentId; fakeMongodbStore.getCommitsByDocument.mockResolvedValue(fakeResponse); + fakeMongodbStore.countCommitsByDocument.mockResolvedValue(fakeResponse.length); await handleCommitHistory(deps, fakeSocket, fakeArgs, fakeCallback); - expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalled(); - expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalledWith({ + expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalledWith( + { documentId, sessionDid: fakeSocket.data.sessionDid }, + { offset: 0, limit: 10, sort: "desc" } + ); + expect(fakeMongodbStore.countCommitsByDocument).toHaveBeenCalledWith({ documentId, sessionDid: fakeSocket.data.sessionDid, - }, { - offset: 0, limit: 10, sort: "desc" }); expect(fakeCallback).toHaveBeenCalledWith({ @@ -118,7 +124,7 @@ describe("commitHistory", () => { }); }); - it('returns update history successfully with proper argument values set', async () => { + it("returns update history successfully with proper argument values set", async () => { const fakeSocket: AppSocket = createFakeSocket(); const fakeArgs: CommitHistoryArgs = { documentId: "test-document-id", @@ -127,21 +133,21 @@ describe("commitHistory", () => { sort: "desc", }; const fakeCallback = vi.fn(); - const fakeResponse: DocumentCommit[] = [ - // {} - ]; + const fakeResponse: DocumentCommit[] = []; const documentId = fakeArgs.documentId || fakeSocket.data.documentId; fakeMongodbStore.getCommitsByDocument.mockResolvedValue(fakeResponse); + fakeMongodbStore.countCommitsByDocument.mockResolvedValue(fakeResponse.length); await handleCommitHistory(deps, fakeSocket, fakeArgs, fakeCallback); - expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalled(); - expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalledWith({ + expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalledWith( + { documentId, sessionDid: fakeSocket.data.sessionDid }, + { offset: fakeArgs.offset, limit: fakeArgs.limit, sort: fakeArgs.sort } + ); + expect(fakeMongodbStore.countCommitsByDocument).toHaveBeenCalledWith({ documentId, sessionDid: fakeSocket.data.sessionDid, - }, { - offset: fakeArgs.offset, limit: fakeArgs.limit, sort: fakeArgs.sort, }); expect(fakeCallback).toHaveBeenCalledWith({ @@ -154,7 +160,7 @@ describe("commitHistory", () => { }); }); - it('returns 500 due to db operation error', async () => { + it("returns 500 due to db operation error", async () => { const fakeSocket: AppSocket = createFakeSocket(); const fakeArgs: CommitHistoryArgs = { documentId: "test-document-id", @@ -166,21 +172,27 @@ describe("commitHistory", () => { const documentId = fakeArgs.documentId || fakeSocket.data.documentId; fakeMongodbStore.getCommitsByDocument.mockRejectedValue(new Error("db error")); + fakeMongodbStore.countCommitsByDocument.mockResolvedValue(0); await handleCommitHistory(deps, fakeSocket, fakeArgs, fakeCallback); - expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalled(); - expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalledWith({ - documentId, - sessionDid: fakeSocket.data.sessionDid, - }, { - offset: fakeArgs.offset, limit: fakeArgs.limit, sort: fakeArgs.sort, - }); + expect(fakeMongodbStore.getCommitsByDocument).toHaveBeenCalledWith( + { + documentId, + sessionDid: fakeSocket.data.sessionDid, + }, + { + offset: fakeArgs.offset, + limit: fakeArgs.limit, + sort: fakeArgs.sort, + } + ); expect(fakeCallback).toHaveBeenCalledWith({ status: false, statusCode: 500, error: "Internal server error", + errorCode: "INTERNAL_ERROR", }); }); }); \ No newline at end of file diff --git a/src/services/socket-handlers.handleDocumentCommit.test.ts b/src/services/socket-handlers.handleDocumentCommit.test.ts index d7fdcdb..db6cb77 100644 --- a/src/services/socket-handlers.handleDocumentCommit.test.ts +++ b/src/services/socket-handlers.handleDocumentCommit.test.ts @@ -64,6 +64,7 @@ describe("handleDocumentCommit", () => { status: false, statusCode: 401, error: "Not authenticated or session not found", + errorCode: "NOT_AUTHENTICATED", }); }); @@ -85,6 +86,7 @@ describe("handleDocumentCommit", () => { status: false, statusCode: 403, error: "Only owners can create commits", + errorCode: "COMMIT_UNAUTHORIZED", }); }); @@ -95,8 +97,8 @@ describe("handleDocumentCommit", () => { updates: ["u1"], cid: "cid-1", ownerToken: "owner-token", - ownerAddress: "0xowner", - contractAddress: "0xcontract", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", }; const callback = vi.fn(); @@ -112,6 +114,7 @@ describe("handleDocumentCommit", () => { status: false, statusCode: 404, error: "Session not found", + errorCode: "SESSION_NOT_FOUND", }); }); @@ -130,16 +133,16 @@ describe("handleDocumentCommit", () => { updates: null as any, cid: "cid-1", ownerToken: "owner-token", - ownerAddress: "0xowner", - contractAddress: "0xcontract", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", }, { documentId: "doc-1", updates: ["u1"], cid: "" as any, ownerToken: "owner-token", - ownerAddress: "0xowner", - contractAddress: "0xcontract", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", }, ]; @@ -154,6 +157,7 @@ describe("handleDocumentCommit", () => { status: false, statusCode: 400, error: "Updates array and CID are required", + errorCode: "COMMIT_MISSING_DATA", }); } }); @@ -165,8 +169,8 @@ describe("handleDocumentCommit", () => { updates: ["u1"], cid: "cid-1", ownerToken: "owner-token", - ownerAddress: "0xowner", - contractAddress: "0xcontract", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", }; const callback = vi.fn(); @@ -185,6 +189,7 @@ describe("handleDocumentCommit", () => { status: false, statusCode: 401, error: "Authentication failed", + errorCode: "AUTH_TOKEN_INVALID", }); }); @@ -195,8 +200,8 @@ describe("handleDocumentCommit", () => { updates: ["u1", "u2"], cid: "cid-1", ownerToken: "owner-token", - ownerAddress: "0xowner", - contractAddress: "0xcontract", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", }; const callback = vi.fn(); diff --git a/src/services/socket-handlers.handleDocumentUpdate.test.ts b/src/services/socket-handlers.handleDocumentUpdate.test.ts index 510142f..87ca71e 100644 --- a/src/services/socket-handlers.handleDocumentUpdate.test.ts +++ b/src/services/socket-handlers.handleDocumentUpdate.test.ts @@ -69,6 +69,7 @@ describe("handleDocumentUpdate", () => { status: false, statusCode: 401, error: "Not authenticated or session not found", + errorCode: "NOT_AUTHENTICATED", }); }); @@ -88,6 +89,7 @@ describe("handleDocumentUpdate", () => { status: false, statusCode: 400, error: "Update data is required", + errorCode: "UPDATE_DATA_MISSING", }); }); @@ -113,6 +115,7 @@ describe("handleDocumentUpdate", () => { status: false, statusCode: 404, error: "Session not found", + errorCode: "SESSION_NOT_FOUND", }); }); @@ -141,6 +144,7 @@ describe("handleDocumentUpdate", () => { status: false, statusCode: 401, error: "Authentication failed", + errorCode: "AUTH_TOKEN_INVALID", }); }); diff --git a/src/services/socket-handlers.handlePeersList.test.ts b/src/services/socket-handlers.handlePeersList.test.ts index 49e7bfe..dd762e5 100644 --- a/src/services/socket-handlers.handlePeersList.test.ts +++ b/src/services/socket-handlers.handlePeersList.test.ts @@ -56,6 +56,7 @@ describe("tests peers list handler", () => { status: false, statusCode: 401, error: "Not authenticated or session not found", + errorCode: "NOT_AUTHENTICATED", }); }); @@ -72,6 +73,7 @@ describe("tests peers list handler", () => { status: false, statusCode: 401, error: "Not authenticated or session not found", + errorCode: "NOT_AUTHENTICATED", }); }); @@ -88,6 +90,7 @@ describe("tests peers list handler", () => { status: false, statusCode: 401, error: "Not authenticated or session not found", + errorCode: "NOT_AUTHENTICATED", }); }); @@ -108,6 +111,7 @@ describe("tests peers list handler", () => { status: false, statusCode: 500, error: "Internal server error", + errorCode: "INTERNAL_ERROR", }); }); diff --git a/src/services/socket-handlers.handleUpdateHistory.test.ts b/src/services/socket-handlers.handleUpdateHistory.test.ts index 8ac463e..320973f 100644 --- a/src/services/socket-handlers.handleUpdateHistory.test.ts +++ b/src/services/socket-handlers.handleUpdateHistory.test.ts @@ -1,11 +1,10 @@ import { describe, vi, it, expect, beforeEach } from "vitest"; import { handleUpdateHistory } from "./socket-handlers"; -// TODO: does it make any difference if I mention/don't-mention type import type { SocketHandlerDeps } from "./socket-handlers.deps"; -import type { AppServer, AppSocket, DocumentUpdate, UpdateHistoryArgs } from "../types"; +import type { AppSocket, DocumentUpdate, UpdateHistoryArgs } from "../types"; function createFakeSocket( - broadcastOperator?: { emit: ReturnType}, + broadcastOperator?: { emit: ReturnType }, dataOverrides?: Partial<{ authenticated: boolean; documentId: string; @@ -36,6 +35,7 @@ describe("updateHistory", () => { const fakeMongodbStore = { getUpdatesByDocument: vi.fn(), + countUpdatesByDocument: vi.fn(), }; const deps: SocketHandlerDeps = { @@ -44,7 +44,7 @@ describe("updateHistory", () => { mongodbStore: fakeMongodbStore as any, }; - it('returns early when not authenticated', async () => { + it("returns early when not authenticated", async () => { const fakeSocket: AppSocket = createFakeSocket(undefined, { authenticated: false }); const fakeArgs: UpdateHistoryArgs = { documentId: "test-document-id", @@ -57,10 +57,11 @@ describe("updateHistory", () => { status: false, statusCode: 401, error: "Not authenticated", + errorCode: "NOT_AUTHENTICATED", }); }); - it('returns early when documentId is empty in socket data', async () => { + it("returns early when documentId is empty in socket data", async () => { const fakeSocket: AppSocket = createFakeSocket(undefined, { documentId: "" }); const fakeArgs: UpdateHistoryArgs = {}; const fakeCallback = vi.fn(); @@ -71,10 +72,11 @@ describe("updateHistory", () => { status: false, statusCode: 401, error: "Not authenticated", + errorCode: "NOT_AUTHENTICATED", }); }); - it('returns early when sessionDid is empty in socket data', async () => { + it("returns early when sessionDid is empty in socket data", async () => { const fakeSocket: AppSocket = createFakeSocket(undefined, { sessionDid: "" }); const fakeArgs: UpdateHistoryArgs = { documentId: "test-document-id", @@ -87,12 +89,13 @@ describe("updateHistory", () => { status: false, statusCode: 401, error: "Not authenticated", + errorCode: "NOT_AUTHENTICATED", }); }); - it('returns update history successfully, with fallback argument values', async () => { + it("returns update history successfully, with fallback argument values", async () => { const fakeSocket: AppSocket = createFakeSocket(undefined, { - documentId: "test-document-id" + documentId: "test-document-id", }); const fakeArgs: UpdateHistoryArgs = { documentId: "test-document-id", @@ -102,28 +105,41 @@ describe("updateHistory", () => { const documentId = fakeArgs.documentId || fakeSocket.data.documentId; fakeMongodbStore.getUpdatesByDocument.mockResolvedValue(fakeResponse); + fakeMongodbStore.countUpdatesByDocument.mockResolvedValue(fakeResponse.length); await handleUpdateHistory(deps, fakeSocket, fakeArgs, fakeCallback); - expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalled(); - expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalledWith({ - documentId, - sessionDid: fakeSocket.data.sessionDid, - }, { - offset: 0, limit: 100, sort: "desc", committed: undefined, - }); + expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalledWith( + { + documentId, + sessionDid: fakeSocket.data.sessionDid, + }, + { + offset: 0, + limit: 100, + sort: "desc", + committed: undefined, + } + ); + expect(fakeMongodbStore.countUpdatesByDocument).toHaveBeenCalledWith( + { + documentId, + sessionDid: fakeSocket.data.sessionDid, + }, + { committed: undefined }, + ); expect(fakeCallback).toHaveBeenCalledWith({ status: true, statusCode: 200, data: { history: [], - total: 0, + total: fakeResponse.length, }, }); }); - it('returns update history successfully with proper argument values set', async () => { + it("returns update history successfully with proper argument values set", async () => { const fakeSocket: AppSocket = createFakeSocket(); const fakeArgs: UpdateHistoryArgs = { documentId: "test-document-id", @@ -137,29 +153,42 @@ describe("updateHistory", () => { const fakeCallback = vi.fn(); const fakeResponse: DocumentUpdate[] = [ { - "id": "test-id", - "documentId": "test-document-id", - "data": "test-encrypted-data", - "updateType": "yjs_update", - "committed": false, - "commitCid": null, - "createdAt": 1772181495470, - "sessionDid": "test-session-did" - } + id: "test-id", + documentId: "test-document-id", + data: "test-encrypted-data", + updateType: "yjs_update", + committed: false, + commitCid: null, + createdAt: 1772181495470, + sessionDid: "test-session-did", + }, ]; const documentId = fakeArgs.documentId || fakeSocket.data.documentId; fakeMongodbStore.getUpdatesByDocument.mockResolvedValue(fakeResponse); + fakeMongodbStore.countUpdatesByDocument.mockResolvedValue(fakeResponse.length); await handleUpdateHistory(deps, fakeSocket, fakeArgs, fakeCallback); - expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalled(); - expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalledWith({ - documentId, - sessionDid: fakeSocket.data.sessionDid, - }, { - offset: fakeArgs.offset, limit: fakeArgs.limit, sort: fakeArgs.sort, committed: fakeArgs.filters?.committed, - }); + expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalledWith( + { + documentId, + sessionDid: fakeSocket.data.sessionDid, + }, + { + offset: fakeArgs.offset, + limit: fakeArgs.limit, + sort: fakeArgs.sort, + committed: fakeArgs.filters?.committed, + } + ); + expect(fakeMongodbStore.countUpdatesByDocument).toHaveBeenCalledWith( + { + documentId, + sessionDid: fakeSocket.data.sessionDid, + }, + { committed: fakeArgs.filters?.committed }, + ); expect(fakeCallback).toHaveBeenCalledWith({ status: true, @@ -171,7 +200,7 @@ describe("updateHistory", () => { }); }); - it('returns 500 due to db operation error', async () => { + it("returns 500 due to db operation error", async () => { const fakeSocket: AppSocket = createFakeSocket(); const fakeArgs: UpdateHistoryArgs = { documentId: "test-document-id", @@ -186,21 +215,28 @@ describe("updateHistory", () => { const documentId = fakeArgs.documentId || fakeSocket.data.documentId; fakeMongodbStore.getUpdatesByDocument.mockRejectedValue(new Error("db error")); + fakeMongodbStore.countUpdatesByDocument.mockResolvedValue(0); await handleUpdateHistory(deps, fakeSocket, fakeArgs, fakeCallback); - expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalled(); - expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalledWith({ - documentId, - sessionDid: fakeSocket.data.sessionDid, - }, { - offset: 0, limit: 1000, sort: "desc", committed: false, - }); + expect(fakeMongodbStore.getUpdatesByDocument).toHaveBeenCalledWith( + { + documentId, + sessionDid: fakeSocket.data.sessionDid, + }, + { + offset: 0, + limit: 1000, + sort: "desc", + committed: false, + } + ); expect(fakeCallback).toHaveBeenCalledWith({ status: false, statusCode: 500, error: "Internal server error", + errorCode: "INTERNAL_ERROR", }); }); }); \ No newline at end of file diff --git a/src/services/socket-handlers.terminateSession.test.ts b/src/services/socket-handlers.terminateSession.test.ts index 44f30dc..c7dca76 100644 --- a/src/services/socket-handlers.terminateSession.test.ts +++ b/src/services/socket-handlers.terminateSession.test.ts @@ -39,6 +39,7 @@ describe("handleTerminateSession", () => { }; const fakeSessionManager = { getSession: vi.fn(), + deactivateSession: vi.fn(), terminateSession: vi.fn(), }; const fakeMongoDBStore = {} as any; @@ -67,47 +68,50 @@ describe("handleTerminateSession", () => { status: false, statusCode: 400, error: "Session DID is required", - } + errorCode: "SESSION_DID_MISSING", + }; await handleTerminateSession(deps, fakeIO, fakeSocket, fakeArgs, callback); expect(callback).toHaveBeenCalledWith(callbackResponse); expect(fakeSessionManager.getSession).not.toHaveBeenCalled(); }); - it("returns 404 when session is not found", async() => { + it("returns 404 when session is not found", async () => { const fakeIO = createFakeIO(); const fakeSocket = createFakeSocket(); const fakeArgs = { documentId: "test-document-id", sessionDid: "test-session-did", ownerToken: "test-owner-token", - ownerAddress: "test-owner-address", - contractAddress: "test-contract-address", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", }; const callback = vi.fn(); fakeSessionManager.getSession.mockResolvedValue(undefined); await handleTerminateSession(deps, fakeIO, fakeSocket, fakeArgs, callback); - expect(fakeSessionManager.getSession).toHaveBeenCalledOnce(); - expect(fakeSessionManager.getSession).toHaveBeenCalledWith(fakeArgs.documentId, fakeArgs.sessionDid); - const callbackResponse = { status: false, statusCode: 404, error: "Session not found", + errorCode: "SESSION_NOT_FOUND", }; + expect(fakeSessionManager.getSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeArgs.sessionDid + ); expect(callback).toHaveBeenCalledWith(callbackResponse); }); - it("returns 401 when ownerDid does not match session owner", async() => { + it("returns 401 when ownerDid does not match session owner", async () => { const fakeIO = createFakeIO(); const fakeSocket = createFakeSocket(); const fakeArgs = { documentId: "test-document-id", sessionDid: "test-session-did", ownerToken: "test-owner-token", - ownerAddress: "test-owner-address", - contractAddress: "test-contract-address", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", }; const callback = vi.fn(); @@ -118,10 +122,11 @@ describe("handleTerminateSession", () => { await handleTerminateSession(deps, fakeIO, fakeSocket, fakeArgs, callback); - expect(fakeSessionManager.getSession).toHaveBeenCalledOnce(); - expect(fakeSessionManager.getSession).toHaveBeenCalledWith(fakeArgs.documentId, fakeArgs.sessionDid); + expect(fakeSessionManager.getSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeArgs.sessionDid + ); - expect(fakeAuthService.verifyOwnerToken).toHaveBeenCalledOnce(); expect(fakeAuthService.verifyOwnerToken).toHaveBeenCalledWith( fakeArgs.ownerToken, fakeArgs.contractAddress, @@ -132,6 +137,7 @@ describe("handleTerminateSession", () => { status: false, statusCode: 401, error: "Unauthorized", + errorCode: "AUTH_TOKEN_INVALID", }; expect(callback).toHaveBeenCalledWith(callbackResponse); }); @@ -165,8 +171,8 @@ describe("handleTerminateSession", () => { documentId: "test-document-id", sessionDid: "test-session-did", ownerToken: "test-owner-token", - ownerAddress: "test-owner-address", - contractAddress: "test-contract-address", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", }; const callback = vi.fn(); From c4273d176ec75068f1b21f834c323b029c659a62 Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Mon, 9 Mar 2026 19:32:09 +0530 Subject: [PATCH 15/24] updates vitest config --- vitest.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vitest.config.ts b/vitest.config.ts index e1d99b1..bc3aa8e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,10 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { + coverage: { + enabled: true, + reporter: ["lcov"], + }, environment: "node", include: ["src/**/*.test.ts", "tests/**/*.test.ts"], globals: true, From a611ff40fb77463bdad0169e4423a172cb009f26 Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Tue, 10 Mar 2026 12:26:40 +0530 Subject: [PATCH 16/24] minor: updates gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 867d95d..89d2560 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,6 @@ scripts.ts # .md files dev-journal.md + +# docs +docs/ From 5a65351488b82501ef4bea89ca8ec6c036519965 Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Tue, 10 Mar 2026 13:40:14 +0530 Subject: [PATCH 17/24] adds more test to increase coverage --- .../socket-handlers.handleAuth.test.ts | 89 +++++++++++++++++++ .../socket-handlers.handleAwareness.test.ts | 17 ++++ ...ocket-handlers.handleDisconnecting.test.ts | 12 +++ ...cket-handlers.handleDocumentCommit.test.ts | 54 ++++++++++- ...cket-handlers.handleDocumentUpdate.test.ts | 26 +++++- .../socket-handlers.terminateSession.test.ts | 54 +++++++++++ src/services/socket-handlers.ts | 2 +- 7 files changed, 249 insertions(+), 5 deletions(-) diff --git a/src/services/socket-handlers.handleAuth.test.ts b/src/services/socket-handlers.handleAuth.test.ts index ac7fd1b..41d724b 100644 --- a/src/services/socket-handlers.handleAuth.test.ts +++ b/src/services/socket-handlers.handleAuth.test.ts @@ -409,6 +409,95 @@ describe("handleAuth", () => { }); }); + it("returns 400 when in owner flow but owner token or session DID is missing", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const callback = vi.fn(); + + fakeSessionManager.getSession.mockResolvedValue(undefined); + + let sessionDidReadCount = 0; + const argsWithGetterSessionDid = { + documentId: "doc-1", + get sessionDid() { + sessionDidReadCount++; + return sessionDidReadCount === 1 ? "session-1" : (undefined as any); + }, + collaborationToken: "collab-token", + ownerToken: "owner-token", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", + } as AuthArgs; + + await handleAuth(deps, fakeIO, fakeSocket, argsWithGetterSessionDid, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 400, + error: "Document ID, owner token, and session DID are required", + errorCode: ErrorCode.AUTH_TOKEN_MISSING, + }); + }); + + it("returns 400 when in owner flow with invalid contract or owner address format", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + ownerToken: "owner-token", + ownerAddress: "not-a-valid-address", + contractAddress: "0x0000000000000000000000000000000000000002", + roomInfo: "room-info", + }; + const callback = vi.fn(); + + fakeSessionManager.getSession.mockResolvedValue(undefined); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 400, + error: "Invalid contract address or owner address format", + errorCode: ErrorCode.INVALID_ADDRESS, + }); + }); + + it("returns 400 when joining existing session with owner token but invalid contract or owner address", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: AuthArgs = { + documentId: "doc-1", + sessionDid: "session-1", + collaborationToken: "collab-token", + ownerToken: "owner-token", + ownerAddress: "invalid-hex", + contractAddress: "0x0000000000000000000000000000000000000002", + roomInfo: "room-info", + }; + const callback = vi.fn(); + + const existingSession = { + sessionDid: fakeArgs.sessionDid, + ownerDid: "owner-did", + roomInfo: "existing-room-info", + }; + + fakeSessionManager.getSession.mockResolvedValue(existingSession); + fakeAuthService.verifyCollaborationToken.mockResolvedValue("user-did"); + + await handleAuth(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 400, + error: "Invalid contract address or owner address format", + errorCode: ErrorCode.INVALID_ADDRESS, + }); + }); + it("returns 401 when owner token verification fails in owner flow", async () => { const fakeIO = createFakeIO(); const fakeSocket = createFakeSocket(); diff --git a/src/services/socket-handlers.handleAwareness.test.ts b/src/services/socket-handlers.handleAwareness.test.ts index 14df10d..27ffb83 100644 --- a/src/services/socket-handlers.handleAwareness.test.ts +++ b/src/services/socket-handlers.handleAwareness.test.ts @@ -67,4 +67,21 @@ describe('handleAwareness', () => { roomId: fakeArgs.documentId, }); }); + + it("does not throw when an error occurs in awareness handler", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(undefined, { authenticated: true }); + Object.defineProperty(fakeSocket, "to", { + get() { + throw new Error("socket.to failed"); + }, + }); + const fakeArgs = { + documentId: "test-document-id", + data: {}, + collaborationToken: "", + }; + + await expect(handleAwareness(fakeIO, fakeSocket as any, fakeArgs)).resolves.not.toThrow(); + }); }); \ No newline at end of file diff --git a/src/services/socket-handlers.handleDisconnecting.test.ts b/src/services/socket-handlers.handleDisconnecting.test.ts index f2a555d..f0b425f 100644 --- a/src/services/socket-handlers.handleDisconnecting.test.ts +++ b/src/services/socket-handlers.handleDisconnecting.test.ts @@ -104,4 +104,16 @@ describe("handleDisconnecting", () => { fakeSocket.id ); }); + + it("does not throw when an error occurs during disconnection cleanup", async () => { + const fakeSocket = createFakeSocket(undefined, { + authenticated: true, + documentId: "doc-1", + sessionDid: "session-1", + role: "owner", + }); + fakeSessionManager.removeClientFromSession.mockRejectedValue(new Error("db error")); + + await expect(handleDisconnecting(deps, fakeSocket)).resolves.not.toThrow(); + }); }); diff --git a/src/services/socket-handlers.handleDocumentCommit.test.ts b/src/services/socket-handlers.handleDocumentCommit.test.ts index db6cb77..af0895a 100644 --- a/src/services/socket-handlers.handleDocumentCommit.test.ts +++ b/src/services/socket-handlers.handleDocumentCommit.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { handleDocumentCommit } from "./socket-handlers"; import type { AppSocket, DocumentCommitArgs } from "../types"; import type { SocketHandlerDeps } from "./socket-handlers.deps"; +import { ErrorCode } from "../types"; function createFakeSocket( dataOverrides?: Partial<{ @@ -162,6 +163,32 @@ describe("handleDocumentCommit", () => { } }); + it("returns 400 when contract or owner address format is invalid", async () => { + const fakeSocket = createFakeSocket(); + const fakeArgs: DocumentCommitArgs = { + documentId: "doc-1", + updates: ["u1"], + cid: "cid-1", + ownerToken: "owner-token", + ownerAddress: "not-a-valid-address", + contractAddress: "0x0000000000000000000000000000000000000002", + }; + const callback = vi.fn(); + + const runtimeSession = { sessionDid: fakeSocket.data.sessionDid }; + fakeSessionManager.getRuntimeSession.mockResolvedValue(runtimeSession); + + await handleDocumentCommit(deps, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 400, + error: "Invalid contract address or owner address format", + errorCode: ErrorCode.INVALID_ADDRESS, + }); + expect(fakeAuthService.verifyOwnerToken).not.toHaveBeenCalled(); + }); + it("returns 401 when owner token verification fails", async () => { const fakeSocket = createFakeSocket(); const fakeArgs: DocumentCommitArgs = { @@ -234,6 +261,29 @@ describe("handleDocumentCommit", () => { }, }); }); -} -); + + it("returns 500 when an unexpected error occurs in document commit handler", async () => { + const fakeSocket = createFakeSocket(); + const fakeArgs: DocumentCommitArgs = { + documentId: "doc-1", + updates: ["u1"], + cid: "cid-1", + ownerToken: "owner-token", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", + }; + const callback = vi.fn(); + + fakeSessionManager.getRuntimeSession.mockRejectedValue(new Error("db error")); + + await handleDocumentCommit(deps, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 500, + error: "Internal server error", + errorCode: ErrorCode.INTERNAL_ERROR, + }); + }); +}); diff --git a/src/services/socket-handlers.handleDocumentUpdate.test.ts b/src/services/socket-handlers.handleDocumentUpdate.test.ts index 87ca71e..7c08580 100644 --- a/src/services/socket-handlers.handleDocumentUpdate.test.ts +++ b/src/services/socket-handlers.handleDocumentUpdate.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { handleDocumentUpdate, getRoomName } from "./socket-handlers"; import type { AppServer, AppSocket, DocumentUpdateArgs } from "../types"; import type { SocketHandlerDeps } from "./socket-handlers.deps"; +import { ErrorCode } from "../types"; function createFakeIO(): AppServer { return {} as unknown as AppServer; @@ -201,6 +202,27 @@ describe("handleDocumentUpdate", () => { }, }); }); -} -); + + it("returns 500 when an unexpected error occurs in document update handler", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs: DocumentUpdateArgs = { + documentId: "doc-1", + data: "update-data", + collaborationToken: "token", + }; + const callback = vi.fn(); + + fakeSessionManager.getRuntimeSession.mockRejectedValue(new Error("db error")); + + await handleDocumentUpdate(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 500, + error: "Internal server error", + errorCode: ErrorCode.INTERNAL_ERROR, + }); + }); +}); diff --git a/src/services/socket-handlers.terminateSession.test.ts b/src/services/socket-handlers.terminateSession.test.ts index c7dca76..5c1930b 100644 --- a/src/services/socket-handlers.terminateSession.test.ts +++ b/src/services/socket-handlers.terminateSession.test.ts @@ -75,6 +75,36 @@ describe("handleTerminateSession", () => { expect(fakeSessionManager.getSession).not.toHaveBeenCalled(); }); + it("returns 400 when contract or owner address format is invalid", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs = { + documentId: "test-document-id", + sessionDid: "test-session-did", + ownerToken: "test-owner-token", + ownerAddress: "not-a-valid-address", + contractAddress: "0x0000000000000000000000000000000000000002", + }; + const callback = vi.fn(); + + const fakeSessionResponse = { ownerDid: "fake-owner-did", sessionDid: fakeArgs.sessionDid }; + fakeSessionManager.getSession.mockResolvedValue(fakeSessionResponse); + + await handleTerminateSession(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(fakeSessionManager.getSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeArgs.sessionDid + ); + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 400, + error: "Invalid contract address or owner address format", + errorCode: "INVALID_ADDRESS", + }); + expect(fakeAuthService.verifyOwnerToken).not.toHaveBeenCalled(); + }); + it("returns 404 when session is not found", async () => { const fakeIO = createFakeIO(); const fakeSocket = createFakeSocket(); @@ -224,5 +254,29 @@ describe("handleTerminateSession", () => { }; expect(callback).toHaveBeenCalledWith(callbackResponse); }); + + it("returns 500 when an unexpected error occurs in terminate session handler", async () => { + const fakeIO = createFakeIO(); + const fakeSocket = createFakeSocket(); + const fakeArgs = { + documentId: "test-document-id", + sessionDid: "test-session-did", + ownerToken: "test-owner-token", + ownerAddress: "0x0000000000000000000000000000000000000001", + contractAddress: "0x0000000000000000000000000000000000000002", + }; + const callback = vi.fn(); + + fakeSessionManager.getSession.mockRejectedValue(new Error("db error")); + + await handleTerminateSession(deps, fakeIO, fakeSocket, fakeArgs, callback); + + expect(callback).toHaveBeenCalledWith({ + status: false, + statusCode: 500, + error: "Internal server error", + errorCode: "INTERNAL_ERROR", + }); + }); }); diff --git a/src/services/socket-handlers.ts b/src/services/socket-handlers.ts index 2c4b8ab..412a626 100644 --- a/src/services/socket-handlers.ts +++ b/src/services/socket-handlers.ts @@ -136,7 +136,7 @@ export async function handleAuth( if (!existingSession && args.ownerToken) { // - Set up a new session (owner flow) - if (!args.ownerToken || !sessionDid) { + if (!args.ownerToken || !args.sessionDid) { return callback({ status: false, statusCode: 400, From 3cddfccb4c498f3b1be89ed945c23f43c9b90197 Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Tue, 10 Mar 2026 15:00:19 +0530 Subject: [PATCH 18/24] chore: replaced called with calledWith to avoid brittle tests --- ...cket-handlers.handleDocumentCommit.test.ts | 19 ++++++++++++++++--- ...cket-handlers.handleDocumentUpdate.test.ts | 15 ++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/services/socket-handlers.handleDocumentCommit.test.ts b/src/services/socket-handlers.handleDocumentCommit.test.ts index af0895a..b94740f 100644 --- a/src/services/socket-handlers.handleDocumentCommit.test.ts +++ b/src/services/socket-handlers.handleDocumentCommit.test.ts @@ -206,7 +206,10 @@ describe("handleDocumentCommit", () => { fakeAuthService.verifyOwnerToken.mockResolvedValue(false); await handleDocumentCommit(deps, fakeSocket, fakeArgs, callback); - + expect(fakeSessionManager.getRuntimeSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeSocket.data.sessionDid + ); expect(fakeAuthService.verifyOwnerToken).toHaveBeenCalledWith( fakeArgs.ownerToken, fakeArgs.contractAddress, @@ -247,8 +250,18 @@ describe("handleDocumentCommit", () => { fakeMongoDBStore.createCommit.mockResolvedValue(fakeCommit); await handleDocumentCommit(deps, fakeSocket, fakeArgs, callback); - - expect(fakeMongoDBStore.createCommit).toHaveBeenCalled(); + expect(fakeSessionManager.getRuntimeSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeSocket.data.sessionDid + ); + expect(fakeMongoDBStore.createCommit).toHaveBeenCalledWith({ + id: expect.any(String), + documentId: fakeArgs.documentId, + cid: fakeArgs.cid, + updates: fakeArgs.updates, + createdAt: expect.any(Number), + sessionDid: runtimeSession.sessionDid, + }); expect(callback).toHaveBeenCalledWith({ status: true, diff --git a/src/services/socket-handlers.handleDocumentUpdate.test.ts b/src/services/socket-handlers.handleDocumentUpdate.test.ts index 7c08580..8a25a1e 100644 --- a/src/services/socket-handlers.handleDocumentUpdate.test.ts +++ b/src/services/socket-handlers.handleDocumentUpdate.test.ts @@ -178,7 +178,20 @@ describe("handleDocumentUpdate", () => { await handleDocumentUpdate(deps, fakeIO, fakeSocket, fakeArgs, callback); - expect(fakeMongoDBStore.createUpdate).toHaveBeenCalled(); + expect(fakeSessionManager.getRuntimeSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeSocket.data.sessionDid + ); + expect(fakeMongoDBStore.createUpdate).toHaveBeenCalledWith({ + id: expect.any(String), + documentId: fakeArgs.documentId, + data: fakeArgs.data, + updateType: "yjs_update", + committed: false, + commitCid: null, + createdAt: expect.any(Number), + sessionDid: runtimeSession.sessionDid, + }); const roomName = getRoomName(fakeArgs.documentId!, fakeSocket.data.sessionDid); expect(fakeSocket.to).toHaveBeenCalledWith(roomName); From 11367d46c28312ae9a2357696641fdf4670f7c24 Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Tue, 10 Mar 2026 16:45:41 +0530 Subject: [PATCH 19/24] minor: adds a missing assertion --- .../socket-handlers.handleDocumentUpdate.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/services/socket-handlers.handleDocumentUpdate.test.ts b/src/services/socket-handlers.handleDocumentUpdate.test.ts index 8a25a1e..36deb30 100644 --- a/src/services/socket-handlers.handleDocumentUpdate.test.ts +++ b/src/services/socket-handlers.handleDocumentUpdate.test.ts @@ -135,7 +135,10 @@ describe("handleDocumentUpdate", () => { fakeAuthService.verifyCollaborationToken.mockResolvedValue(false); await handleDocumentUpdate(deps, fakeIO, fakeSocket, fakeArgs, callback); - + expect(fakeSessionManager.getRuntimeSession).toHaveBeenCalledWith( + fakeArgs.documentId, + fakeSocket.data.sessionDid, + ); expect(fakeAuthService.verifyCollaborationToken).toHaveBeenCalledWith( fakeArgs.collaborationToken, runtimeSession.sessionDid, @@ -182,6 +185,11 @@ describe("handleDocumentUpdate", () => { fakeArgs.documentId, fakeSocket.data.sessionDid ); + expect(fakeAuthService.verifyCollaborationToken).toHaveBeenCalledWith( + fakeArgs.collaborationToken, + runtimeSession.sessionDid, + fakeArgs.documentId + ); expect(fakeMongoDBStore.createUpdate).toHaveBeenCalledWith({ id: expect.any(String), documentId: fakeArgs.documentId, From 73e34c5af822b01889bfda557f760e26a5576cec Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Tue, 10 Mar 2026 18:46:39 +0530 Subject: [PATCH 20/24] refactor: moved all tests under separate directory --- .../services/socket-handlers.handleAuth.test.ts | 6 +++--- .../services/socket-handlers.handleAwareness.test.ts | 6 +++--- .../services/socket-handlers.handleCommitHistory.test.ts | 6 +++--- .../services/socket-handlers.handleDisconnecting.test.ts | 6 +++--- .../services/socket-handlers.handleDocumentCommit.test.ts | 8 ++++---- .../services/socket-handlers.handleDocumentUpdate.test.ts | 8 ++++---- .../services/socket-handlers.handlePeersList.test.ts | 6 +++--- .../services/socket-handlers.handleUpdateHistory.test.ts | 6 +++--- .../services/socket-handlers.terminateSession.test.ts | 6 +++--- vitest.config.ts | 2 +- 10 files changed, 30 insertions(+), 30 deletions(-) rename src/{ => tests}/services/socket-handlers.handleAuth.test.ts (98%) rename src/{ => tests}/services/socket-handlers.handleAwareness.test.ts (94%) rename src/{ => tests}/services/socket-handlers.handleCommitHistory.test.ts (97%) rename src/{ => tests}/services/socket-handlers.handleDisconnecting.test.ts (95%) rename src/{ => tests}/services/socket-handlers.handleDocumentCommit.test.ts (97%) rename src/{ => tests}/services/socket-handlers.handleDocumentUpdate.test.ts (97%) rename src/{ => tests}/services/socket-handlers.handlePeersList.test.ts (95%) rename src/{ => tests}/services/socket-handlers.handleUpdateHistory.test.ts (97%) rename src/{ => tests}/services/socket-handlers.terminateSession.test.ts (97%) diff --git a/src/services/socket-handlers.handleAuth.test.ts b/src/tests/services/socket-handlers.handleAuth.test.ts similarity index 98% rename from src/services/socket-handlers.handleAuth.test.ts rename to src/tests/services/socket-handlers.handleAuth.test.ts index 41d724b..750d3d2 100644 --- a/src/services/socket-handlers.handleAuth.test.ts +++ b/src/tests/services/socket-handlers.handleAuth.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { handleAuth, getRoomName } from "./socket-handlers"; -import { AppServer, AppSocket, AuthArgs, ErrorCode } from "../types"; -import type { SocketHandlerDeps } from "./socket-handlers.deps"; +import { handleAuth, getRoomName } from "../../services/socket-handlers"; +import { AppServer, AppSocket, AuthArgs, ErrorCode } from "../../types"; +import type { SocketHandlerDeps } from "../../services/socket-handlers.deps"; function createFakeIO(options?: { broadcastOperator?: { emit: ReturnType }; diff --git a/src/services/socket-handlers.handleAwareness.test.ts b/src/tests/services/socket-handlers.handleAwareness.test.ts similarity index 94% rename from src/services/socket-handlers.handleAwareness.test.ts rename to src/tests/services/socket-handlers.handleAwareness.test.ts index 27ffb83..d30197f 100644 --- a/src/services/socket-handlers.handleAwareness.test.ts +++ b/src/tests/services/socket-handlers.handleAwareness.test.ts @@ -1,7 +1,7 @@ import { beforeEach, it, describe, vi, expect } from "vitest"; -import { handleAwareness } from "./socket-handlers"; -import type { AppServer, AppSocket } from "../types"; -import type { SocketData } from "../types"; +import { handleAwareness } from "../../services/socket-handlers"; +import type { AppServer, AppSocket } from "../../types"; +import type { SocketData } from "../../types"; const defaultSocketData: SocketData = { documentId: "test-document-id", diff --git a/src/services/socket-handlers.handleCommitHistory.test.ts b/src/tests/services/socket-handlers.handleCommitHistory.test.ts similarity index 97% rename from src/services/socket-handlers.handleCommitHistory.test.ts rename to src/tests/services/socket-handlers.handleCommitHistory.test.ts index c51135e..3df70c4 100644 --- a/src/services/socket-handlers.handleCommitHistory.test.ts +++ b/src/tests/services/socket-handlers.handleCommitHistory.test.ts @@ -1,7 +1,7 @@ import { describe, vi, it, expect, beforeEach } from "vitest"; -import { handleCommitHistory } from "./socket-handlers"; -import type { SocketHandlerDeps } from "./socket-handlers.deps"; -import type { AppSocket, DocumentCommit, CommitHistoryArgs } from "../types"; +import { handleCommitHistory } from "../../services/socket-handlers"; +import type { SocketHandlerDeps } from "../../services/socket-handlers.deps"; +import type { AppSocket, DocumentCommit, CommitHistoryArgs } from "../../types"; function createFakeSocket( broadcastOperator?: { emit: ReturnType }, diff --git a/src/services/socket-handlers.handleDisconnecting.test.ts b/src/tests/services/socket-handlers.handleDisconnecting.test.ts similarity index 95% rename from src/services/socket-handlers.handleDisconnecting.test.ts rename to src/tests/services/socket-handlers.handleDisconnecting.test.ts index f0b425f..4310886 100644 --- a/src/services/socket-handlers.handleDisconnecting.test.ts +++ b/src/tests/services/socket-handlers.handleDisconnecting.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { handleDisconnecting } from "./socket-handlers"; -import type { AppSocket } from "../types/index"; -import type { SocketHandlerDeps } from "./socket-handlers.deps"; +import { handleDisconnecting } from "../../services/socket-handlers"; +import type { AppSocket } from "../../types"; +import type { SocketHandlerDeps } from "../../services/socket-handlers.deps"; /** * Fake socket for handler tests. diff --git a/src/services/socket-handlers.handleDocumentCommit.test.ts b/src/tests/services/socket-handlers.handleDocumentCommit.test.ts similarity index 97% rename from src/services/socket-handlers.handleDocumentCommit.test.ts rename to src/tests/services/socket-handlers.handleDocumentCommit.test.ts index b94740f..35a6118 100644 --- a/src/services/socket-handlers.handleDocumentCommit.test.ts +++ b/src/tests/services/socket-handlers.handleDocumentCommit.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { handleDocumentCommit } from "./socket-handlers"; -import type { AppSocket, DocumentCommitArgs } from "../types"; -import type { SocketHandlerDeps } from "./socket-handlers.deps"; -import { ErrorCode } from "../types"; +import { handleDocumentCommit } from "../../services/socket-handlers"; +import type { AppSocket, DocumentCommitArgs } from "../../types"; +import type { SocketHandlerDeps } from "../../services/socket-handlers.deps"; +import { ErrorCode } from "../../types"; function createFakeSocket( dataOverrides?: Partial<{ diff --git a/src/services/socket-handlers.handleDocumentUpdate.test.ts b/src/tests/services/socket-handlers.handleDocumentUpdate.test.ts similarity index 97% rename from src/services/socket-handlers.handleDocumentUpdate.test.ts rename to src/tests/services/socket-handlers.handleDocumentUpdate.test.ts index 36deb30..5be27a1 100644 --- a/src/services/socket-handlers.handleDocumentUpdate.test.ts +++ b/src/tests/services/socket-handlers.handleDocumentUpdate.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { handleDocumentUpdate, getRoomName } from "./socket-handlers"; -import type { AppServer, AppSocket, DocumentUpdateArgs } from "../types"; -import type { SocketHandlerDeps } from "./socket-handlers.deps"; -import { ErrorCode } from "../types"; +import { handleDocumentUpdate, getRoomName } from "../../services/socket-handlers"; +import type { AppServer, AppSocket, DocumentUpdateArgs } from "../../types"; +import type { SocketHandlerDeps } from "../../services/socket-handlers.deps"; +import { ErrorCode } from "../../types"; function createFakeIO(): AppServer { return {} as unknown as AppServer; diff --git a/src/services/socket-handlers.handlePeersList.test.ts b/src/tests/services/socket-handlers.handlePeersList.test.ts similarity index 95% rename from src/services/socket-handlers.handlePeersList.test.ts rename to src/tests/services/socket-handlers.handlePeersList.test.ts index dd762e5..67b5c3a 100644 --- a/src/services/socket-handlers.handlePeersList.test.ts +++ b/src/tests/services/socket-handlers.handlePeersList.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; -import { handlePeersList, getRoomName } from "./socket-handlers"; -// import { PeersListArgs } from "../types"; -import type { AppServer, AppSocket, PeersListArgs } from "../types"; +import { handlePeersList, getRoomName } from "../../services/socket-handlers"; +// import { PeersListArgs } from "../../types"; +import type { AppServer, AppSocket, PeersListArgs } from "../../types"; function createFakeIO(fetchSocketsResponse: any[] = []): AppServer { const fetchSocketsMock = vi.fn().mockResolvedValue(fetchSocketsResponse); diff --git a/src/services/socket-handlers.handleUpdateHistory.test.ts b/src/tests/services/socket-handlers.handleUpdateHistory.test.ts similarity index 97% rename from src/services/socket-handlers.handleUpdateHistory.test.ts rename to src/tests/services/socket-handlers.handleUpdateHistory.test.ts index 320973f..15d2068 100644 --- a/src/services/socket-handlers.handleUpdateHistory.test.ts +++ b/src/tests/services/socket-handlers.handleUpdateHistory.test.ts @@ -1,7 +1,7 @@ import { describe, vi, it, expect, beforeEach } from "vitest"; -import { handleUpdateHistory } from "./socket-handlers"; -import type { SocketHandlerDeps } from "./socket-handlers.deps"; -import type { AppSocket, DocumentUpdate, UpdateHistoryArgs } from "../types"; +import { handleUpdateHistory } from "../../services/socket-handlers"; +import type { SocketHandlerDeps } from "../../services/socket-handlers.deps"; +import type { AppSocket, DocumentUpdate, UpdateHistoryArgs } from "../../types"; function createFakeSocket( broadcastOperator?: { emit: ReturnType }, diff --git a/src/services/socket-handlers.terminateSession.test.ts b/src/tests/services/socket-handlers.terminateSession.test.ts similarity index 97% rename from src/services/socket-handlers.terminateSession.test.ts rename to src/tests/services/socket-handlers.terminateSession.test.ts index 5c1930b..4343003 100644 --- a/src/services/socket-handlers.terminateSession.test.ts +++ b/src/tests/services/socket-handlers.terminateSession.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { handleTerminateSession } from "./socket-handlers"; -import type { AppServer, AppSocket } from "../types/index"; -import type { SocketHandlerDeps } from "./socket-handlers.deps"; +import { handleTerminateSession } from "../../services/socket-handlers"; +import type { AppServer, AppSocket } from "../../types"; +import type { SocketHandlerDeps } from "../../services/socket-handlers.deps"; function createFakeIO(fetchSocketsResponse: any[] = []): AppServer { const fetchSocketsMock = vi.fn().mockResolvedValue(fetchSocketsResponse); diff --git a/vitest.config.ts b/vitest.config.ts index bc3aa8e..b8d0f7f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ reporter: ["lcov"], }, environment: "node", - include: ["src/**/*.test.ts", "tests/**/*.test.ts"], + include: ["src/tests/**/*.test.ts", "tests/**/*.test.ts"], globals: true, }, }); From 34ee0098050bf97c865a3240ce67d6d15482aedd Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Wed, 18 Mar 2026 12:40:37 +0530 Subject: [PATCH 21/24] tests: updated tests as pagination was removed from getUpdatesByDocument call --- .../services/socket-handlers.handleUpdateHistory.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/tests/services/socket-handlers.handleUpdateHistory.test.ts b/src/tests/services/socket-handlers.handleUpdateHistory.test.ts index 15d2068..3725084 100644 --- a/src/tests/services/socket-handlers.handleUpdateHistory.test.ts +++ b/src/tests/services/socket-handlers.handleUpdateHistory.test.ts @@ -115,9 +115,6 @@ describe("updateHistory", () => { sessionDid: fakeSocket.data.sessionDid, }, { - offset: 0, - limit: 100, - sort: "desc", committed: undefined, } ); @@ -176,9 +173,6 @@ describe("updateHistory", () => { sessionDid: fakeSocket.data.sessionDid, }, { - offset: fakeArgs.offset, - limit: fakeArgs.limit, - sort: fakeArgs.sort, committed: fakeArgs.filters?.committed, } ); @@ -225,9 +219,6 @@ describe("updateHistory", () => { sessionDid: fakeSocket.data.sessionDid, }, { - offset: 0, - limit: 1000, - sort: "desc", committed: false, } ); From ec0d68334099ce90445771d8dcd4cd5faa0d1afd Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Wed, 18 Mar 2026 12:48:18 +0530 Subject: [PATCH 22/24] minor: updates .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 89d2560..4c4a60c 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ scripts.ts # .md files dev-journal.md +CLAUDE.md # docs docs/ From 4e3df4cecbaf3d4164f745187fb42da5d667e48a Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Wed, 18 Mar 2026 13:42:37 +0530 Subject: [PATCH 23/24] chore: adds github actions workflow to run unit tests on raised PR --- .github/workflows/unit-tests.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/unit-tests.yml diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..41abca1 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,25 @@ +name: Unit Tests + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test -- --run From 488aa15bd3f9427cdbf8693f7c6ed7d6f32fdc01 Mon Sep 17 00:00:00 2001 From: Swagnik Dutta Date: Wed, 18 Mar 2026 14:55:45 +0530 Subject: [PATCH 24/24] tests: fixed unit test --- .../socket-handlers.handleDocumentUpdate.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/tests/services/socket-handlers.handleDocumentUpdate.test.ts b/src/tests/services/socket-handlers.handleDocumentUpdate.test.ts index 5be27a1..751b75e 100644 --- a/src/tests/services/socket-handlers.handleDocumentUpdate.test.ts +++ b/src/tests/services/socket-handlers.handleDocumentUpdate.test.ts @@ -168,13 +168,13 @@ describe("handleDocumentUpdate", () => { fakeAuthService.verifyCollaborationToken.mockResolvedValue(true); const fakeUpdate = { - id: "update-id", + id: "some-id", documentId: fakeArgs.documentId, data: fakeArgs.data, updateType: "yjs_update", committed: false, commitCid: null, - createdAt: 123456, + createdAt: 1000, sessionDid: runtimeSession.sessionDid, }; fakeMongoDBStore.createUpdate.mockResolvedValue(fakeUpdate); @@ -204,9 +204,9 @@ describe("handleDocumentUpdate", () => { const roomName = getRoomName(fakeArgs.documentId!, fakeSocket.data.sessionDid); expect(fakeSocket.to).toHaveBeenCalledWith(roomName); expect(fakeBroadcastOperator.emit).toHaveBeenCalledWith("/document/content_update", { - id: fakeUpdate.id, - data: fakeUpdate.data, - createdAt: fakeUpdate.createdAt, + id: expect.any(String), + data: fakeArgs.data, + createdAt: expect.any(Number), roomId: fakeArgs.documentId, }); @@ -214,12 +214,12 @@ describe("handleDocumentUpdate", () => { status: true, statusCode: 200, data: { - id: fakeUpdate.id, + id: expect.any(String), documentId: fakeUpdate.documentId, data: fakeUpdate.data, updateType: fakeUpdate.updateType, commitCid: fakeUpdate.commitCid, - createdAt: fakeUpdate.createdAt, + createdAt: expect.any(Number), }, }); });