ChannelManagerTests.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. /* eslint-disable
  2. no-return-assign,
  3. no-unused-vars,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  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 { expect } = require('chai')
  13. const sinon = require('sinon')
  14. const modulePath = '../../../app/js/ChannelManager.js'
  15. const SandboxedModule = require('sandboxed-module')
  16. describe('ChannelManager', function () {
  17. beforeEach(function () {
  18. this.rclient = {}
  19. this.other_rclient = {}
  20. return (this.ChannelManager = SandboxedModule.require(modulePath, {
  21. requires: {
  22. '@overleaf/settings': (this.settings = {}),
  23. '@overleaf/metrics': (this.metrics = {
  24. inc: sinon.stub(),
  25. summary: sinon.stub(),
  26. }),
  27. },
  28. }))
  29. })
  30. describe('subscribe', function () {
  31. describe('when there is no existing subscription for this redis client', function () {
  32. beforeEach(function (done) {
  33. this.rclient.subscribe = sinon.stub().resolves()
  34. this.ChannelManager.subscribe(
  35. this.rclient,
  36. 'applied-ops',
  37. '1234567890abcdef'
  38. )
  39. return setTimeout(done)
  40. })
  41. return it('should subscribe to the redis channel', function () {
  42. return this.rclient.subscribe
  43. .calledWithExactly('applied-ops:1234567890abcdef')
  44. .should.equal(true)
  45. })
  46. })
  47. describe('when there is an existing subscription for this redis client', function () {
  48. beforeEach(function (done) {
  49. this.rclient.subscribe = sinon.stub().resolves()
  50. this.ChannelManager.subscribe(
  51. this.rclient,
  52. 'applied-ops',
  53. '1234567890abcdef'
  54. )
  55. this.ChannelManager.subscribe(
  56. this.rclient,
  57. 'applied-ops',
  58. '1234567890abcdef'
  59. )
  60. return setTimeout(done)
  61. })
  62. return it('should subscribe to the redis channel again', function () {
  63. return this.rclient.subscribe.callCount.should.equal(2)
  64. })
  65. })
  66. describe('when subscribe errors', function () {
  67. beforeEach(function (done) {
  68. this.rclient.subscribe = sinon
  69. .stub()
  70. .onFirstCall()
  71. .rejects(new Error('some redis error'))
  72. .onSecondCall()
  73. .resolves()
  74. const p = this.ChannelManager.subscribe(
  75. this.rclient,
  76. 'applied-ops',
  77. '1234567890abcdef'
  78. )
  79. p.then(() => done(new Error('should not subscribe but fail'))).catch(
  80. err => {
  81. err.message.should.equal('failed to subscribe to channel')
  82. err.cause.message.should.equal('some redis error')
  83. this.ChannelManager.getClientMapEntry(this.rclient)
  84. .has('applied-ops:1234567890abcdef')
  85. .should.equal(false)
  86. this.ChannelManager.subscribe(
  87. this.rclient,
  88. 'applied-ops',
  89. '1234567890abcdef'
  90. )
  91. // subscribe is wrapped in Promise, delay other assertions
  92. return setTimeout(done)
  93. }
  94. )
  95. return null
  96. })
  97. it('should have recorded the error', function () {
  98. return expect(
  99. this.metrics.inc.calledWithExactly('subscribe.failed.applied-ops')
  100. ).to.equal(true)
  101. })
  102. it('should subscribe again', function () {
  103. return this.rclient.subscribe.callCount.should.equal(2)
  104. })
  105. return it('should cleanup', function () {
  106. return this.ChannelManager.getClientMapEntry(this.rclient)
  107. .has('applied-ops:1234567890abcdef')
  108. .should.equal(false)
  109. })
  110. })
  111. describe('when subscribe errors and the clientChannelMap entry was replaced', function () {
  112. beforeEach(function (done) {
  113. this.rclient.subscribe = sinon
  114. .stub()
  115. .onFirstCall()
  116. .rejects(new Error('some redis error'))
  117. .onSecondCall()
  118. .resolves()
  119. this.first = this.ChannelManager.subscribe(
  120. this.rclient,
  121. 'applied-ops',
  122. '1234567890abcdef'
  123. )
  124. // ignore error
  125. this.first.catch(() => {})
  126. expect(
  127. this.ChannelManager.getClientMapEntry(this.rclient).get(
  128. 'applied-ops:1234567890abcdef'
  129. )
  130. ).to.equal(this.first)
  131. this.rclient.unsubscribe = sinon.stub().resolves()
  132. this.ChannelManager.unsubscribe(
  133. this.rclient,
  134. 'applied-ops',
  135. '1234567890abcdef'
  136. )
  137. this.second = this.ChannelManager.subscribe(
  138. this.rclient,
  139. 'applied-ops',
  140. '1234567890abcdef'
  141. )
  142. // should get replaced immediately
  143. expect(
  144. this.ChannelManager.getClientMapEntry(this.rclient).get(
  145. 'applied-ops:1234567890abcdef'
  146. )
  147. ).to.equal(this.second)
  148. // let the first subscribe error -> unsubscribe -> subscribe
  149. return setTimeout(done)
  150. })
  151. return it('should cleanup the second subscribePromise', function () {
  152. return expect(
  153. this.ChannelManager.getClientMapEntry(this.rclient).has(
  154. 'applied-ops:1234567890abcdef'
  155. )
  156. ).to.equal(false)
  157. })
  158. })
  159. return describe('when there is an existing subscription for another redis client but not this one', function () {
  160. beforeEach(function (done) {
  161. this.other_rclient.subscribe = sinon.stub().resolves()
  162. this.ChannelManager.subscribe(
  163. this.other_rclient,
  164. 'applied-ops',
  165. '1234567890abcdef'
  166. )
  167. this.rclient.subscribe = sinon.stub().resolves() // discard the original stub
  168. this.ChannelManager.subscribe(
  169. this.rclient,
  170. 'applied-ops',
  171. '1234567890abcdef'
  172. )
  173. return setTimeout(done)
  174. })
  175. return it('should subscribe to the redis channel on this redis client', function () {
  176. return this.rclient.subscribe
  177. .calledWithExactly('applied-ops:1234567890abcdef')
  178. .should.equal(true)
  179. })
  180. })
  181. })
  182. describe('unsubscribe', function () {
  183. describe('when there is no existing subscription for this redis client', function () {
  184. beforeEach(function (done) {
  185. this.rclient.unsubscribe = sinon.stub().resolves()
  186. this.ChannelManager.unsubscribe(
  187. this.rclient,
  188. 'applied-ops',
  189. '1234567890abcdef'
  190. )
  191. return setTimeout(done)
  192. })
  193. return it('should unsubscribe from the redis channel', function () {
  194. return this.rclient.unsubscribe.called.should.equal(true)
  195. })
  196. })
  197. describe('when there is an existing subscription for this another redis client but not this one', function () {
  198. beforeEach(function (done) {
  199. this.other_rclient.subscribe = sinon.stub().resolves()
  200. this.rclient.unsubscribe = sinon.stub().resolves()
  201. this.ChannelManager.subscribe(
  202. this.other_rclient,
  203. 'applied-ops',
  204. '1234567890abcdef'
  205. )
  206. this.ChannelManager.unsubscribe(
  207. this.rclient,
  208. 'applied-ops',
  209. '1234567890abcdef'
  210. )
  211. return setTimeout(done)
  212. })
  213. return it('should still unsubscribe from the redis channel on this client', function () {
  214. return this.rclient.unsubscribe.called.should.equal(true)
  215. })
  216. })
  217. describe('when unsubscribe errors and completes', function () {
  218. beforeEach(function (done) {
  219. this.rclient.subscribe = sinon.stub().resolves()
  220. this.ChannelManager.subscribe(
  221. this.rclient,
  222. 'applied-ops',
  223. '1234567890abcdef'
  224. )
  225. this.rclient.unsubscribe = sinon
  226. .stub()
  227. .rejects(new Error('some redis error'))
  228. this.ChannelManager.unsubscribe(
  229. this.rclient,
  230. 'applied-ops',
  231. '1234567890abcdef'
  232. )
  233. setTimeout(done)
  234. return null
  235. })
  236. it('should have cleaned up', function () {
  237. return this.ChannelManager.getClientMapEntry(this.rclient)
  238. .has('applied-ops:1234567890abcdef')
  239. .should.equal(false)
  240. })
  241. return it('should not error out when subscribing again', function (done) {
  242. const p = this.ChannelManager.subscribe(
  243. this.rclient,
  244. 'applied-ops',
  245. '1234567890abcdef'
  246. )
  247. p.then(() => done()).catch(done)
  248. return null
  249. })
  250. })
  251. describe('when unsubscribe errors and another client subscribes at the same time', function () {
  252. beforeEach(function (done) {
  253. this.rclient.subscribe = sinon.stub().resolves()
  254. this.ChannelManager.subscribe(
  255. this.rclient,
  256. 'applied-ops',
  257. '1234567890abcdef'
  258. )
  259. let rejectSubscribe
  260. this.rclient.unsubscribe = () =>
  261. new Promise((resolve, reject) => (rejectSubscribe = reject))
  262. this.ChannelManager.unsubscribe(
  263. this.rclient,
  264. 'applied-ops',
  265. '1234567890abcdef'
  266. )
  267. setTimeout(() => {
  268. // delay, actualUnsubscribe should not see the new subscribe request
  269. this.ChannelManager.subscribe(
  270. this.rclient,
  271. 'applied-ops',
  272. '1234567890abcdef'
  273. )
  274. .then(() => setTimeout(done))
  275. .catch(done)
  276. return setTimeout(() =>
  277. // delay, rejectSubscribe is not defined immediately
  278. rejectSubscribe(new Error('redis error'))
  279. )
  280. })
  281. return null
  282. })
  283. it('should have recorded the error', function () {
  284. return expect(
  285. this.metrics.inc.calledWithExactly('unsubscribe.failed.applied-ops')
  286. ).to.equal(true)
  287. })
  288. it('should have subscribed', function () {
  289. return this.rclient.subscribe.called.should.equal(true)
  290. })
  291. return it('should have discarded the finished Promise', function () {
  292. return this.ChannelManager.getClientMapEntry(this.rclient)
  293. .has('applied-ops:1234567890abcdef')
  294. .should.equal(false)
  295. })
  296. })
  297. return describe('when there is an existing subscription for this redis client', function () {
  298. beforeEach(function (done) {
  299. this.rclient.subscribe = sinon.stub().resolves()
  300. this.rclient.unsubscribe = sinon.stub().resolves()
  301. this.ChannelManager.subscribe(
  302. this.rclient,
  303. 'applied-ops',
  304. '1234567890abcdef'
  305. )
  306. this.ChannelManager.unsubscribe(
  307. this.rclient,
  308. 'applied-ops',
  309. '1234567890abcdef'
  310. )
  311. return setTimeout(done)
  312. })
  313. return it('should unsubscribe from the redis channel', function () {
  314. return this.rclient.unsubscribe
  315. .calledWithExactly('applied-ops:1234567890abcdef')
  316. .should.equal(true)
  317. })
  318. })
  319. })
  320. return describe('publish', function () {
  321. describe("when the channel is 'all'", function () {
  322. beforeEach(function () {
  323. this.rclient.publish = sinon.stub()
  324. return this.ChannelManager.publish(
  325. this.rclient,
  326. 'applied-ops',
  327. 'all',
  328. 'random-message'
  329. )
  330. })
  331. return it('should publish on the base channel', function () {
  332. return this.rclient.publish
  333. .calledWithExactly('applied-ops', 'random-message')
  334. .should.equal(true)
  335. })
  336. })
  337. describe('when the channel has an specific id', function () {
  338. describe('when the individual channel setting is false', function () {
  339. beforeEach(function () {
  340. this.rclient.publish = sinon.stub()
  341. this.settings.publishOnIndividualChannels = false
  342. return this.ChannelManager.publish(
  343. this.rclient,
  344. 'applied-ops',
  345. '1234567890abcdef',
  346. 'random-message'
  347. )
  348. })
  349. return it('should publish on the per-id channel', function () {
  350. this.rclient.publish
  351. .calledWithExactly('applied-ops', 'random-message')
  352. .should.equal(true)
  353. return this.rclient.publish.calledOnce.should.equal(true)
  354. })
  355. })
  356. return describe('when the individual channel setting is true', function () {
  357. beforeEach(function () {
  358. this.rclient.publish = sinon.stub()
  359. this.settings.publishOnIndividualChannels = true
  360. return this.ChannelManager.publish(
  361. this.rclient,
  362. 'applied-ops',
  363. '1234567890abcdef',
  364. 'random-message'
  365. )
  366. })
  367. return it('should publish on the per-id channel', function () {
  368. this.rclient.publish
  369. .calledWithExactly('applied-ops:1234567890abcdef', 'random-message')
  370. .should.equal(true)
  371. return this.rclient.publish.calledOnce.should.equal(true)
  372. })
  373. })
  374. })
  375. return describe('metrics', function () {
  376. beforeEach(function () {
  377. this.rclient.publish = sinon.stub()
  378. return this.ChannelManager.publish(
  379. this.rclient,
  380. 'applied-ops',
  381. 'all',
  382. 'random-message'
  383. )
  384. })
  385. return it('should track the payload size', function () {
  386. return this.metrics.summary
  387. .calledWithExactly(
  388. 'redis.publish.applied-ops',
  389. 'random-message'.length
  390. )
  391. .should.equal(true)
  392. })
  393. })
  394. })
  395. })