WebsocketLoadBalancerTests.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. /* eslint-disable
  2. no-return-assign,
  3. */
  4. // TODO: This file was created by bulk-decaffeinate.
  5. // Fix any style issues and re-enable lint.
  6. /*
  7. * decaffeinate suggestions:
  8. * DS101: Remove unnecessary use of Array.from
  9. * DS102: Remove unnecessary code created because of implicit returns
  10. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  11. */
  12. const SandboxedModule = require('sandboxed-module')
  13. const sinon = require('sinon')
  14. const expect = require('chai').expect
  15. const modulePath = require('path').join(
  16. __dirname,
  17. '../../../app/js/WebsocketLoadBalancer'
  18. )
  19. describe('WebsocketLoadBalancer', function () {
  20. beforeEach(function () {
  21. this.rclient = {}
  22. this.RoomEvents = { on: sinon.stub() }
  23. this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, {
  24. requires: {
  25. '@overleaf/settings': (this.Settings = { redis: {} }),
  26. './RedisClientManager': {
  27. createClientList: () => [],
  28. },
  29. './SafeJsonParse': (this.SafeJsonParse = {
  30. parse: (data, cb) => cb(null, JSON.parse(data)),
  31. }),
  32. './EventLogger': { checkEventOrder: sinon.stub() },
  33. './HealthCheckManager': { check: sinon.stub() },
  34. './RoomManager': (this.RoomManager = {
  35. eventSource: sinon.stub().returns(this.RoomEvents),
  36. }),
  37. './ChannelManager': (this.ChannelManager = { publish: sinon.stub() }),
  38. './ConnectedUsersManager': (this.ConnectedUsersManager = {
  39. refreshClient: sinon.stub(),
  40. }),
  41. },
  42. })
  43. this.io = {}
  44. this.WebsocketLoadBalancer.rclientPubList = [{ publish: sinon.stub() }]
  45. this.WebsocketLoadBalancer.rclientSubList = [
  46. {
  47. subscribe: sinon.stub(),
  48. on: sinon.stub(),
  49. },
  50. ]
  51. this.room_id = 'room-id'
  52. this.message = 'otUpdateApplied'
  53. return (this.payload = ['argument one', 42])
  54. })
  55. describe('shouldDisconnectClient', function () {
  56. it('should return false for general messages', function () {
  57. const client = {
  58. ol_context: { user_id: 'abcd' },
  59. }
  60. const message = {
  61. message: 'someNiceMessage',
  62. payload: [{ data: 'whatever' }],
  63. }
  64. expect(
  65. this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
  66. ).to.equal(false)
  67. })
  68. describe('user removed from project', function () {
  69. const messageName = 'userRemovedFromProject'
  70. const client = {
  71. ol_context: { user_id: 'abcd' },
  72. }
  73. it('should return false, when the user_id does not match', function () {
  74. const message = {
  75. message: messageName,
  76. payload: ['xyz'],
  77. }
  78. expect(
  79. this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
  80. ).to.equal(false)
  81. })
  82. it('should return true, if the user_id matches', function () {
  83. const message = {
  84. message: messageName,
  85. payload: [`${client.ol_context.user_id}`],
  86. }
  87. expect(
  88. this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
  89. ).to.equal(true)
  90. })
  91. })
  92. describe('link-sharing turned off', function () {
  93. const messageName = 'project:publicAccessLevel:changed'
  94. describe('when the new access level is set to "private"', function () {
  95. const message = {
  96. message: messageName,
  97. payload: [{ newAccessLevel: 'private' }],
  98. }
  99. describe('when the user is an invited member', function () {
  100. const client = {
  101. ol_context: {
  102. is_invited_member: true,
  103. },
  104. }
  105. it('should return false', function () {
  106. expect(
  107. this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
  108. ).to.equal(false)
  109. })
  110. })
  111. describe('when the user not an invited member', function () {
  112. const client = {
  113. ol_context: {
  114. is_invited_member: false,
  115. },
  116. }
  117. it('should return true', function () {
  118. expect(
  119. this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
  120. ).to.equal(true)
  121. })
  122. })
  123. })
  124. describe('when the new access level is "tokenBased"', function () {
  125. const message = {
  126. message: messageName,
  127. payload: [{ newAccessLevel: 'tokenBased' }],
  128. }
  129. describe('when the user is an invited member', function () {
  130. const client = {
  131. ol_context: {
  132. is_invited_member: true,
  133. },
  134. }
  135. it('should return false', function () {
  136. expect(
  137. this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
  138. ).to.equal(false)
  139. })
  140. })
  141. describe('when the user not an invited member', function () {
  142. const client = {
  143. ol_context: {
  144. is_invited_member: false,
  145. },
  146. }
  147. it('should return false', function () {
  148. expect(
  149. this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
  150. ).to.equal(false)
  151. })
  152. })
  153. })
  154. })
  155. })
  156. describe('emitToRoom', function () {
  157. beforeEach(function () {
  158. return this.WebsocketLoadBalancer.emitToRoom(
  159. this.room_id,
  160. this.message,
  161. ...Array.from(this.payload)
  162. )
  163. })
  164. return it('should publish the message to redis', function () {
  165. return this.ChannelManager.publish
  166. .calledWith(
  167. this.WebsocketLoadBalancer.rclientPubList[0],
  168. 'editor-events',
  169. this.room_id,
  170. JSON.stringify({
  171. room_id: this.room_id,
  172. message: this.message,
  173. payload: this.payload,
  174. })
  175. )
  176. .should.equal(true)
  177. })
  178. })
  179. describe('emitToAll', function () {
  180. beforeEach(function () {
  181. this.WebsocketLoadBalancer.emitToRoom = sinon.stub()
  182. return this.WebsocketLoadBalancer.emitToAll(
  183. this.message,
  184. ...Array.from(this.payload)
  185. )
  186. })
  187. return it("should emit to the room 'all'", function () {
  188. return this.WebsocketLoadBalancer.emitToRoom
  189. .calledWith('all', this.message, ...Array.from(this.payload))
  190. .should.equal(true)
  191. })
  192. })
  193. describe('listenForEditorEvents', function () {
  194. beforeEach(function () {
  195. this.WebsocketLoadBalancer._processEditorEvent = sinon.stub()
  196. return this.WebsocketLoadBalancer.listenForEditorEvents()
  197. })
  198. it('should subscribe to the editor-events channel', function () {
  199. return this.WebsocketLoadBalancer.rclientSubList[0].subscribe
  200. .calledWith('editor-events')
  201. .should.equal(true)
  202. })
  203. return it('should process the events with _processEditorEvent', function () {
  204. return this.WebsocketLoadBalancer.rclientSubList[0].on
  205. .calledWith('message', sinon.match.func)
  206. .should.equal(true)
  207. })
  208. })
  209. return describe('_processEditorEvent', function () {
  210. describe('with bad JSON', function () {
  211. beforeEach(function () {
  212. this.isRestrictedUser = false
  213. this.SafeJsonParse.parse = sinon
  214. .stub()
  215. .callsArgWith(1, new Error('oops'))
  216. return this.WebsocketLoadBalancer._processEditorEvent(
  217. this.io,
  218. 'editor-events',
  219. 'blah'
  220. )
  221. })
  222. return it('should log an error', function () {
  223. return this.logger.error.called.should.equal(true)
  224. })
  225. })
  226. describe('with a designated room', function () {
  227. beforeEach(function () {
  228. this.io.sockets = {
  229. clients: sinon.stub().returns([
  230. {
  231. id: 'client-id-1',
  232. emit: (this.emit1 = sinon.stub()),
  233. ol_context: {},
  234. },
  235. {
  236. id: 'client-id-2',
  237. emit: (this.emit2 = sinon.stub()),
  238. ol_context: {},
  239. },
  240. {
  241. id: 'client-id-1',
  242. emit: (this.emit3 = sinon.stub()),
  243. ol_context: {},
  244. }, // duplicate client
  245. ]),
  246. }
  247. const data = JSON.stringify({
  248. room_id: this.room_id,
  249. message: this.message,
  250. payload: this.payload,
  251. })
  252. return this.WebsocketLoadBalancer._processEditorEvent(
  253. this.io,
  254. 'editor-events',
  255. data
  256. )
  257. })
  258. return it('should send the message to all (unique) clients in the room', function () {
  259. this.io.sockets.clients.calledWith(this.room_id).should.equal(true)
  260. this.emit1
  261. .calledWith(this.message, ...Array.from(this.payload))
  262. .should.equal(true)
  263. this.emit2
  264. .calledWith(this.message, ...Array.from(this.payload))
  265. .should.equal(true)
  266. return this.emit3.called.should.equal(false)
  267. })
  268. }) // duplicate client should be ignored
  269. describe('with a designated room, and restricted clients, not restricted message', function () {
  270. beforeEach(function () {
  271. this.io.sockets = {
  272. clients: sinon.stub().returns([
  273. {
  274. id: 'client-id-1',
  275. emit: (this.emit1 = sinon.stub()),
  276. ol_context: {},
  277. },
  278. {
  279. id: 'client-id-2',
  280. emit: (this.emit2 = sinon.stub()),
  281. ol_context: {},
  282. },
  283. {
  284. id: 'client-id-1',
  285. emit: (this.emit3 = sinon.stub()),
  286. ol_context: {},
  287. }, // duplicate client
  288. {
  289. id: 'client-id-4',
  290. emit: (this.emit4 = sinon.stub()),
  291. ol_context: { is_restricted_user: true },
  292. },
  293. ]),
  294. }
  295. const data = JSON.stringify({
  296. room_id: this.room_id,
  297. message: this.message,
  298. payload: this.payload,
  299. })
  300. return this.WebsocketLoadBalancer._processEditorEvent(
  301. this.io,
  302. 'editor-events',
  303. data
  304. )
  305. })
  306. return it('should send the message to all (unique) clients in the room', function () {
  307. this.io.sockets.clients.calledWith(this.room_id).should.equal(true)
  308. this.emit1
  309. .calledWith(this.message, ...Array.from(this.payload))
  310. .should.equal(true)
  311. this.emit2
  312. .calledWith(this.message, ...Array.from(this.payload))
  313. .should.equal(true)
  314. this.emit3.called.should.equal(false) // duplicate client should be ignored
  315. return this.emit4.called.should.equal(true)
  316. })
  317. }) // restricted client, but should be called
  318. describe('with a designated room, and restricted clients, restricted message', function () {
  319. beforeEach(function () {
  320. this.io.sockets = {
  321. clients: sinon.stub().returns([
  322. {
  323. id: 'client-id-1',
  324. emit: (this.emit1 = sinon.stub()),
  325. ol_context: {},
  326. },
  327. {
  328. id: 'client-id-2',
  329. emit: (this.emit2 = sinon.stub()),
  330. ol_context: {},
  331. },
  332. {
  333. id: 'client-id-1',
  334. emit: (this.emit3 = sinon.stub()),
  335. ol_context: {},
  336. }, // duplicate client
  337. {
  338. id: 'client-id-4',
  339. emit: (this.emit4 = sinon.stub()),
  340. ol_context: { is_restricted_user: true },
  341. },
  342. ]),
  343. }
  344. const data = JSON.stringify({
  345. room_id: this.room_id,
  346. message: (this.restrictedMessage = 'new-comment'),
  347. payload: this.payload,
  348. })
  349. return this.WebsocketLoadBalancer._processEditorEvent(
  350. this.io,
  351. 'editor-events',
  352. data
  353. )
  354. })
  355. return it('should send the message to all (unique) clients in the room, who are not restricted', function () {
  356. this.io.sockets.clients.calledWith(this.room_id).should.equal(true)
  357. this.emit1
  358. .calledWith(this.restrictedMessage, ...Array.from(this.payload))
  359. .should.equal(true)
  360. this.emit2
  361. .calledWith(this.restrictedMessage, ...Array.from(this.payload))
  362. .should.equal(true)
  363. this.emit3.called.should.equal(false) // duplicate client should be ignored
  364. return this.emit4.called.should.equal(false)
  365. })
  366. }) // restricted client, should not be called
  367. describe('when emitting to all', function () {
  368. beforeEach(function () {
  369. this.io.sockets = { emit: (this.emit = sinon.stub()) }
  370. const data = JSON.stringify({
  371. room_id: 'all',
  372. message: this.message,
  373. payload: this.payload,
  374. })
  375. return this.WebsocketLoadBalancer._processEditorEvent(
  376. this.io,
  377. 'editor-events',
  378. data
  379. )
  380. })
  381. return it('should send the message to all clients', function () {
  382. return this.emit
  383. .calledWith(this.message, ...Array.from(this.payload))
  384. .should.equal(true)
  385. })
  386. })
  387. describe('when it should disconnect one of the clients', function () {
  388. const targetUserId = 'bbb'
  389. const message = 'userRemovedFromProject'
  390. const payload = [`${targetUserId}`]
  391. const clients = [
  392. {
  393. id: 'client-id-1',
  394. emit: sinon.stub(),
  395. ol_context: { user_id: 'aaa' },
  396. disconnect: sinon.stub(),
  397. },
  398. {
  399. id: 'client-id-2',
  400. emit: sinon.stub(),
  401. ol_context: { user_id: `${targetUserId}` },
  402. disconnect: sinon.stub(),
  403. },
  404. {
  405. id: 'client-id-3',
  406. emit: sinon.stub(),
  407. ol_context: { user_id: 'ccc' },
  408. disconnect: sinon.stub(),
  409. },
  410. ]
  411. beforeEach(function () {
  412. this.io.sockets = {
  413. clients: sinon.stub().returns(clients),
  414. }
  415. const data = JSON.stringify({
  416. room_id: this.room_id,
  417. message,
  418. payload,
  419. })
  420. return this.WebsocketLoadBalancer._processEditorEvent(
  421. this.io,
  422. 'editor-events',
  423. data
  424. )
  425. })
  426. it('should disconnect the matching client, while sending message to other clients', function () {
  427. this.io.sockets.clients.calledWith(this.room_id).should.equal(true)
  428. const [client1, client2, client3] = clients
  429. // disconnecting one client
  430. client1.disconnect.called.should.equal(false)
  431. client2.disconnect.called.should.equal(true)
  432. client3.disconnect.called.should.equal(false)
  433. // emitting to remaining clients
  434. client1.emit
  435. .calledWith(message, ...Array.from(payload))
  436. .should.equal(true)
  437. client2.emit.calledWith('project:access:revoked').should.equal(true) // disconnected client should get informative message
  438. client3.emit
  439. .calledWith(message, ...Array.from(payload))
  440. .should.equal(true)
  441. })
  442. })
  443. })
  444. })