2020-06-23 13:30:03 -04:00
|
|
|
/* eslint-disable
|
|
|
|
no-return-assign,
|
|
|
|
no-unused-vars,
|
|
|
|
*/
|
|
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
|
|
// Fix any style issues and re-enable lint.
|
2020-06-23 13:29:59 -04:00
|
|
|
/*
|
|
|
|
* decaffeinate suggestions:
|
|
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
|
|
*/
|
2021-03-18 16:19:31 -04:00
|
|
|
const { expect } = require('chai')
|
2020-06-23 13:30:16 -04:00
|
|
|
const sinon = require('sinon')
|
|
|
|
const modulePath = '../../../app/js/ChannelManager.js'
|
|
|
|
const SandboxedModule = require('sandboxed-module')
|
|
|
|
|
|
|
|
describe('ChannelManager', function () {
|
|
|
|
beforeEach(function () {
|
|
|
|
this.rclient = {}
|
|
|
|
this.other_rclient = {}
|
|
|
|
return (this.ChannelManager = SandboxedModule.require(modulePath, {
|
|
|
|
requires: {
|
2021-07-12 12:47:18 -04:00
|
|
|
'@overleaf/settings': (this.settings = {}),
|
2020-11-25 06:57:22 -05:00
|
|
|
'@overleaf/metrics': (this.metrics = {
|
2020-06-23 13:30:16 -04:00
|
|
|
inc: sinon.stub(),
|
|
|
|
summary: sinon.stub()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('subscribe', function () {
|
|
|
|
describe('when there is no existing subscription for this redis client', function () {
|
|
|
|
beforeEach(function (done) {
|
|
|
|
this.rclient.subscribe = sinon.stub().resolves()
|
|
|
|
this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
return setTimeout(done)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should subscribe to the redis channel', function () {
|
|
|
|
return this.rclient.subscribe
|
|
|
|
.calledWithExactly('applied-ops:1234567890abcdef')
|
|
|
|
.should.equal(true)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('when there is an existing subscription for this redis client', function () {
|
|
|
|
beforeEach(function (done) {
|
|
|
|
this.rclient.subscribe = sinon.stub().resolves()
|
|
|
|
this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
return setTimeout(done)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should subscribe to the redis channel again', function () {
|
|
|
|
return this.rclient.subscribe.callCount.should.equal(2)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('when subscribe errors', function () {
|
|
|
|
beforeEach(function (done) {
|
|
|
|
this.rclient.subscribe = sinon
|
|
|
|
.stub()
|
|
|
|
.onFirstCall()
|
|
|
|
.rejects(new Error('some redis error'))
|
|
|
|
.onSecondCall()
|
|
|
|
.resolves()
|
|
|
|
const p = this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
p.then(() => done(new Error('should not subscribe but fail'))).catch(
|
|
|
|
(err) => {
|
2020-08-21 07:15:35 -04:00
|
|
|
err.message.should.equal('failed to subscribe to channel')
|
|
|
|
err.cause.message.should.equal('some redis error')
|
2020-06-23 13:30:16 -04:00
|
|
|
this.ChannelManager.getClientMapEntry(this.rclient)
|
|
|
|
.has('applied-ops:1234567890abcdef')
|
|
|
|
.should.equal(false)
|
|
|
|
this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
// subscribe is wrapped in Promise, delay other assertions
|
|
|
|
return setTimeout(done)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
it('should have recorded the error', function () {
|
|
|
|
return expect(
|
|
|
|
this.metrics.inc.calledWithExactly('subscribe.failed.applied-ops')
|
|
|
|
).to.equal(true)
|
|
|
|
})
|
|
|
|
|
|
|
|
it('should subscribe again', function () {
|
|
|
|
return this.rclient.subscribe.callCount.should.equal(2)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should cleanup', function () {
|
|
|
|
return this.ChannelManager.getClientMapEntry(this.rclient)
|
|
|
|
.has('applied-ops:1234567890abcdef')
|
|
|
|
.should.equal(false)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('when subscribe errors and the clientChannelMap entry was replaced', function () {
|
|
|
|
beforeEach(function (done) {
|
|
|
|
this.rclient.subscribe = sinon
|
|
|
|
.stub()
|
|
|
|
.onFirstCall()
|
|
|
|
.rejects(new Error('some redis error'))
|
|
|
|
.onSecondCall()
|
|
|
|
.resolves()
|
|
|
|
this.first = this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
// ignore error
|
|
|
|
this.first.catch(() => {})
|
|
|
|
expect(
|
|
|
|
this.ChannelManager.getClientMapEntry(this.rclient).get(
|
|
|
|
'applied-ops:1234567890abcdef'
|
|
|
|
)
|
|
|
|
).to.equal(this.first)
|
|
|
|
|
|
|
|
this.rclient.unsubscribe = sinon.stub().resolves()
|
|
|
|
this.ChannelManager.unsubscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
this.second = this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
// should get replaced immediately
|
|
|
|
expect(
|
|
|
|
this.ChannelManager.getClientMapEntry(this.rclient).get(
|
|
|
|
'applied-ops:1234567890abcdef'
|
|
|
|
)
|
|
|
|
).to.equal(this.second)
|
|
|
|
|
|
|
|
// let the first subscribe error -> unsubscribe -> subscribe
|
|
|
|
return setTimeout(done)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should cleanup the second subscribePromise', function () {
|
|
|
|
return expect(
|
|
|
|
this.ChannelManager.getClientMapEntry(this.rclient).has(
|
|
|
|
'applied-ops:1234567890abcdef'
|
|
|
|
)
|
|
|
|
).to.equal(false)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
return describe('when there is an existing subscription for another redis client but not this one', function () {
|
|
|
|
beforeEach(function (done) {
|
|
|
|
this.other_rclient.subscribe = sinon.stub().resolves()
|
|
|
|
this.ChannelManager.subscribe(
|
|
|
|
this.other_rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
this.rclient.subscribe = sinon.stub().resolves() // discard the original stub
|
|
|
|
this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
return setTimeout(done)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should subscribe to the redis channel on this redis client', function () {
|
|
|
|
return this.rclient.subscribe
|
|
|
|
.calledWithExactly('applied-ops:1234567890abcdef')
|
|
|
|
.should.equal(true)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('unsubscribe', function () {
|
|
|
|
describe('when there is no existing subscription for this redis client', function () {
|
|
|
|
beforeEach(function (done) {
|
|
|
|
this.rclient.unsubscribe = sinon.stub().resolves()
|
|
|
|
this.ChannelManager.unsubscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
return setTimeout(done)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should unsubscribe from the redis channel', function () {
|
|
|
|
return this.rclient.unsubscribe.called.should.equal(true)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('when there is an existing subscription for this another redis client but not this one', function () {
|
|
|
|
beforeEach(function (done) {
|
|
|
|
this.other_rclient.subscribe = sinon.stub().resolves()
|
|
|
|
this.rclient.unsubscribe = sinon.stub().resolves()
|
|
|
|
this.ChannelManager.subscribe(
|
|
|
|
this.other_rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
this.ChannelManager.unsubscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
return setTimeout(done)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should still unsubscribe from the redis channel on this client', function () {
|
|
|
|
return this.rclient.unsubscribe.called.should.equal(true)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('when unsubscribe errors and completes', function () {
|
|
|
|
beforeEach(function (done) {
|
|
|
|
this.rclient.subscribe = sinon.stub().resolves()
|
|
|
|
this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
this.rclient.unsubscribe = sinon
|
|
|
|
.stub()
|
|
|
|
.rejects(new Error('some redis error'))
|
|
|
|
this.ChannelManager.unsubscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
setTimeout(done)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
it('should have cleaned up', function () {
|
|
|
|
return this.ChannelManager.getClientMapEntry(this.rclient)
|
|
|
|
.has('applied-ops:1234567890abcdef')
|
|
|
|
.should.equal(false)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should not error out when subscribing again', function (done) {
|
|
|
|
const p = this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
p.then(() => done()).catch(done)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('when unsubscribe errors and another client subscribes at the same time', function () {
|
|
|
|
beforeEach(function (done) {
|
|
|
|
this.rclient.subscribe = sinon.stub().resolves()
|
|
|
|
this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
let rejectSubscribe
|
|
|
|
this.rclient.unsubscribe = () =>
|
|
|
|
new Promise((resolve, reject) => (rejectSubscribe = reject))
|
|
|
|
this.ChannelManager.unsubscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
// delay, actualUnsubscribe should not see the new subscribe request
|
|
|
|
this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
.then(() => setTimeout(done))
|
|
|
|
.catch(done)
|
|
|
|
return setTimeout(() =>
|
|
|
|
// delay, rejectSubscribe is not defined immediately
|
|
|
|
rejectSubscribe(new Error('redis error'))
|
|
|
|
)
|
|
|
|
})
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
it('should have recorded the error', function () {
|
|
|
|
return expect(
|
|
|
|
this.metrics.inc.calledWithExactly('unsubscribe.failed.applied-ops')
|
|
|
|
).to.equal(true)
|
|
|
|
})
|
|
|
|
|
|
|
|
it('should have subscribed', function () {
|
|
|
|
return this.rclient.subscribe.called.should.equal(true)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should have discarded the finished Promise', function () {
|
|
|
|
return this.ChannelManager.getClientMapEntry(this.rclient)
|
|
|
|
.has('applied-ops:1234567890abcdef')
|
|
|
|
.should.equal(false)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
return describe('when there is an existing subscription for this redis client', function () {
|
|
|
|
beforeEach(function (done) {
|
|
|
|
this.rclient.subscribe = sinon.stub().resolves()
|
|
|
|
this.rclient.unsubscribe = sinon.stub().resolves()
|
|
|
|
this.ChannelManager.subscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
this.ChannelManager.unsubscribe(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef'
|
|
|
|
)
|
|
|
|
return setTimeout(done)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should unsubscribe from the redis channel', function () {
|
|
|
|
return this.rclient.unsubscribe
|
|
|
|
.calledWithExactly('applied-ops:1234567890abcdef')
|
|
|
|
.should.equal(true)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
return describe('publish', function () {
|
|
|
|
describe("when the channel is 'all'", function () {
|
|
|
|
beforeEach(function () {
|
|
|
|
this.rclient.publish = sinon.stub()
|
|
|
|
return this.ChannelManager.publish(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'all',
|
|
|
|
'random-message'
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should publish on the base channel', function () {
|
|
|
|
return this.rclient.publish
|
|
|
|
.calledWithExactly('applied-ops', 'random-message')
|
|
|
|
.should.equal(true)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe('when the channel has an specific id', function () {
|
|
|
|
describe('when the individual channel setting is false', function () {
|
|
|
|
beforeEach(function () {
|
|
|
|
this.rclient.publish = sinon.stub()
|
|
|
|
this.settings.publishOnIndividualChannels = false
|
|
|
|
return this.ChannelManager.publish(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef',
|
|
|
|
'random-message'
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should publish on the per-id channel', function () {
|
|
|
|
this.rclient.publish
|
|
|
|
.calledWithExactly('applied-ops', 'random-message')
|
|
|
|
.should.equal(true)
|
|
|
|
return this.rclient.publish.calledOnce.should.equal(true)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
return describe('when the individual channel setting is true', function () {
|
|
|
|
beforeEach(function () {
|
|
|
|
this.rclient.publish = sinon.stub()
|
|
|
|
this.settings.publishOnIndividualChannels = true
|
|
|
|
return this.ChannelManager.publish(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'1234567890abcdef',
|
|
|
|
'random-message'
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should publish on the per-id channel', function () {
|
|
|
|
this.rclient.publish
|
|
|
|
.calledWithExactly('applied-ops:1234567890abcdef', 'random-message')
|
|
|
|
.should.equal(true)
|
|
|
|
return this.rclient.publish.calledOnce.should.equal(true)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
return describe('metrics', function () {
|
|
|
|
beforeEach(function () {
|
|
|
|
this.rclient.publish = sinon.stub()
|
|
|
|
return this.ChannelManager.publish(
|
|
|
|
this.rclient,
|
|
|
|
'applied-ops',
|
|
|
|
'all',
|
|
|
|
'random-message'
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it('should track the payload size', function () {
|
|
|
|
return this.metrics.summary
|
|
|
|
.calledWithExactly(
|
|
|
|
'redis.publish.applied-ops',
|
|
|
|
'random-message'.length
|
|
|
|
)
|
|
|
|
.should.equal(true)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|