overleaf/services/real-time/test/unit/coffee/ChannelManagerTests.js

220 lines
8.9 KiB
JavaScript

chai = require('chai')
should = chai.should()
expect = chai.expect
sinon = require("sinon")
modulePath = "../../../app/js/ChannelManager.js"
SandboxedModule = require('sandboxed-module')
describe 'ChannelManager', ->
beforeEach ->
@rclient = {}
@other_rclient = {}
@ChannelManager = SandboxedModule.require modulePath, requires:
"settings-sharelatex": @settings = {}
"metrics-sharelatex": @metrics = {inc: sinon.stub(), summary: sinon.stub()}
"logger-sharelatex": @logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() }
describe "subscribe", ->
describe "when there is no existing subscription for this redis client", ->
beforeEach (done) ->
@rclient.subscribe = sinon.stub().resolves()
@ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
setTimeout done
it "should subscribe to the redis channel", ->
@rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal true
describe "when there is an existing subscription for this redis client", ->
beforeEach (done) ->
@rclient.subscribe = sinon.stub().resolves()
@ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
@ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
setTimeout done
it "should subscribe to the redis channel again", ->
@rclient.subscribe.callCount.should.equal 2
describe "when subscribe errors", ->
beforeEach (done) ->
@rclient.subscribe = sinon.stub()
.onFirstCall().rejects(new Error("some redis error"))
.onSecondCall().resolves()
p = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
p.then () ->
done(new Error('should not subscribe but fail'))
.catch (err) =>
err.message.should.equal "some redis error"
@ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false
@ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
# subscribe is wrapped in Promise, delay other assertions
setTimeout done
return null
it "should have recorded the error", ->
expect(@metrics.inc.calledWithExactly("subscribe.failed.applied-ops")).to.equal(true)
it "should subscribe again", ->
@rclient.subscribe.callCount.should.equal 2
it "should cleanup", ->
@ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false
describe "when subscribe errors and the clientChannelMap entry was replaced", ->
beforeEach (done) ->
@rclient.subscribe = sinon.stub()
.onFirstCall().rejects(new Error("some redis error"))
.onSecondCall().resolves()
@first = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
# ignore error
@first.catch((()->))
expect(@ChannelManager.getClientMapEntry(@rclient).get("applied-ops:1234567890abcdef")).to.equal @first
@rclient.unsubscribe = sinon.stub().resolves()
@ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef"
@second = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
# should get replaced immediately
expect(@ChannelManager.getClientMapEntry(@rclient).get("applied-ops:1234567890abcdef")).to.equal @second
# let the first subscribe error -> unsubscribe -> subscribe
setTimeout done
it "should cleanup the second subscribePromise", ->
expect(@ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef")).to.equal false
describe "when there is an existing subscription for another redis client but not this one", ->
beforeEach (done) ->
@other_rclient.subscribe = sinon.stub().resolves()
@ChannelManager.subscribe @other_rclient, "applied-ops", "1234567890abcdef"
@rclient.subscribe = sinon.stub().resolves() # discard the original stub
@ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
setTimeout done
it "should subscribe to the redis channel on this redis client", ->
@rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal true
describe "unsubscribe", ->
describe "when there is no existing subscription for this redis client", ->
beforeEach (done) ->
@rclient.unsubscribe = sinon.stub().resolves()
@ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef"
setTimeout done
it "should unsubscribe from the redis channel", ->
@rclient.unsubscribe.called.should.equal true
describe "when there is an existing subscription for this another redis client but not this one", ->
beforeEach (done) ->
@other_rclient.subscribe = sinon.stub().resolves()
@rclient.unsubscribe = sinon.stub().resolves()
@ChannelManager.subscribe @other_rclient, "applied-ops", "1234567890abcdef"
@ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef"
setTimeout done
it "should still unsubscribe from the redis channel on this client", ->
@rclient.unsubscribe.called.should.equal true
describe "when unsubscribe errors and completes", ->
beforeEach (done) ->
@rclient.subscribe = sinon.stub().resolves()
@ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
@rclient.unsubscribe = sinon.stub().rejects(new Error("some redis error"))
@ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef"
setTimeout done
return null
it "should have cleaned up", ->
@ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false
it "should not error out when subscribing again", (done) ->
p = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
p.then () ->
done()
.catch done
return null
describe "when unsubscribe errors and another client subscribes at the same time", ->
beforeEach (done) ->
@rclient.subscribe = sinon.stub().resolves()
@ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
rejectSubscribe = undefined
@rclient.unsubscribe = () ->
return new Promise (resolve, reject) ->
rejectSubscribe = reject
@ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef"
setTimeout () =>
# delay, actualUnsubscribe should not see the new subscribe request
@ChannelManager.subscribe(@rclient, "applied-ops", "1234567890abcdef")
.then () ->
setTimeout done
.catch done
setTimeout ->
# delay, rejectSubscribe is not defined immediately
rejectSubscribe(new Error("redis error"))
return null
it "should have recorded the error", ->
expect(@metrics.inc.calledWithExactly("unsubscribe.failed.applied-ops")).to.equal(true)
it "should have subscribed", ->
@rclient.subscribe.called.should.equal true
it "should have discarded the finished Promise", ->
@ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false
describe "when there is an existing subscription for this redis client", ->
beforeEach (done) ->
@rclient.subscribe = sinon.stub().resolves()
@rclient.unsubscribe = sinon.stub().resolves()
@ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
@ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef"
setTimeout done
it "should unsubscribe from the redis channel", ->
@rclient.unsubscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal true
describe "publish", ->
describe "when the channel is 'all'", ->
beforeEach ->
@rclient.publish = sinon.stub()
@ChannelManager.publish @rclient, "applied-ops", "all", "random-message"
it "should publish on the base channel", ->
@rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal true
describe "when the channel has an specific id", ->
describe "when the individual channel setting is false", ->
beforeEach ->
@rclient.publish = sinon.stub()
@settings.publishOnIndividualChannels = false
@ChannelManager.publish @rclient, "applied-ops", "1234567890abcdef", "random-message"
it "should publish on the per-id channel", ->
@rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal true
@rclient.publish.calledOnce.should.equal true
describe "when the individual channel setting is true", ->
beforeEach ->
@rclient.publish = sinon.stub()
@settings.publishOnIndividualChannels = true
@ChannelManager.publish @rclient, "applied-ops", "1234567890abcdef", "random-message"
it "should publish on the per-id channel", ->
@rclient.publish.calledWithExactly("applied-ops:1234567890abcdef", "random-message").should.equal true
@rclient.publish.calledOnce.should.equal true
describe "metrics", ->
beforeEach ->
@rclient.publish = sinon.stub()
@ChannelManager.publish @rclient, "applied-ops", "all", "random-message"
it "should track the payload size", ->
@metrics.summary.calledWithExactly(
"redis.publish.applied-ops",
"random-message".length
).should.equal true