real time messaging
This commit is contained in:
389
backend/package-lock.json
generated
389
backend/package-lock.json
generated
@@ -32,6 +32,7 @@
|
||||
"pg": "^8.16.3",
|
||||
"sequelize": "^6.37.7",
|
||||
"sequelize-cli": "^6.6.3",
|
||||
"socket.io": "^4.8.1",
|
||||
"stripe": "^18.4.0",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
@@ -43,6 +44,7 @@
|
||||
"nodemon": "^3.1.10",
|
||||
"sequelize-mock": "^0.10.2",
|
||||
"sinon": "^21.0.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"supertest": "^7.1.4"
|
||||
}
|
||||
},
|
||||
@@ -3530,6 +3532,12 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -3575,6 +3583,15 @@
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||
@@ -3969,6 +3986,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz",
|
||||
@@ -4934,6 +4960,170 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
|
||||
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.7.2",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client/node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
@@ -8790,6 +8980,196 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.6.0",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
||||
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "~4.3.4",
|
||||
"ws": "~8.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io/node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io/node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -9841,6 +10221,15 @@
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"pg": "^8.16.3",
|
||||
"sequelize": "^6.37.7",
|
||||
"sequelize-cli": "^6.6.3",
|
||||
"socket.io": "^4.8.1",
|
||||
"stripe": "^18.4.0",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
@@ -62,6 +63,7 @@
|
||||
"nodemon": "^3.1.10",
|
||||
"sequelize-mock": "^0.10.2",
|
||||
"sinon": "^21.0.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"supertest": "^7.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const { Message, User } = require('../models');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const logger = require('../utils/logger');
|
||||
const { emitNewMessage, emitMessageRead } = require('../sockets/messageSocket');
|
||||
const router = express.Router();
|
||||
|
||||
// Get all messages for the current user (inbox)
|
||||
@@ -109,15 +110,26 @@ router.get('/:id', authenticateToken, async (req, res) => {
|
||||
}
|
||||
|
||||
// Mark as read if user is the receiver
|
||||
if (message.receiverId === req.user.id && !message.isRead) {
|
||||
const wasUnread = message.receiverId === req.user.id && !message.isRead;
|
||||
if (wasUnread) {
|
||||
await message.update({ isRead: true });
|
||||
|
||||
// Emit socket event to sender for real-time read receipt
|
||||
const io = req.app.get('io');
|
||||
if (io) {
|
||||
emitMessageRead(io, message.senderId, {
|
||||
messageId: message.id,
|
||||
readAt: new Date().toISOString(),
|
||||
readBy: req.user.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Message fetched", {
|
||||
userId: req.user.id,
|
||||
messageId: req.params.id,
|
||||
markedAsRead: message.receiverId === req.user.id && !message.isRead
|
||||
markedAsRead: wasUnread
|
||||
});
|
||||
|
||||
res.json(message);
|
||||
@@ -165,6 +177,12 @@ router.post('/', authenticateToken, async (req, res) => {
|
||||
}]
|
||||
});
|
||||
|
||||
// Emit socket event to receiver for real-time notification
|
||||
const io = req.app.get('io');
|
||||
if (io) {
|
||||
emitNewMessage(io, receiverId, messageWithSender.toJSON());
|
||||
}
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Message sent", {
|
||||
senderId: req.user.id,
|
||||
@@ -202,6 +220,16 @@ router.put('/:id/read', authenticateToken, async (req, res) => {
|
||||
|
||||
await message.update({ isRead: true });
|
||||
|
||||
// Emit socket event to sender for real-time read receipt
|
||||
const io = req.app.get('io');
|
||||
if (io) {
|
||||
emitMessageRead(io, message.senderId, {
|
||||
messageId: message.id,
|
||||
readAt: new Date().toISOString(),
|
||||
readBy: req.user.id
|
||||
});
|
||||
}
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Message marked as read", {
|
||||
userId: req.user.id,
|
||||
|
||||
@@ -6,6 +6,8 @@ require("dotenv").config({
|
||||
path: envFile,
|
||||
});
|
||||
const express = require("express");
|
||||
const http = require("http");
|
||||
const { Server } = require("socket.io");
|
||||
const cors = require("cors");
|
||||
const bodyParser = require("body-parser");
|
||||
const path = require("path");
|
||||
@@ -31,7 +33,30 @@ const PayoutProcessor = require("./jobs/payoutProcessor");
|
||||
const RentalStatusJob = require("./jobs/rentalStatusJob");
|
||||
const ConditionCheckReminderJob = require("./jobs/conditionCheckReminder");
|
||||
|
||||
// Socket.io setup
|
||||
const { authenticateSocket } = require("./sockets/socketAuth");
|
||||
const { initializeMessageSocket } = require("./sockets/messageSocket");
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
|
||||
// Initialize Socket.io with CORS
|
||||
const io = new Server(server, {
|
||||
cors: {
|
||||
origin: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||
credentials: true,
|
||||
methods: ["GET", "POST"],
|
||||
},
|
||||
});
|
||||
|
||||
// Apply socket authentication middleware
|
||||
io.use(authenticateSocket);
|
||||
|
||||
// Initialize message socket handlers
|
||||
initializeMessageSocket(io);
|
||||
|
||||
// Store io instance in app for use in routes
|
||||
app.set("io", io);
|
||||
|
||||
// Import security middleware
|
||||
const {
|
||||
@@ -152,11 +177,12 @@ sequelize
|
||||
const conditionCheckJobs = ConditionCheckReminderJob.startScheduledReminders();
|
||||
logger.info("Condition check reminder job started");
|
||||
|
||||
app.listen(PORT, () => {
|
||||
server.listen(PORT, () => {
|
||||
logger.info(`Server is running on port ${PORT}`, {
|
||||
port: PORT,
|
||||
environment: env,
|
||||
});
|
||||
logger.info("Socket.io server initialized");
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
343
backend/sockets/messageSocket.js
Normal file
343
backend/sockets/messageSocket.js
Normal file
@@ -0,0 +1,343 @@
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
/**
|
||||
* Map to track typing status: { userId_receiverId: timestamp }
|
||||
* Used to prevent duplicate typing events and auto-clear stale states
|
||||
*/
|
||||
const typingStatus = new Map();
|
||||
|
||||
/**
|
||||
* Cleanup interval for stale typing indicators (every 5 seconds)
|
||||
*/
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
const TYPING_TIMEOUT = 5000; // 5 seconds
|
||||
|
||||
for (const [key, timestamp] of typingStatus.entries()) {
|
||||
if (now - timestamp > TYPING_TIMEOUT) {
|
||||
typingStatus.delete(key);
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
/**
|
||||
* Generate conversation room ID from two user IDs
|
||||
* Always sorts IDs to ensure consistent room naming regardless of who initiates
|
||||
*/
|
||||
const getConversationRoom = (userId1, userId2) => {
|
||||
const sorted = [userId1, userId2].sort();
|
||||
return `conv_${sorted[0]}_${sorted[1]}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get personal user room ID
|
||||
*/
|
||||
const getUserRoom = (userId) => {
|
||||
return `user_${userId}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize message socket handlers
|
||||
* @param {SocketIO.Server} io - Socket.io server instance
|
||||
*/
|
||||
const initializeMessageSocket = (io) => {
|
||||
io.on("connection", (socket) => {
|
||||
const userId = socket.userId;
|
||||
const userRoom = getUserRoom(userId);
|
||||
|
||||
logger.info("User connected to messaging", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
userEmail: socket.user.email,
|
||||
});
|
||||
|
||||
// Join user's personal room for receiving direct messages
|
||||
socket.join(userRoom);
|
||||
logger.debug("User joined personal room", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
room: userRoom,
|
||||
});
|
||||
|
||||
/**
|
||||
* Join a specific conversation room
|
||||
* Used when user opens a chat with another user
|
||||
*/
|
||||
socket.on("join_conversation", (data) => {
|
||||
try {
|
||||
const { otherUserId } = data;
|
||||
|
||||
if (!otherUserId) {
|
||||
logger.warn("join_conversation - missing otherUserId", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const conversationRoom = getConversationRoom(userId, otherUserId);
|
||||
socket.join(conversationRoom);
|
||||
|
||||
logger.debug("User joined conversation room", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
otherUserId,
|
||||
room: conversationRoom,
|
||||
});
|
||||
|
||||
socket.emit("conversation_joined", {
|
||||
conversationRoom,
|
||||
otherUserId,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error joining conversation", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Leave a specific conversation room
|
||||
* Used when user closes a chat
|
||||
*/
|
||||
socket.on("leave_conversation", (data) => {
|
||||
try {
|
||||
const { otherUserId } = data;
|
||||
|
||||
if (!otherUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conversationRoom = getConversationRoom(userId, otherUserId);
|
||||
socket.leave(conversationRoom);
|
||||
|
||||
logger.debug("User left conversation room", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
otherUserId,
|
||||
room: conversationRoom,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error leaving conversation", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Typing start indicator
|
||||
* Notifies the recipient that this user is typing
|
||||
*/
|
||||
socket.on("typing_start", (data) => {
|
||||
try {
|
||||
const { receiverId } = data;
|
||||
|
||||
if (!receiverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Throttle typing events (prevent spam)
|
||||
const typingKey = `${userId}_${receiverId}`;
|
||||
const lastTyping = typingStatus.get(typingKey);
|
||||
const now = Date.now();
|
||||
|
||||
if (lastTyping && now - lastTyping < 1000) {
|
||||
// Ignore if typed within last 1 second
|
||||
return;
|
||||
}
|
||||
|
||||
typingStatus.set(typingKey, now);
|
||||
|
||||
// Emit to recipient's personal room
|
||||
const receiverRoom = getUserRoom(receiverId);
|
||||
io.to(receiverRoom).emit("user_typing", {
|
||||
userId,
|
||||
firstName: socket.user.firstName,
|
||||
isTyping: true,
|
||||
});
|
||||
|
||||
logger.debug("Typing indicator sent", {
|
||||
socketId: socket.id,
|
||||
senderId: userId,
|
||||
receiverId,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error handling typing_start", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Typing stop indicator
|
||||
* Notifies the recipient that this user stopped typing
|
||||
*/
|
||||
socket.on("typing_stop", (data) => {
|
||||
try {
|
||||
const { receiverId } = data;
|
||||
|
||||
if (!receiverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear typing status
|
||||
const typingKey = `${userId}_${receiverId}`;
|
||||
typingStatus.delete(typingKey);
|
||||
|
||||
// Emit to recipient's personal room
|
||||
const receiverRoom = getUserRoom(receiverId);
|
||||
io.to(receiverRoom).emit("user_typing", {
|
||||
userId,
|
||||
firstName: socket.user.firstName,
|
||||
isTyping: false,
|
||||
});
|
||||
|
||||
logger.debug("Typing stop sent", {
|
||||
socketId: socket.id,
|
||||
senderId: userId,
|
||||
receiverId,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error handling typing_stop", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mark message as read (from client)
|
||||
* This is handled by the REST API route, but we listen here for consistency
|
||||
*/
|
||||
socket.on("mark_message_read", (data) => {
|
||||
try {
|
||||
const { messageId, senderId } = data;
|
||||
|
||||
if (!messageId || !senderId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit to sender's room to update their UI
|
||||
const senderRoom = getUserRoom(senderId);
|
||||
io.to(senderRoom).emit("message_read", {
|
||||
messageId,
|
||||
readAt: new Date().toISOString(),
|
||||
readBy: userId,
|
||||
});
|
||||
|
||||
logger.debug("Message read notification sent", {
|
||||
socketId: socket.id,
|
||||
messageId,
|
||||
readBy: userId,
|
||||
notifiedUserId: senderId,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error handling mark_message_read", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Disconnect handler
|
||||
* Clean up rooms and typing status
|
||||
*/
|
||||
socket.on("disconnect", (reason) => {
|
||||
// Clean up all typing statuses for this user
|
||||
for (const [key] of typingStatus.entries()) {
|
||||
if (key.startsWith(`${userId}_`)) {
|
||||
typingStatus.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("User disconnected from messaging", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
reason,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Error handler
|
||||
*/
|
||||
socket.on("error", (error) => {
|
||||
logger.error("Socket error", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
logger.info("Message socket handlers initialized");
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit new message event to a specific user
|
||||
* Called from message routes when a message is created
|
||||
* @param {SocketIO.Server} io - Socket.io server instance
|
||||
* @param {string} receiverId - User ID to send the message to
|
||||
* @param {Object} messageData - Message object with sender info
|
||||
*/
|
||||
const emitNewMessage = (io, receiverId, messageData) => {
|
||||
try {
|
||||
const receiverRoom = getUserRoom(receiverId);
|
||||
io.to(receiverRoom).emit("new_message", messageData);
|
||||
|
||||
logger.info("New message emitted", {
|
||||
receiverId,
|
||||
receiverRoom,
|
||||
messageId: messageData.id,
|
||||
senderId: messageData.senderId,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error emitting new message", {
|
||||
receiverId,
|
||||
messageId: messageData.id,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit message read event to sender
|
||||
* Called from message routes when a message is marked as read
|
||||
* @param {SocketIO.Server} io - Socket.io server instance
|
||||
* @param {string} senderId - User ID who sent the message
|
||||
* @param {Object} readData - Read status data
|
||||
*/
|
||||
const emitMessageRead = (io, senderId, readData) => {
|
||||
try {
|
||||
const senderRoom = getUserRoom(senderId);
|
||||
io.to(senderRoom).emit("message_read", readData);
|
||||
|
||||
logger.debug("Message read status emitted", {
|
||||
senderId,
|
||||
messageId: readData.messageId,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error emitting message read status", {
|
||||
senderId,
|
||||
messageId: readData.messageId,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initializeMessageSocket,
|
||||
emitNewMessage,
|
||||
emitMessageRead,
|
||||
getConversationRoom,
|
||||
getUserRoom,
|
||||
};
|
||||
111
backend/sockets/socketAuth.js
Normal file
111
backend/sockets/socketAuth.js
Normal file
@@ -0,0 +1,111 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { User } = require("../models");
|
||||
const logger = require("../utils/logger");
|
||||
const cookie = require("cookie");
|
||||
|
||||
/**
|
||||
* Socket.io authentication middleware
|
||||
* Verifies JWT token and attaches user to socket
|
||||
* Tokens can be provided via:
|
||||
* 1. Cookie (accessToken) - preferred for browser clients
|
||||
* 2. Query parameter (token) - fallback for mobile/other clients
|
||||
*/
|
||||
const authenticateSocket = async (socket, next) => {
|
||||
try {
|
||||
let token = null;
|
||||
|
||||
// Try to get token from cookies first (browser clients)
|
||||
if (socket.handshake.headers.cookie) {
|
||||
const cookies = cookie.parse(socket.handshake.headers.cookie);
|
||||
token = cookies.accessToken;
|
||||
}
|
||||
|
||||
// Fallback to query parameter (mobile/other clients)
|
||||
if (!token && socket.handshake.auth?.token) {
|
||||
token = socket.handshake.auth.token;
|
||||
}
|
||||
|
||||
// Fallback to legacy query parameter
|
||||
if (!token && socket.handshake.query?.token) {
|
||||
token = socket.handshake.query.token;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
logger.warn("Socket connection rejected - no token provided", {
|
||||
socketId: socket.id,
|
||||
address: socket.handshake.address,
|
||||
});
|
||||
return next(new Error("Authentication required"));
|
||||
}
|
||||
|
||||
// Verify JWT
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const userId = decoded.id;
|
||||
|
||||
if (!userId) {
|
||||
logger.warn("Socket connection rejected - invalid token format", {
|
||||
socketId: socket.id,
|
||||
});
|
||||
return next(new Error("Invalid token format"));
|
||||
}
|
||||
|
||||
// Look up user
|
||||
const user = await User.findByPk(userId);
|
||||
|
||||
if (!user) {
|
||||
logger.warn("Socket connection rejected - user not found", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
});
|
||||
return next(new Error("User not found"));
|
||||
}
|
||||
|
||||
// Validate JWT version (invalidate old tokens after password change)
|
||||
if (decoded.jwtVersion !== user.jwtVersion) {
|
||||
logger.warn("Socket connection rejected - JWT version mismatch", {
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
tokenVersion: decoded.jwtVersion,
|
||||
userVersion: user.jwtVersion,
|
||||
});
|
||||
return next(
|
||||
new Error("Session expired due to password change. Please log in again.")
|
||||
);
|
||||
}
|
||||
|
||||
// Attach user to socket for use in event handlers
|
||||
socket.userId = user.id;
|
||||
socket.user = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
};
|
||||
|
||||
logger.info("Socket authenticated successfully", {
|
||||
socketId: socket.id,
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
// Check if token is expired
|
||||
if (error.name === "TokenExpiredError") {
|
||||
logger.warn("Socket connection rejected - token expired", {
|
||||
socketId: socket.id,
|
||||
});
|
||||
return next(new Error("Token expired"));
|
||||
}
|
||||
|
||||
logger.error("Socket authentication error", {
|
||||
socketId: socket.id,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
|
||||
return next(new Error("Authentication failed"));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { authenticateSocket };
|
||||
156
backend/tests/unit/sockets/messageSocket.test.js
Normal file
156
backend/tests/unit/sockets/messageSocket.test.js
Normal file
@@ -0,0 +1,156 @@
|
||||
const { Server } = require('socket.io');
|
||||
const Client = require('socket.io-client');
|
||||
const http = require('http');
|
||||
const { initializeMessageSocket, emitNewMessage, emitMessageRead } = require('../../../sockets/messageSocket');
|
||||
|
||||
describe('Message Socket', () => {
|
||||
let io, serverSocket, clientSocket;
|
||||
let httpServer;
|
||||
|
||||
beforeAll((done) => {
|
||||
// Create HTTP server
|
||||
httpServer = http.createServer();
|
||||
|
||||
// Create Socket.io server
|
||||
io = new Server(httpServer);
|
||||
|
||||
httpServer.listen(() => {
|
||||
const port = httpServer.address().port;
|
||||
|
||||
// Initialize message socket handlers
|
||||
initializeMessageSocket(io);
|
||||
|
||||
// Create client socket
|
||||
clientSocket = new Client(`http://localhost:${port}`);
|
||||
|
||||
// Mock authentication by setting userId
|
||||
io.use((socket, next) => {
|
||||
socket.userId = 'test-user-123';
|
||||
socket.user = {
|
||||
id: 'test-user-123',
|
||||
email: 'test@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User'
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
// Wait for connection
|
||||
io.on('connection', (socket) => {
|
||||
serverSocket = socket;
|
||||
});
|
||||
|
||||
clientSocket.on('connect', done);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
io.close();
|
||||
clientSocket.close();
|
||||
httpServer.close();
|
||||
});
|
||||
|
||||
test('should connect successfully', () => {
|
||||
expect(clientSocket.connected).toBe(true);
|
||||
});
|
||||
|
||||
test('should join conversation room', (done) => {
|
||||
const otherUserId = 'other-user-456';
|
||||
|
||||
clientSocket.on('conversation_joined', (data) => {
|
||||
expect(data.otherUserId).toBe(otherUserId);
|
||||
expect(data.conversationRoom).toContain('conv_');
|
||||
done();
|
||||
});
|
||||
|
||||
clientSocket.emit('join_conversation', { otherUserId });
|
||||
});
|
||||
|
||||
test('should emit typing start event', (done) => {
|
||||
const receiverId = 'receiver-789';
|
||||
|
||||
serverSocket.on('typing_start', (data) => {
|
||||
expect(data.receiverId).toBe(receiverId);
|
||||
done();
|
||||
});
|
||||
|
||||
clientSocket.emit('typing_start', { receiverId });
|
||||
});
|
||||
|
||||
test('should emit typing stop event', (done) => {
|
||||
const receiverId = 'receiver-789';
|
||||
|
||||
serverSocket.on('typing_stop', (data) => {
|
||||
expect(data.receiverId).toBe(receiverId);
|
||||
done();
|
||||
});
|
||||
|
||||
clientSocket.emit('typing_stop', { receiverId });
|
||||
});
|
||||
|
||||
test('should emit new message to receiver', (done) => {
|
||||
const receiverId = 'receiver-123';
|
||||
const messageData = {
|
||||
id: 'message-456',
|
||||
senderId: 'sender-789',
|
||||
receiverId: receiverId,
|
||||
subject: 'Test Subject',
|
||||
content: 'Test message content',
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Create a second client to receive the message
|
||||
const port = httpServer.address().port;
|
||||
const receiverClient = new Client(`http://localhost:${port}`);
|
||||
|
||||
receiverClient.on('connect', () => {
|
||||
receiverClient.on('new_message', (message) => {
|
||||
expect(message.id).toBe(messageData.id);
|
||||
expect(message.content).toBe(messageData.content);
|
||||
receiverClient.close();
|
||||
done();
|
||||
});
|
||||
|
||||
// Emit the message
|
||||
emitNewMessage(io, receiverId, messageData);
|
||||
});
|
||||
});
|
||||
|
||||
test('should emit message read status to sender', (done) => {
|
||||
const senderId = 'sender-123';
|
||||
const readData = {
|
||||
messageId: 'message-789',
|
||||
readAt: new Date().toISOString(),
|
||||
readBy: 'reader-456'
|
||||
};
|
||||
|
||||
// Create a sender client to receive the read receipt
|
||||
const port = httpServer.address().port;
|
||||
const senderClient = new Client(`http://localhost:${port}`);
|
||||
|
||||
senderClient.on('connect', () => {
|
||||
senderClient.on('message_read', (data) => {
|
||||
expect(data.messageId).toBe(readData.messageId);
|
||||
expect(data.readBy).toBe(readData.readBy);
|
||||
senderClient.close();
|
||||
done();
|
||||
});
|
||||
|
||||
// Emit the read status
|
||||
emitMessageRead(io, senderId, readData);
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle disconnection gracefully', (done) => {
|
||||
const testClient = new Client(`http://localhost:${httpServer.address().port}`);
|
||||
|
||||
testClient.on('connect', () => {
|
||||
testClient.on('disconnect', (reason) => {
|
||||
expect(reason).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
testClient.disconnect();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user