WebsocketLoadBalancerTests.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  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. './RedisClientManager': {
  26. createClientList: () => [],
  27. },
  28. './SafeJsonParse': (this.SafeJsonParse = {
  29. parse: (data, cb) => cb(null, JSON.parse(data)),
  30. }),
  31. './EventLogger': { checkEventOrder: sinon.stub() },
  32. './HealthCheckManager': { check: sinon.stub() },
  33. './RoomManager': (this.RoomManager = {
  34. eventSource: sinon.stub().returns(this.RoomEvents),
  35. }),
  36. './ChannelManager': (this.ChannelManager = { publish: sinon.stub() }),
  37. './ConnectedUsersManager': (this.ConnectedUsersManager = {
  38. refreshClient: sinon.stub(),
  39. }),
  40. },
  41. })
  42. this.io = {}
  43. this.WebsocketLoadBalancer.rclientPubList = [{ publish: sinon.stub() }]
  44. this.WebsocketLoadBalancer.rclientSubList = [
  45. {
  46. subscribe: sinon.stub(),
  47. on: sinon.stub(),
  48. },
  49. ]
  50. this.room_id = 'room-id'
  51. this.message = 'otUpdateApplied'
  52. return (this.payload = ['argument one', 42])
  53. })
  54. describe('shouldDisconnectClient', function () {
  55. const client = {
  56. ol_context: { user_id: 'abcd' },
  57. }
  58. it('should return false for general messages', function () {
  59. const message = {
  60. message: 'someNiceMessage',
  61. payload: [{ data: 'whatever' }],
  62. }
  63. expect(
  64. this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
  65. ).to.equal(false)
  66. })
  67. it('should return false for userRemovedFromProject, when the user_id does not match', function () {
  68. const message = {
  69. message: 'userRemovedFromProject',
  70. payload: ['xyz'],
  71. }
  72. expect(
  73. this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
  74. ).to.equal(false)
  75. })
  76. it('should return true for userRemovedFromProject, if the user_id matches', function () {
  77. const message = {
  78. message: 'userRemovedFromProject',
  79. payload: [`${client.ol_context.user_id}`],
  80. }
  81. expect(
  82. this.WebsocketLoadBalancer.shouldDisconnectClient(client, message)
  83. ).to.equal(true)
  84. })
  85. })
  86. describe('emitToRoom', function () {
  87. beforeEach(function () {
  88. return this.WebsocketLoadBalancer.emitToRoom(
  89. this.room_id,
  90. this.message,
  91. ...Array.from(this.payload)
  92. )
  93. })
  94. return it('should publish the message to redis', function () {
  95. return this.ChannelManager.publish
  96. .calledWith(
  97. this.WebsocketLoadBalancer.rclientPubList[0],
  98. 'editor-events',
  99. this.room_id,
  100. JSON.stringify({
  101. room_id: this.room_id,
  102. message: this.message,
  103. payload: this.payload,
  104. })
  105. )
  106. .should.equal(true)
  107. })
  108. })
  109. describe('emitToAll', function () {
  110. beforeEach(function () {
  111. this.WebsocketLoadBalancer.emitToRoom = sinon.stub()
  112. return this.WebsocketLoadBalancer.emitToAll(
  113. this.message,
  114. ...Array.from(this.payload)
  115. )
  116. })
  117. return it("should emit to the room 'all'", function () {
  118. return this.WebsocketLoadBalancer.emitToRoom
  119. .calledWith('all', this.message, ...Array.from(this.payload))
  120. .should.equal(true)
  121. })
  122. })
  123. describe('listenForEditorEvents', function () {
  124. beforeEach(function () {
  125. this.WebsocketLoadBalancer._processEditorEvent = sinon.stub()
  126. return this.WebsocketLoadBalancer.listenForEditorEvents()
  127. })
  128. it('should subscribe to the editor-events channel', function () {
  129. return this.WebsocketLoadBalancer.rclientSubList[0].subscribe
  130. .calledWith('editor-events')
  131. .should.equal(true)
  132. })
  133. return it('should process the events with _processEditorEvent', function () {
  134. return this.WebsocketLoadBalancer.rclientSubList[0].on
  135. .calledWith('message', sinon.match.func)
  136. .should.equal(true)
  137. })
  138. })
  139. return describe('_processEditorEvent', function () {
  140. describe('with bad JSON', function () {
  141. beforeEach(function () {
  142. this.isRestrictedUser = false
  143. this.SafeJsonParse.parse = sinon
  144. .stub()
  145. .callsArgWith(1, new Error('oops'))
  146. return this.WebsocketLoadBalancer._processEditorEvent(
  147. this.io,
  148. 'editor-events',
  149. 'blah'
  150. )
  151. })
  152. return it('should log an error', function () {
  153. return this.logger.error.called.should.equal(true)
  154. })
  155. })
  156. describe('with a designated room', function () {
  157. beforeEach(function () {
  158. this.io.sockets = {
  159. clients: sinon.stub().returns([
  160. {
  161. id: 'client-id-1',
  162. emit: (this.emit1 = sinon.stub()),
  163. ol_context: {},
  164. },
  165. {
  166. id: 'client-id-2',
  167. emit: (this.emit2 = sinon.stub()),
  168. ol_context: {},
  169. },
  170. {
  171. id: 'client-id-1',
  172. emit: (this.emit3 = sinon.stub()),
  173. ol_context: {},
  174. }, // duplicate client
  175. ]),
  176. }
  177. const data = JSON.stringify({
  178. room_id: this.room_id,
  179. message: this.message,
  180. payload: this.payload,
  181. })
  182. return this.WebsocketLoadBalancer._processEditorEvent(
  183. this.io,
  184. 'editor-events',
  185. data
  186. )
  187. })
  188. return it('should send the message to all (unique) clients in the room', function () {
  189. this.io.sockets.clients.calledWith(this.room_id).should.equal(true)
  190. this.emit1
  191. .calledWith(this.message, ...Array.from(this.payload))
  192. .should.equal(true)
  193. this.emit2
  194. .calledWith(this.message, ...Array.from(this.payload))
  195. .should.equal(true)
  196. return this.emit3.called.should.equal(false)
  197. })
  198. }) // duplicate client should be ignored
  199. describe('with a designated room, and restricted clients, not restricted message', function () {
  200. beforeEach(function () {
  201. this.io.sockets = {
  202. clients: sinon.stub().returns([
  203. {
  204. id: 'client-id-1',
  205. emit: (this.emit1 = sinon.stub()),
  206. ol_context: {},
  207. },
  208. {
  209. id: 'client-id-2',
  210. emit: (this.emit2 = sinon.stub()),
  211. ol_context: {},
  212. },
  213. {
  214. id: 'client-id-1',
  215. emit: (this.emit3 = sinon.stub()),
  216. ol_context: {},
  217. }, // duplicate client
  218. {
  219. id: 'client-id-4',
  220. emit: (this.emit4 = sinon.stub()),
  221. ol_context: { is_restricted_user: true },
  222. },
  223. ]),
  224. }
  225. const data = JSON.stringify({
  226. room_id: this.room_id,
  227. message: this.message,
  228. payload: this.payload,
  229. })
  230. return this.WebsocketLoadBalancer._processEditorEvent(
  231. this.io,
  232. 'editor-events',
  233. data
  234. )
  235. })
  236. return it('should send the message to all (unique) clients in the room', function () {
  237. this.io.sockets.clients.calledWith(this.room_id).should.equal(true)
  238. this.emit1
  239. .calledWith(this.message, ...Array.from(this.payload))
  240. .should.equal(true)
  241. this.emit2
  242. .calledWith(this.message, ...Array.from(this.payload))
  243. .should.equal(true)
  244. this.emit3.called.should.equal(false) // duplicate client should be ignored
  245. return this.emit4.called.should.equal(true)
  246. })
  247. }) // restricted client, but should be called
  248. describe('with a designated room, and restricted clients, restricted message', function () {
  249. beforeEach(function () {
  250. this.io.sockets = {
  251. clients: sinon.stub().returns([
  252. {
  253. id: 'client-id-1',
  254. emit: (this.emit1 = sinon.stub()),
  255. ol_context: {},
  256. },
  257. {
  258. id: 'client-id-2',
  259. emit: (this.emit2 = sinon.stub()),
  260. ol_context: {},
  261. },
  262. {
  263. id: 'client-id-1',
  264. emit: (this.emit3 = sinon.stub()),
  265. ol_context: {},
  266. }, // duplicate client
  267. {
  268. id: 'client-id-4',
  269. emit: (this.emit4 = sinon.stub()),
  270. ol_context: { is_restricted_user: true },
  271. },
  272. ]),
  273. }
  274. const data = JSON.stringify({
  275. room_id: this.room_id,
  276. message: (this.restrictedMessage = 'new-comment'),
  277. payload: this.payload,
  278. })
  279. return this.WebsocketLoadBalancer._processEditorEvent(
  280. this.io,
  281. 'editor-events',
  282. data
  283. )
  284. })
  285. return it('should send the message to all (unique) clients in the room, who are not restricted', function () {
  286. this.io.sockets.clients.calledWith(this.room_id).should.equal(true)
  287. this.emit1
  288. .calledWith(this.restrictedMessage, ...Array.from(this.payload))
  289. .should.equal(true)
  290. this.emit2
  291. .calledWith(this.restrictedMessage, ...Array.from(this.payload))
  292. .should.equal(true)
  293. this.emit3.called.should.equal(false) // duplicate client should be ignored
  294. return this.emit4.called.should.equal(false)
  295. })
  296. }) // restricted client, should not be called
  297. describe('when emitting to all', function () {
  298. beforeEach(function () {
  299. this.io.sockets = { emit: (this.emit = sinon.stub()) }
  300. const data = JSON.stringify({
  301. room_id: 'all',
  302. message: this.message,
  303. payload: this.payload,
  304. })
  305. return this.WebsocketLoadBalancer._processEditorEvent(
  306. this.io,
  307. 'editor-events',
  308. data
  309. )
  310. })
  311. return it('should send the message to all clients', function () {
  312. return this.emit
  313. .calledWith(this.message, ...Array.from(this.payload))
  314. .should.equal(true)
  315. })
  316. })
  317. describe('when it should disconnect one of the clients', function () {
  318. const targetUserId = 'bbb'
  319. const message = 'userRemovedFromProject'
  320. const payload = [`${targetUserId}`]
  321. const clients = [
  322. {
  323. id: 'client-id-1',
  324. emit: sinon.stub(),
  325. ol_context: { user_id: 'aaa' },
  326. disconnect: sinon.stub(),
  327. },
  328. {
  329. id: 'client-id-2',
  330. emit: sinon.stub(),
  331. ol_context: { user_id: `${targetUserId}` },
  332. disconnect: sinon.stub(),
  333. },
  334. {
  335. id: 'client-id-3',
  336. emit: sinon.stub(),
  337. ol_context: { user_id: 'ccc' },
  338. disconnect: sinon.stub(),
  339. },
  340. ]
  341. beforeEach(function () {
  342. this.io.sockets = {
  343. clients: sinon.stub().returns(clients),
  344. }
  345. const data = JSON.stringify({
  346. room_id: this.room_id,
  347. message,
  348. payload,
  349. })
  350. return this.WebsocketLoadBalancer._processEditorEvent(
  351. this.io,
  352. 'editor-events',
  353. data
  354. )
  355. })
  356. it('should disconnect the matching client, while sending message to other clients', function () {
  357. this.io.sockets.clients.calledWith(this.room_id).should.equal(true)
  358. const [client1, client2, client3] = clients
  359. // disconnecting one client
  360. client1.disconnect.called.should.equal(false)
  361. client2.disconnect.called.should.equal(true)
  362. client3.disconnect.called.should.equal(false)
  363. // emitting to remaining clients
  364. client1.emit
  365. .calledWith(message, ...Array.from(payload))
  366. .should.equal(true)
  367. client2.emit.called.should.equal(false) // disconnected client should not be called
  368. client3.emit
  369. .calledWith(message, ...Array.from(payload))
  370. .should.equal(true)
  371. })
  372. })
  373. })
  374. })