decaffeinate: Convert AuthorizationManagerTests.coffee and 13 other files to JS

This commit is contained in:
decaffeinate 2020-06-23 18:29:59 +01:00 committed by Jakob Ackermann
parent e5d07bd3af
commit 2ca620e7a0
14 changed files with 2987 additions and 2231 deletions

View file

@ -1,166 +1,216 @@
chai = require "chai"
chai.should()
expect = chai.expect
sinon = require("sinon")
SandboxedModule = require('sandboxed-module')
path = require "path"
modulePath = '../../../app/js/AuthorizationManager'
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const chai = require("chai");
chai.should();
const {
expect
} = chai;
const sinon = require("sinon");
const SandboxedModule = require('sandboxed-module');
const path = require("path");
const modulePath = '../../../app/js/AuthorizationManager';
describe 'AuthorizationManager', ->
beforeEach ->
@client =
ol_context: {}
describe('AuthorizationManager', function() {
beforeEach(function() {
this.client =
{ol_context: {}};
@AuthorizationManager = SandboxedModule.require modulePath, requires: {}
return this.AuthorizationManager = SandboxedModule.require(modulePath, {requires: {}});});
describe "assertClientCanViewProject", ->
it "should allow the readOnly privilegeLevel", (done) ->
@client.ol_context.privilege_level = "readOnly"
@AuthorizationManager.assertClientCanViewProject @client, (error) ->
expect(error).to.be.null
done()
describe("assertClientCanViewProject", function() {
it("should allow the readOnly privilegeLevel", function(done) {
this.client.ol_context.privilege_level = "readOnly";
return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) {
expect(error).to.be.null;
return done();
});
});
it "should allow the readAndWrite privilegeLevel", (done) ->
@client.ol_context.privilege_level = "readAndWrite"
@AuthorizationManager.assertClientCanViewProject @client, (error) ->
expect(error).to.be.null
done()
it("should allow the readAndWrite privilegeLevel", function(done) {
this.client.ol_context.privilege_level = "readAndWrite";
return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) {
expect(error).to.be.null;
return done();
});
});
it "should allow the owner privilegeLevel", (done) ->
@client.ol_context.privilege_level = "owner"
@AuthorizationManager.assertClientCanViewProject @client, (error) ->
expect(error).to.be.null
done()
it("should allow the owner privilegeLevel", function(done) {
this.client.ol_context.privilege_level = "owner";
return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) {
expect(error).to.be.null;
return done();
});
});
it "should return an error with any other privilegeLevel", (done) ->
@client.ol_context.privilege_level = "unknown"
@AuthorizationManager.assertClientCanViewProject @client, (error) ->
error.message.should.equal "not authorized"
done()
return it("should return an error with any other privilegeLevel", function(done) {
this.client.ol_context.privilege_level = "unknown";
return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) {
error.message.should.equal("not authorized");
return done();
});
});
});
describe "assertClientCanEditProject", ->
it "should not allow the readOnly privilegeLevel", (done) ->
@client.ol_context.privilege_level = "readOnly"
@AuthorizationManager.assertClientCanEditProject @client, (error) ->
error.message.should.equal "not authorized"
done()
describe("assertClientCanEditProject", function() {
it("should not allow the readOnly privilegeLevel", function(done) {
this.client.ol_context.privilege_level = "readOnly";
return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) {
error.message.should.equal("not authorized");
return done();
});
});
it "should allow the readAndWrite privilegeLevel", (done) ->
@client.ol_context.privilege_level = "readAndWrite"
@AuthorizationManager.assertClientCanEditProject @client, (error) ->
expect(error).to.be.null
done()
it("should allow the readAndWrite privilegeLevel", function(done) {
this.client.ol_context.privilege_level = "readAndWrite";
return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) {
expect(error).to.be.null;
return done();
});
});
it "should allow the owner privilegeLevel", (done) ->
@client.ol_context.privilege_level = "owner"
@AuthorizationManager.assertClientCanEditProject @client, (error) ->
expect(error).to.be.null
done()
it("should allow the owner privilegeLevel", function(done) {
this.client.ol_context.privilege_level = "owner";
return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) {
expect(error).to.be.null;
return done();
});
});
it "should return an error with any other privilegeLevel", (done) ->
@client.ol_context.privilege_level = "unknown"
@AuthorizationManager.assertClientCanEditProject @client, (error) ->
error.message.should.equal "not authorized"
done()
return it("should return an error with any other privilegeLevel", function(done) {
this.client.ol_context.privilege_level = "unknown";
return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) {
error.message.should.equal("not authorized");
return done();
});
});
});
# check doc access for project
// check doc access for project
describe "assertClientCanViewProjectAndDoc", ->
beforeEach () ->
@doc_id = "12345"
@callback = sinon.stub()
@client.ol_context = {}
describe("assertClientCanViewProjectAndDoc", function() {
beforeEach(function() {
this.doc_id = "12345";
this.callback = sinon.stub();
return this.client.ol_context = {};});
describe "when not authorised at the project level", ->
beforeEach () ->
@client.ol_context.privilege_level = "unknown"
describe("when not authorised at the project level", function() {
beforeEach(function() {
return this.client.ol_context.privilege_level = "unknown";
});
it "should not allow access", () ->
@AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, (err) ->
err.message.should.equal "not authorized"
it("should not allow access", function() {
return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
});
describe "even when authorised at the doc level", ->
beforeEach (done) ->
@AuthorizationManager.addAccessToDoc @client, @doc_id, done
return describe("even when authorised at the doc level", function() {
beforeEach(function(done) {
return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done);
});
it "should not allow access", () ->
@AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, (err) ->
err.message.should.equal "not authorized"
return it("should not allow access", function() {
return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
});
});
});
describe "when authorised at the project level", ->
beforeEach () ->
@client.ol_context.privilege_level = "readOnly"
return describe("when authorised at the project level", function() {
beforeEach(function() {
return this.client.ol_context.privilege_level = "readOnly";
});
describe "and not authorised at the document level", ->
it "should not allow access", () ->
@AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, (err) ->
err.message.should.equal "not authorized"
describe("and not authorised at the document level", () => it("should not allow access", function() {
return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
}));
describe "and authorised at the document level", ->
beforeEach (done) ->
@AuthorizationManager.addAccessToDoc @client, @doc_id, done
describe("and authorised at the document level", function() {
beforeEach(function(done) {
return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done);
});
it "should allow access", () ->
@AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, @callback
@callback
return it("should allow access", function() {
this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, this.callback);
return this.callback
.calledWith(null)
.should.equal true
.should.equal(true);
});
});
describe "when document authorisation is added and then removed", ->
beforeEach (done) ->
@AuthorizationManager.addAccessToDoc @client, @doc_id, () =>
@AuthorizationManager.removeAccessToDoc @client, @doc_id, done
return describe("when document authorisation is added and then removed", function() {
beforeEach(function(done) {
return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, () => {
return this.AuthorizationManager.removeAccessToDoc(this.client, this.doc_id, done);
});
});
it "should deny access", () ->
@AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, (err) ->
err.message.should.equal "not authorized"
return it("should deny access", function() {
return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
});
});
});
});
describe "assertClientCanEditProjectAndDoc", ->
beforeEach () ->
@doc_id = "12345"
@callback = sinon.stub()
@client.ol_context = {}
return describe("assertClientCanEditProjectAndDoc", function() {
beforeEach(function() {
this.doc_id = "12345";
this.callback = sinon.stub();
return this.client.ol_context = {};});
describe "when not authorised at the project level", ->
beforeEach () ->
@client.ol_context.privilege_level = "readOnly"
describe("when not authorised at the project level", function() {
beforeEach(function() {
return this.client.ol_context.privilege_level = "readOnly";
});
it "should not allow access", () ->
@AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, (err) ->
err.message.should.equal "not authorized"
it("should not allow access", function() {
return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
});
describe "even when authorised at the doc level", ->
beforeEach (done) ->
@AuthorizationManager.addAccessToDoc @client, @doc_id, done
return describe("even when authorised at the doc level", function() {
beforeEach(function(done) {
return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done);
});
it "should not allow access", () ->
@AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, (err) ->
err.message.should.equal "not authorized"
return it("should not allow access", function() {
return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
});
});
});
describe "when authorised at the project level", ->
beforeEach () ->
@client.ol_context.privilege_level = "readAndWrite"
return describe("when authorised at the project level", function() {
beforeEach(function() {
return this.client.ol_context.privilege_level = "readAndWrite";
});
describe "and not authorised at the document level", ->
it "should not allow access", () ->
@AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, (err) ->
err.message.should.equal "not authorized"
describe("and not authorised at the document level", () => it("should not allow access", function() {
return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
}));
describe "and authorised at the document level", ->
beforeEach (done) ->
@AuthorizationManager.addAccessToDoc @client, @doc_id, done
describe("and authorised at the document level", function() {
beforeEach(function(done) {
return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done);
});
it "should allow access", () ->
@AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, @callback
@callback
return it("should allow access", function() {
this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, this.callback);
return this.callback
.calledWith(null)
.should.equal true
.should.equal(true);
});
});
describe "when document authorisation is added and then removed", ->
beforeEach (done) ->
@AuthorizationManager.addAccessToDoc @client, @doc_id, () =>
@AuthorizationManager.removeAccessToDoc @client, @doc_id, done
return describe("when document authorisation is added and then removed", function() {
beforeEach(function(done) {
return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, () => {
return this.AuthorizationManager.removeAccessToDoc(this.client, this.doc_id, done);
});
});
it "should deny access", () ->
@AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, (err) ->
err.message.should.equal "not authorized"
return it("should deny access", function() {
return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
});
});
});
});
});

View file

@ -1,220 +1,274 @@
chai = require('chai')
should = chai.should()
expect = chai.expect
sinon = require("sinon")
modulePath = "../../../app/js/ChannelManager.js"
SandboxedModule = require('sandboxed-module')
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const chai = require('chai');
const should = chai.should();
const {
expect
} = chai;
const sinon = require("sinon");
const modulePath = "../../../app/js/ChannelManager.js";
const 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('ChannelManager', function() {
beforeEach(function() {
this.rclient = {};
this.other_rclient = {};
return this.ChannelManager = SandboxedModule.require(modulePath, { requires: {
"settings-sharelatex": (this.settings = {}),
"metrics-sharelatex": (this.metrics = {inc: sinon.stub(), summary: sinon.stub()}),
"logger-sharelatex": (this.logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() })
}
});});
describe "subscribe", ->
describe("subscribe", function() {
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
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);
});
it "should subscribe to the redis channel", ->
@rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal true
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", ->
beforeEach (done) ->
@rclient.subscribe = sinon.stub().resolves()
@ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
@ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
setTimeout done
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);
});
it "should subscribe to the redis channel again", ->
@rclient.subscribe.callCount.should.equal 2
return it("should subscribe to the redis channel again", function() {
return this.rclient.subscribe.callCount.should.equal(2);
});
});
describe "when subscribe errors", ->
beforeEach (done) ->
@rclient.subscribe = sinon.stub()
describe("when subscribe errors", function() {
beforeEach(function(done) {
this.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
.onSecondCall().resolves();
const p = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef");
p.then(() => done(new Error('should not subscribe but fail'))).catch(err => {
err.message.should.equal("some redis error");
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", ->
expect(@metrics.inc.calledWithExactly("subscribe.failed.applied-ops")).to.equal(true)
it("should have recorded the error", function() {
return expect(this.metrics.inc.calledWithExactly("subscribe.failed.applied-ops")).to.equal(true);
});
it "should subscribe again", ->
@rclient.subscribe.callCount.should.equal 2
it("should subscribe again", function() {
return this.rclient.subscribe.callCount.should.equal(2);
});
it "should cleanup", ->
@ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false
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", ->
beforeEach (done) ->
@rclient.subscribe = sinon.stub()
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()
@first = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef"
# ignore error
@first.catch((()->))
expect(@ChannelManager.getClientMapEntry(@rclient).get("applied-ops:1234567890abcdef")).to.equal @first
.onSecondCall().resolves();
this.first = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef");
// ignore error
this.first.catch((function(){}));
expect(this.ChannelManager.getClientMapEntry(this.rclient).get("applied-ops:1234567890abcdef")).to.equal(this.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
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
setTimeout done
// let the first subscribe error -> unsubscribe -> subscribe
return setTimeout(done);
});
it "should cleanup the second subscribePromise", ->
expect(@ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef")).to.equal false
return it("should cleanup the second subscribePromise", function() {
return expect(this.ChannelManager.getClientMapEntry(this.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
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);
});
it "should subscribe to the redis channel on this redis client", ->
@rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal true
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", ->
describe("unsubscribe", function() {
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
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);
});
it "should unsubscribe from the redis channel", ->
@rclient.unsubscribe.called.should.equal true
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", ->
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
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);
});
it "should still unsubscribe from the redis channel on this client", ->
@rclient.unsubscribe.called.should.equal true
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", ->
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
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", ->
@ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false
it("should have cleaned up", function() {
return this.ChannelManager.getClientMapEntry(this.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
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", ->
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"
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 = undefined;
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
@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
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", ->
expect(@metrics.inc.calledWithExactly("unsubscribe.failed.applied-ops")).to.equal(true)
it("should have recorded the error", function() {
return expect(this.metrics.inc.calledWithExactly("unsubscribe.failed.applied-ops")).to.equal(true);
});
it "should have subscribed", ->
@rclient.subscribe.called.should.equal true
it("should have subscribed", function() {
return this.rclient.subscribe.called.should.equal(true);
});
it "should have discarded the finished Promise", ->
@ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false
return it("should have discarded the finished Promise", function() {
return this.ChannelManager.getClientMapEntry(this.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
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);
});
it "should unsubscribe from the redis channel", ->
@rclient.unsubscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal true
return it("should unsubscribe from the redis channel", function() {
return this.rclient.unsubscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true);
});
});
});
describe "publish", ->
return describe("publish", function() {
describe "when the channel is 'all'", ->
beforeEach ->
@rclient.publish = sinon.stub()
@ChannelManager.publish @rclient, "applied-ops", "all", "random-message"
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");
});
it "should publish on the base channel", ->
@rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal true
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", ->
describe("when the channel has an specific id", function() {
describe "when the individual channel setting is false", ->
beforeEach ->
@rclient.publish = sinon.stub()
@settings.publishOnIndividualChannels = false
@ChannelManager.publish @rclient, "applied-ops", "1234567890abcdef", "random-message"
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");
});
it "should publish on the per-id channel", ->
@rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal true
@rclient.publish.calledOnce.should.equal true
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);
});
});
describe "when the individual channel setting is true", ->
beforeEach ->
@rclient.publish = sinon.stub()
@settings.publishOnIndividualChannels = true
@ChannelManager.publish @rclient, "applied-ops", "1234567890abcdef", "random-message"
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");
});
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
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);
});
});
});
describe "metrics", ->
beforeEach ->
@rclient.publish = sinon.stub()
@ChannelManager.publish @rclient, "applied-ops", "all", "random-message"
return describe("metrics", function() {
beforeEach(function() {
this.rclient.publish = sinon.stub();
return this.ChannelManager.publish(this.rclient, "applied-ops", "all", "random-message");
});
it "should track the payload size", ->
@metrics.summary.calledWithExactly(
return it("should track the payload size", function() {
return this.metrics.summary.calledWithExactly(
"redis.publish.applied-ops",
"random-message".length
).should.equal true
).should.equal(true);
});
});
});
});

View file

@ -1,164 +1,221 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('assert')
path = require('path')
sinon = require('sinon')
modulePath = path.join __dirname, "../../../app/js/ConnectedUsersManager"
expect = require("chai").expect
tk = require("timekeeper")
const should = require('chai').should();
const SandboxedModule = require('sandboxed-module');
const assert = require('assert');
const path = require('path');
const sinon = require('sinon');
const modulePath = path.join(__dirname, "../../../app/js/ConnectedUsersManager");
const {
expect
} = require("chai");
const tk = require("timekeeper");
describe "ConnectedUsersManager", ->
describe("ConnectedUsersManager", function() {
beforeEach ->
beforeEach(function() {
@settings =
redis:
realtime:
key_schema:
clientsInProject: ({project_id}) -> "clients_in_project:#{project_id}"
connectedUser: ({project_id, client_id})-> "connected_user:#{project_id}:#{client_id}"
@rClient =
auth:->
setex:sinon.stub()
sadd:sinon.stub()
get: sinon.stub()
srem:sinon.stub()
del:sinon.stub()
smembers:sinon.stub()
expire:sinon.stub()
hset:sinon.stub()
hgetall:sinon.stub()
exec:sinon.stub()
multi: => return @rClient
tk.freeze(new Date())
this.settings = {
redis: {
realtime: {
key_schema: {
clientsInProject({project_id}) { return `clients_in_project:${project_id}`; },
connectedUser({project_id, client_id}){ return `connected_user:${project_id}:${client_id}`; }
}
}
}
};
this.rClient = {
auth() {},
setex:sinon.stub(),
sadd:sinon.stub(),
get: sinon.stub(),
srem:sinon.stub(),
del:sinon.stub(),
smembers:sinon.stub(),
expire:sinon.stub(),
hset:sinon.stub(),
hgetall:sinon.stub(),
exec:sinon.stub(),
multi: () => { return this.rClient; }
};
tk.freeze(new Date());
@ConnectedUsersManager = SandboxedModule.require modulePath, requires:
"settings-sharelatex":@settings
"logger-sharelatex": log:->
"redis-sharelatex": createClient:=>
return @rClient
@client_id = "32132132"
@project_id = "dskjh2u21321"
@user = {
_id: "user-id-123"
first_name: "Joe"
last_name: "Bloggs"
email: "joe@example.com"
this.ConnectedUsersManager = SandboxedModule.require(modulePath, { requires: {
"settings-sharelatex":this.settings,
"logger-sharelatex": { log() {}
},
"redis-sharelatex": { createClient:() => {
return this.rClient;
}
}
@cursorData = { row: 12, column: 9, doc_id: '53c3b8c85fee64000023dc6e' }
}
}
);
this.client_id = "32132132";
this.project_id = "dskjh2u21321";
this.user = {
_id: "user-id-123",
first_name: "Joe",
last_name: "Bloggs",
email: "joe@example.com"
};
return this.cursorData = { row: 12, column: 9, doc_id: '53c3b8c85fee64000023dc6e' };});
afterEach ->
tk.reset()
afterEach(() => tk.reset());
describe "updateUserPosition", ->
beforeEach ->
@rClient.exec.callsArgWith(0)
describe("updateUserPosition", function() {
beforeEach(function() {
return this.rClient.exec.callsArgWith(0);
});
it "should set a key with the date and give it a ttl", (done)->
@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
@rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "last_updated_at", Date.now()).should.equal true
done()
it("should set a key with the date and give it a ttl", function(done){
return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> {
this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "last_updated_at", Date.now()).should.equal(true);
return done();
});
});
it "should set a key with the user_id", (done)->
@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
@rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "user_id", @user._id).should.equal true
done()
it("should set a key with the user_id", function(done){
return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> {
this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "user_id", this.user._id).should.equal(true);
return done();
});
});
it "should set a key with the first_name", (done)->
@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
@rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "first_name", @user.first_name).should.equal true
done()
it("should set a key with the first_name", function(done){
return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> {
this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "first_name", this.user.first_name).should.equal(true);
return done();
});
});
it "should set a key with the last_name", (done)->
@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
@rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "last_name", @user.last_name).should.equal true
done()
it("should set a key with the last_name", function(done){
return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> {
this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "last_name", this.user.last_name).should.equal(true);
return done();
});
});
it "should set a key with the email", (done)->
@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
@rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "email", @user.email).should.equal true
done()
it("should set a key with the email", function(done){
return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> {
this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "email", this.user.email).should.equal(true);
return done();
});
});
it "should push the client_id on to the project list", (done)->
@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
@rClient.sadd.calledWith("clients_in_project:#{@project_id}", @client_id).should.equal true
done()
it("should push the client_id on to the project list", function(done){
return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> {
this.rClient.sadd.calledWith(`clients_in_project:${this.project_id}`, this.client_id).should.equal(true);
return done();
});
});
it "should add a ttl to the project set so it stays clean", (done)->
@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
@rClient.expire.calledWith("clients_in_project:#{@project_id}", 24 * 4 * 60 * 60).should.equal true
done()
it("should add a ttl to the project set so it stays clean", function(done){
return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> {
this.rClient.expire.calledWith(`clients_in_project:${this.project_id}`, 24 * 4 * 60 * 60).should.equal(true);
return done();
});
});
it "should add a ttl to the connected user so it stays clean", (done) ->
@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=>
@rClient.expire.calledWith("connected_user:#{@project_id}:#{@client_id}", 60 * 15).should.equal true
done()
it("should add a ttl to the connected user so it stays clean", function(done) {
return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> {
this.rClient.expire.calledWith(`connected_user:${this.project_id}:${this.client_id}`, 60 * 15).should.equal(true);
return done();
});
});
it "should set the cursor position when provided", (done)->
@ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, @cursorData, (err)=>
@rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "cursorData", JSON.stringify(@cursorData)).should.equal true
done()
return it("should set the cursor position when provided", function(done){
return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, this.cursorData, err=> {
this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "cursorData", JSON.stringify(this.cursorData)).should.equal(true);
return done();
});
});
});
describe "markUserAsDisconnected", ->
beforeEach ->
@rClient.exec.callsArgWith(0)
describe("markUserAsDisconnected", function() {
beforeEach(function() {
return this.rClient.exec.callsArgWith(0);
});
it "should remove the user from the set", (done)->
@ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=>
@rClient.srem.calledWith("clients_in_project:#{@project_id}", @client_id).should.equal true
done()
it("should remove the user from the set", function(done){
return this.ConnectedUsersManager.markUserAsDisconnected(this.project_id, this.client_id, err=> {
this.rClient.srem.calledWith(`clients_in_project:${this.project_id}`, this.client_id).should.equal(true);
return done();
});
});
it "should delete the connected_user string", (done)->
@ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=>
@rClient.del.calledWith("connected_user:#{@project_id}:#{@client_id}").should.equal true
done()
it("should delete the connected_user string", function(done){
return this.ConnectedUsersManager.markUserAsDisconnected(this.project_id, this.client_id, err=> {
this.rClient.del.calledWith(`connected_user:${this.project_id}:${this.client_id}`).should.equal(true);
return done();
});
});
it "should add a ttl to the connected user set so it stays clean", (done)->
@ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=>
@rClient.expire.calledWith("clients_in_project:#{@project_id}", 24 * 4 * 60 * 60).should.equal true
done()
return it("should add a ttl to the connected user set so it stays clean", function(done){
return this.ConnectedUsersManager.markUserAsDisconnected(this.project_id, this.client_id, err=> {
this.rClient.expire.calledWith(`clients_in_project:${this.project_id}`, 24 * 4 * 60 * 60).should.equal(true);
return done();
});
});
});
describe "_getConnectedUser", ->
describe("_getConnectedUser", function() {
it "should return a connected user if there is a user object", (done)->
cursorData = JSON.stringify(cursorData:{row:1})
@rClient.hgetall.callsArgWith(1, null, {connected_at:new Date(), user_id: @user._id, last_updated_at: "#{Date.now()}", cursorData})
@ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=>
result.connected.should.equal true
result.client_id.should.equal @client_id
done()
it("should return a connected user if there is a user object", function(done){
const cursorData = JSON.stringify({cursorData:{row:1}});
this.rClient.hgetall.callsArgWith(1, null, {connected_at:new Date(), user_id: this.user._id, last_updated_at: `${Date.now()}`, cursorData});
return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> {
result.connected.should.equal(true);
result.client_id.should.equal(this.client_id);
return done();
});
});
it "should return a not connected user if there is no object", (done)->
@rClient.hgetall.callsArgWith(1, null, null)
@ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=>
result.connected.should.equal false
result.client_id.should.equal @client_id
done()
it("should return a not connected user if there is no object", function(done){
this.rClient.hgetall.callsArgWith(1, null, null);
return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> {
result.connected.should.equal(false);
result.client_id.should.equal(this.client_id);
return done();
});
});
it "should return a not connected user if there is an empty object", (done)->
@rClient.hgetall.callsArgWith(1, null, {})
@ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=>
result.connected.should.equal false
result.client_id.should.equal @client_id
done()
return it("should return a not connected user if there is an empty object", function(done){
this.rClient.hgetall.callsArgWith(1, null, {});
return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> {
result.connected.should.equal(false);
result.client_id.should.equal(this.client_id);
return done();
});
});
});
describe "getConnectedUsers", ->
return describe("getConnectedUsers", function() {
beforeEach ->
@users = ["1234", "5678", "9123", "8234"]
@rClient.smembers.callsArgWith(1, null, @users)
@ConnectedUsersManager._getConnectedUser = sinon.stub()
@ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[0]).callsArgWith(2, null, {connected:true, client_age: 2, client_id:@users[0]})
@ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[1]).callsArgWith(2, null, {connected:false, client_age: 1, client_id:@users[1]})
@ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[2]).callsArgWith(2, null, {connected:true, client_age: 3, client_id:@users[2]})
@ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[3]).callsArgWith(2, null, {connected:true, client_age: 11, client_id:@users[3]}) # connected but old
beforeEach(function() {
this.users = ["1234", "5678", "9123", "8234"];
this.rClient.smembers.callsArgWith(1, null, this.users);
this.ConnectedUsersManager._getConnectedUser = sinon.stub();
this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[0]).callsArgWith(2, null, {connected:true, client_age: 2, client_id:this.users[0]});
this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[1]).callsArgWith(2, null, {connected:false, client_age: 1, client_id:this.users[1]});
this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[2]).callsArgWith(2, null, {connected:true, client_age: 3, client_id:this.users[2]});
return this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[3]).callsArgWith(2, null, {connected:true, client_age: 11, client_id:this.users[3]});
}); // connected but old
it "should only return the users in the list which are still in redis and recently updated", (done)->
@ConnectedUsersManager.getConnectedUsers @project_id, (err, users)=>
users.length.should.equal 2
users[0].should.deep.equal {client_id:@users[0], client_age: 2, connected:true}
users[1].should.deep.equal {client_id:@users[2], client_age: 3, connected:true}
done()
return it("should only return the users in the list which are still in redis and recently updated", function(done){
return this.ConnectedUsersManager.getConnectedUsers(this.project_id, (err, users)=> {
users.length.should.equal(2);
users[0].should.deep.equal({client_id:this.users[0], client_age: 2, connected:true});
users[1].should.deep.equal({client_id:this.users[2], client_age: 3, connected:true});
return done();
});
});
});
});

View file

@ -1,153 +1,203 @@
SandboxedModule = require('sandboxed-module')
sinon = require('sinon')
require('chai').should()
modulePath = require('path').join __dirname, '../../../app/js/DocumentUpdaterController'
MockClient = require "./helpers/MockClient"
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module');
const sinon = require('sinon');
require('chai').should();
const modulePath = require('path').join(__dirname, '../../../app/js/DocumentUpdaterController');
const MockClient = require("./helpers/MockClient");
describe "DocumentUpdaterController", ->
beforeEach ->
@project_id = "project-id-123"
@doc_id = "doc-id-123"
@callback = sinon.stub()
@io = { "mock": "socket.io" }
@rclient = []
@RoomEvents = { on: sinon.stub() }
@EditorUpdatesController = SandboxedModule.require modulePath, requires:
"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }
"settings-sharelatex": @settings =
redis:
documentupdater:
key_schema:
pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}"
describe("DocumentUpdaterController", function() {
beforeEach(function() {
this.project_id = "project-id-123";
this.doc_id = "doc-id-123";
this.callback = sinon.stub();
this.io = { "mock": "socket.io" };
this.rclient = [];
this.RoomEvents = { on: sinon.stub() };
return this.EditorUpdatesController = SandboxedModule.require(modulePath, { requires: {
"logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }),
"settings-sharelatex": (this.settings = {
redis: {
documentupdater: {
key_schema: {
pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; }
}
},
pubsub: null
"redis-sharelatex" : @redis =
createClient: (name) =>
@rclient.push(rclientStub = {name:name})
return rclientStub
"./SafeJsonParse": @SafeJsonParse =
parse: (data, cb) => cb null, JSON.parse(data)
"./EventLogger": @EventLogger = {checkEventOrder: sinon.stub()}
"./HealthCheckManager": {check: sinon.stub()}
"metrics-sharelatex": @metrics = {inc: sinon.stub()}
"./RoomManager" : @RoomManager = { eventSource: sinon.stub().returns @RoomEvents}
"./ChannelManager": @ChannelManager = {}
}
}),
"redis-sharelatex" : (this.redis = {
createClient: name => {
let rclientStub;
this.rclient.push(rclientStub = {name});
return rclientStub;
}
}),
"./SafeJsonParse": (this.SafeJsonParse =
{parse: (data, cb) => cb(null, JSON.parse(data))}),
"./EventLogger": (this.EventLogger = {checkEventOrder: sinon.stub()}),
"./HealthCheckManager": {check: sinon.stub()},
"metrics-sharelatex": (this.metrics = {inc: sinon.stub()}),
"./RoomManager" : (this.RoomManager = { eventSource: sinon.stub().returns(this.RoomEvents)}),
"./ChannelManager": (this.ChannelManager = {})
}
});});
describe "listenForUpdatesFromDocumentUpdater", ->
beforeEach ->
@rclient.length = 0 # clear any existing clients
@EditorUpdatesController.rclientList = [@redis.createClient("first"), @redis.createClient("second")]
@rclient[0].subscribe = sinon.stub()
@rclient[0].on = sinon.stub()
@rclient[1].subscribe = sinon.stub()
@rclient[1].on = sinon.stub()
@EditorUpdatesController.listenForUpdatesFromDocumentUpdater()
describe("listenForUpdatesFromDocumentUpdater", function() {
beforeEach(function() {
this.rclient.length = 0; // clear any existing clients
this.EditorUpdatesController.rclientList = [this.redis.createClient("first"), this.redis.createClient("second")];
this.rclient[0].subscribe = sinon.stub();
this.rclient[0].on = sinon.stub();
this.rclient[1].subscribe = sinon.stub();
this.rclient[1].on = sinon.stub();
return this.EditorUpdatesController.listenForUpdatesFromDocumentUpdater();
});
it "should subscribe to the doc-updater stream", ->
@rclient[0].subscribe.calledWith("applied-ops").should.equal true
it("should subscribe to the doc-updater stream", function() {
return this.rclient[0].subscribe.calledWith("applied-ops").should.equal(true);
});
it "should register a callback to handle updates", ->
@rclient[0].on.calledWith("message").should.equal true
it("should register a callback to handle updates", function() {
return this.rclient[0].on.calledWith("message").should.equal(true);
});
it "should subscribe to any additional doc-updater stream", ->
@rclient[1].subscribe.calledWith("applied-ops").should.equal true
@rclient[1].on.calledWith("message").should.equal true
return it("should subscribe to any additional doc-updater stream", function() {
this.rclient[1].subscribe.calledWith("applied-ops").should.equal(true);
return this.rclient[1].on.calledWith("message").should.equal(true);
});
});
describe "_processMessageFromDocumentUpdater", ->
describe "with bad JSON", ->
beforeEach ->
@SafeJsonParse.parse = sinon.stub().callsArgWith 1, new Error("oops")
@EditorUpdatesController._processMessageFromDocumentUpdater @io, "applied-ops", "blah"
describe("_processMessageFromDocumentUpdater", function() {
describe("with bad JSON", function() {
beforeEach(function() {
this.SafeJsonParse.parse = sinon.stub().callsArgWith(1, new Error("oops"));
return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", "blah");
});
it "should log an error", ->
@logger.error.called.should.equal true
return it("should log an error", function() {
return this.logger.error.called.should.equal(true);
});
});
describe "with update", ->
beforeEach ->
@message =
doc_id: @doc_id
describe("with update", function() {
beforeEach(function() {
this.message = {
doc_id: this.doc_id,
op: {t: "foo", p: 12}
@EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub()
@EditorUpdatesController._processMessageFromDocumentUpdater @io, "applied-ops", JSON.stringify(@message)
};
this.EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub();
return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", JSON.stringify(this.message));
});
it "should apply the update", ->
@EditorUpdatesController._applyUpdateFromDocumentUpdater
.calledWith(@io, @doc_id, @message.op)
.should.equal true
return it("should apply the update", function() {
return this.EditorUpdatesController._applyUpdateFromDocumentUpdater
.calledWith(this.io, this.doc_id, this.message.op)
.should.equal(true);
});
});
describe "with error", ->
beforeEach ->
@message =
doc_id: @doc_id
return describe("with error", function() {
beforeEach(function() {
this.message = {
doc_id: this.doc_id,
error: "Something went wrong"
@EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub()
@EditorUpdatesController._processMessageFromDocumentUpdater @io, "applied-ops", JSON.stringify(@message)
};
this.EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub();
return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", JSON.stringify(this.message));
});
it "should process the error", ->
@EditorUpdatesController._processErrorFromDocumentUpdater
.calledWith(@io, @doc_id, @message.error)
.should.equal true
return it("should process the error", function() {
return this.EditorUpdatesController._processErrorFromDocumentUpdater
.calledWith(this.io, this.doc_id, this.message.error)
.should.equal(true);
});
});
});
describe "_applyUpdateFromDocumentUpdater", ->
beforeEach ->
@sourceClient = new MockClient()
@otherClients = [new MockClient(), new MockClient()]
@update =
op: [ t: "foo", p: 12 ]
meta: source: @sourceClient.publicId
v: @version = 42
doc: @doc_id
@io.sockets =
clients: sinon.stub().returns([@sourceClient, @otherClients..., @sourceClient]) # include a duplicate client
describe("_applyUpdateFromDocumentUpdater", function() {
beforeEach(function() {
this.sourceClient = new MockClient();
this.otherClients = [new MockClient(), new MockClient()];
this.update = {
op: [ {t: "foo", p: 12} ],
meta: { source: this.sourceClient.publicId
},
v: (this.version = 42),
doc: this.doc_id
};
return this.io.sockets =
{clients: sinon.stub().returns([this.sourceClient, ...Array.from(this.otherClients), this.sourceClient])};
}); // include a duplicate client
describe "normally", ->
beforeEach ->
@EditorUpdatesController._applyUpdateFromDocumentUpdater @io, @doc_id, @update
describe("normally", function() {
beforeEach(function() {
return this.EditorUpdatesController._applyUpdateFromDocumentUpdater(this.io, this.doc_id, this.update);
});
it "should send a version bump to the source client", ->
@sourceClient.emit
.calledWith("otUpdateApplied", v: @version, doc: @doc_id)
.should.equal true
@sourceClient.emit.calledOnce.should.equal true
it("should send a version bump to the source client", function() {
this.sourceClient.emit
.calledWith("otUpdateApplied", {v: this.version, doc: this.doc_id})
.should.equal(true);
return this.sourceClient.emit.calledOnce.should.equal(true);
});
it "should get the clients connected to the document", ->
@io.sockets.clients
.calledWith(@doc_id)
.should.equal true
it("should get the clients connected to the document", function() {
return this.io.sockets.clients
.calledWith(this.doc_id)
.should.equal(true);
});
it "should send the full update to the other clients", ->
for client in @otherClients
return it("should send the full update to the other clients", function() {
return Array.from(this.otherClients).map((client) =>
client.emit
.calledWith("otUpdateApplied", @update)
.should.equal true
.calledWith("otUpdateApplied", this.update)
.should.equal(true));
});
});
describe "with a duplicate op", ->
beforeEach ->
@update.dup = true
@EditorUpdatesController._applyUpdateFromDocumentUpdater @io, @doc_id, @update
return describe("with a duplicate op", function() {
beforeEach(function() {
this.update.dup = true;
return this.EditorUpdatesController._applyUpdateFromDocumentUpdater(this.io, this.doc_id, this.update);
});
it "should send a version bump to the source client as usual", ->
@sourceClient.emit
.calledWith("otUpdateApplied", v: @version, doc: @doc_id)
.should.equal true
it("should send a version bump to the source client as usual", function() {
return this.sourceClient.emit
.calledWith("otUpdateApplied", {v: this.version, doc: this.doc_id})
.should.equal(true);
});
it "should not send anything to the other clients (they've already had the op)", ->
for client in @otherClients
return it("should not send anything to the other clients (they've already had the op)", function() {
return Array.from(this.otherClients).map((client) =>
client.emit
.calledWith("otUpdateApplied")
.should.equal false
.should.equal(false));
});
});
});
describe "_processErrorFromDocumentUpdater", ->
beforeEach ->
@clients = [new MockClient(), new MockClient()]
@io.sockets =
clients: sinon.stub().returns(@clients)
@EditorUpdatesController._processErrorFromDocumentUpdater @io, @doc_id, "Something went wrong"
return describe("_processErrorFromDocumentUpdater", function() {
beforeEach(function() {
this.clients = [new MockClient(), new MockClient()];
this.io.sockets =
{clients: sinon.stub().returns(this.clients)};
return this.EditorUpdatesController._processErrorFromDocumentUpdater(this.io, this.doc_id, "Something went wrong");
});
it "should log a warning", ->
@logger.warn.called.should.equal true
it("should log a warning", function() {
return this.logger.warn.called.should.equal(true);
});
it "should disconnect all clients in that document", ->
@io.sockets.clients.calledWith(@doc_id).should.equal true
for client in @clients
client.disconnect.called.should.equal true
return it("should disconnect all clients in that document", function() {
this.io.sockets.clients.calledWith(this.doc_id).should.equal(true);
return Array.from(this.clients).map((client) =>
client.disconnect.called.should.equal(true));
});
});
});

View file

@ -1,193 +1,260 @@
require('chai').should()
sinon = require("sinon")
SandboxedModule = require('sandboxed-module')
path = require "path"
modulePath = '../../../app/js/DocumentUpdaterManager'
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
require('chai').should();
const sinon = require("sinon");
const SandboxedModule = require('sandboxed-module');
const path = require("path");
const modulePath = '../../../app/js/DocumentUpdaterManager';
describe 'DocumentUpdaterManager', ->
beforeEach ->
@project_id = "project-id-923"
@doc_id = "doc-id-394"
@lines = ["one", "two", "three"]
@version = 42
@settings =
apis: documentupdater: url: "http://doc-updater.example.com"
redis: documentupdater:
key_schema:
pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}"
maxUpdateSize: 7 * 1024 * 1024
@rclient = {auth:->}
@DocumentUpdaterManager = SandboxedModule.require modulePath,
requires:
'settings-sharelatex':@settings
'logger-sharelatex': @logger = {log: sinon.stub(), error: sinon.stub(), warn: sinon.stub()}
'request': @request = {}
'redis-sharelatex' : createClient: () => @rclient
'metrics-sharelatex': @Metrics =
summary: sinon.stub()
Timer: class Timer
done: () ->
globals:
JSON: @JSON = Object.create(JSON) # avoid modifying JSON object directly
describe "getDocument", ->
beforeEach ->
@callback = sinon.stub()
describe "successfully", ->
beforeEach ->
@body = JSON.stringify
lines: @lines
version: @version
ops: @ops = ["mock-op-1", "mock-op-2"]
ranges: @ranges = {"mock": "ranges"}
@fromVersion = 2
@request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body)
@DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback
it 'should get the document from the document updater', ->
url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}?fromVersion=#{@fromVersion}"
@request.get.calledWith(url).should.equal true
it "should call the callback with the lines, version, ranges and ops", ->
@callback.calledWith(null, @lines, @version, @ranges, @ops).should.equal true
describe "when the document updater API returns an error", ->
beforeEach ->
@request.get = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null)
@DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback
it "should return an error to the callback", ->
@callback.calledWith(@error).should.equal true
[404, 422].forEach (statusCode) ->
describe "when the document updater returns a #{statusCode} status code", ->
beforeEach ->
@request.get = sinon.stub().callsArgWith(1, null, { statusCode }, "")
@DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback
it "should return the callback with an error", ->
@callback.called.should.equal(true)
err = @callback.getCall(0).args[0]
err.should.have.property('statusCode', statusCode)
err.should.have.property('message', "doc updater could not load requested ops")
@logger.error.called.should.equal(false)
@logger.warn.called.should.equal(true)
describe "when the document updater returns a failure error code", ->
beforeEach ->
@request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "")
@DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback
it "should return the callback with an error", ->
@callback.called.should.equal(true)
err = @callback.getCall(0).args[0]
err.should.have.property('statusCode', 500)
err.should.have.property('message', "doc updater returned a non-success status code: 500")
@logger.error.called.should.equal(true)
describe 'flushProjectToMongoAndDelete', ->
beforeEach ->
@callback = sinon.stub()
describe "successfully", ->
beforeEach ->
@request.del = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "")
@DocumentUpdaterManager.flushProjectToMongoAndDelete @project_id, @callback
it 'should delete the project from the document updater', ->
url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}?background=true"
@request.del.calledWith(url).should.equal true
it "should call the callback with no error", ->
@callback.calledWith(null).should.equal true
describe "when the document updater API returns an error", ->
beforeEach ->
@request.del = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null)
@DocumentUpdaterManager.flushProjectToMongoAndDelete @project_id, @callback
it "should return an error to the callback", ->
@callback.calledWith(@error).should.equal true
describe "when the document updater returns a failure error code", ->
beforeEach ->
@request.del = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "")
@DocumentUpdaterManager.flushProjectToMongoAndDelete @project_id, @callback
it "should return the callback with an error", ->
@callback.called.should.equal(true)
err = @callback.getCall(0).args[0]
err.should.have.property('statusCode', 500)
err.should.have.property('message', "document updater returned a failure status code: 500")
describe 'queueChange', ->
beforeEach ->
@change = {
"doc":"1234567890",
"op":["d":"test", "p":345]
"v": 789
}
@rclient.rpush = sinon.stub().yields()
@callback = sinon.stub()
describe "successfully", ->
beforeEach ->
@DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback)
it "should push the change", ->
@rclient.rpush
.calledWith("PendingUpdates:#{@doc_id}", JSON.stringify(@change))
.should.equal true
it "should notify the doc updater of the change via the pending-updates-list queue", ->
@rclient.rpush
.calledWith("pending-updates-list", "#{@project_id}:#{@doc_id}")
.should.equal true
describe "with error talking to redis during rpush", ->
beforeEach ->
@rclient.rpush = sinon.stub().yields(new Error("something went wrong"))
@DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback)
it "should return an error", ->
@callback.calledWithExactly(sinon.match(Error)).should.equal true
describe "with null byte corruption", ->
beforeEach ->
@JSON.stringify = () -> return '["bad bytes! \u0000 <- here"]'
@DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback)
it "should return an error", ->
@callback.calledWithExactly(sinon.match(Error)).should.equal true
it "should not push the change onto the pending-updates-list queue", ->
@rclient.rpush.called.should.equal false
describe "when the update is too large", ->
beforeEach ->
@change = {op: {p: 12,t: "update is too large".repeat(1024 * 400)}}
@DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback)
it "should return an error", ->
@callback.calledWithExactly(sinon.match(Error)).should.equal true
it "should add the size to the error", ->
@callback.args[0][0].updateSize.should.equal 7782422
it "should not push the change onto the pending-updates-list queue", ->
@rclient.rpush.called.should.equal false
describe "with invalid keys", ->
beforeEach ->
@change = {
"op":["d":"test", "p":345]
"version": 789 # not a valid key
describe('DocumentUpdaterManager', function() {
beforeEach(function() {
let Timer;
this.project_id = "project-id-923";
this.doc_id = "doc-id-394";
this.lines = ["one", "two", "three"];
this.version = 42;
this.settings = {
apis: { documentupdater: {url: "http://doc-updater.example.com"}
},
redis: { documentupdater: {
key_schema: {
pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; }
}
@DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback)
}
},
maxUpdateSize: 7 * 1024 * 1024
};
this.rclient = {auth() {}};
it "should remove the invalid keys from the change", ->
@rclient.rpush
.calledWith("PendingUpdates:#{@doc_id}", JSON.stringify({op:@change.op}))
.should.equal true
return this.DocumentUpdaterManager = SandboxedModule.require(modulePath, {
requires: {
'settings-sharelatex':this.settings,
'logger-sharelatex': (this.logger = {log: sinon.stub(), error: sinon.stub(), warn: sinon.stub()}),
'request': (this.request = {}),
'redis-sharelatex' : { createClient: () => this.rclient
},
'metrics-sharelatex': (this.Metrics = {
summary: sinon.stub(),
Timer: (Timer = class Timer {
done() {}
})
})
},
globals: {
JSON: (this.JSON = Object.create(JSON))
}
}
);
}); // avoid modifying JSON object directly
describe("getDocument", function() {
beforeEach(function() {
return this.callback = sinon.stub();
});
describe("successfully", function() {
beforeEach(function() {
this.body = JSON.stringify({
lines: this.lines,
version: this.version,
ops: (this.ops = ["mock-op-1", "mock-op-2"]),
ranges: (this.ranges = {"mock": "ranges"})});
this.fromVersion = 2;
this.request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.body);
return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback);
});
it('should get the document from the document updater', function() {
const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`;
return this.request.get.calledWith(url).should.equal(true);
});
return it("should call the callback with the lines, version, ranges and ops", function() {
return this.callback.calledWith(null, this.lines, this.version, this.ranges, this.ops).should.equal(true);
});
});
describe("when the document updater API returns an error", function() {
beforeEach(function() {
this.request.get = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null);
return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback);
});
return it("should return an error to the callback", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
[404, 422].forEach(statusCode => describe(`when the document updater returns a ${statusCode} status code`, function() {
beforeEach(function() {
this.request.get = sinon.stub().callsArgWith(1, null, { statusCode }, "");
return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback);
});
return it("should return the callback with an error", function() {
this.callback.called.should.equal(true);
const err = this.callback.getCall(0).args[0];
err.should.have.property('statusCode', statusCode);
err.should.have.property('message', "doc updater could not load requested ops");
this.logger.error.called.should.equal(false);
return this.logger.warn.called.should.equal(true);
});
}));
return describe("when the document updater returns a failure error code", function() {
beforeEach(function() {
this.request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "");
return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback);
});
return it("should return the callback with an error", function() {
this.callback.called.should.equal(true);
const err = this.callback.getCall(0).args[0];
err.should.have.property('statusCode', 500);
err.should.have.property('message', "doc updater returned a non-success status code: 500");
return this.logger.error.called.should.equal(true);
});
});
});
describe('flushProjectToMongoAndDelete', function() {
beforeEach(function() {
return this.callback = sinon.stub();
});
describe("successfully", function() {
beforeEach(function() {
this.request.del = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "");
return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(this.project_id, this.callback);
});
it('should delete the project from the document updater', function() {
const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}?background=true`;
return this.request.del.calledWith(url).should.equal(true);
});
return it("should call the callback with no error", function() {
return this.callback.calledWith(null).should.equal(true);
});
});
describe("when the document updater API returns an error", function() {
beforeEach(function() {
this.request.del = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null);
return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(this.project_id, this.callback);
});
return it("should return an error to the callback", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
return describe("when the document updater returns a failure error code", function() {
beforeEach(function() {
this.request.del = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "");
return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(this.project_id, this.callback);
});
return it("should return the callback with an error", function() {
this.callback.called.should.equal(true);
const err = this.callback.getCall(0).args[0];
err.should.have.property('statusCode', 500);
return err.should.have.property('message', "document updater returned a failure status code: 500");
});
});
});
return describe('queueChange', function() {
beforeEach(function() {
this.change = {
"doc":"1234567890",
"op":[{"d":"test", "p":345}],
"v": 789
};
this.rclient.rpush = sinon.stub().yields();
return this.callback = sinon.stub();
});
describe("successfully", function() {
beforeEach(function() {
return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback);
});
it("should push the change", function() {
return this.rclient.rpush
.calledWith(`PendingUpdates:${this.doc_id}`, JSON.stringify(this.change))
.should.equal(true);
});
return it("should notify the doc updater of the change via the pending-updates-list queue", function() {
return this.rclient.rpush
.calledWith("pending-updates-list", `${this.project_id}:${this.doc_id}`)
.should.equal(true);
});
});
describe("with error talking to redis during rpush", function() {
beforeEach(function() {
this.rclient.rpush = sinon.stub().yields(new Error("something went wrong"));
return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback);
});
return it("should return an error", function() {
return this.callback.calledWithExactly(sinon.match(Error)).should.equal(true);
});
});
describe("with null byte corruption", function() {
beforeEach(function() {
this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]';
return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback);
});
it("should return an error", function() {
return this.callback.calledWithExactly(sinon.match(Error)).should.equal(true);
});
return it("should not push the change onto the pending-updates-list queue", function() {
return this.rclient.rpush.called.should.equal(false);
});
});
describe("when the update is too large", function() {
beforeEach(function() {
this.change = {op: {p: 12,t: "update is too large".repeat(1024 * 400)}};
return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback);
});
it("should return an error", function() {
return this.callback.calledWithExactly(sinon.match(Error)).should.equal(true);
});
it("should add the size to the error", function() {
return this.callback.args[0][0].updateSize.should.equal(7782422);
});
return it("should not push the change onto the pending-updates-list queue", function() {
return this.rclient.rpush.called.should.equal(false);
});
});
return describe("with invalid keys", function() {
beforeEach(function() {
this.change = {
"op":[{"d":"test", "p":345}],
"version": 789 // not a valid key
};
return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback);
});
return it("should remove the invalid keys from the change", function() {
return this.rclient.rpush
.calledWith(`PendingUpdates:${this.doc_id}`, JSON.stringify({op:this.change.op}))
.should.equal(true);
});
});
});
});

View file

@ -1,81 +1,113 @@
should = require('chai').should()
sinon = require "sinon"
SandboxedModule = require('sandboxed-module')
path = require "path"
modulePath = path.join __dirname, "../../../app/js/DrainManager"
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const should = require('chai').should();
const sinon = require("sinon");
const SandboxedModule = require('sandboxed-module');
const path = require("path");
const modulePath = path.join(__dirname, "../../../app/js/DrainManager");
describe "DrainManager", ->
beforeEach ->
@DrainManager = SandboxedModule.require modulePath, requires:
"logger-sharelatex": @logger = log: sinon.stub()
@io =
sockets:
describe("DrainManager", function() {
beforeEach(function() {
this.DrainManager = SandboxedModule.require(modulePath, { requires: {
"logger-sharelatex": (this.logger = {log: sinon.stub()})
}
}
);
return this.io = {
sockets: {
clients: sinon.stub()
}
};
});
describe "startDrainTimeWindow", ->
beforeEach ->
@clients = []
for i in [0..5399]
@clients[i] = {
id: i
describe("startDrainTimeWindow", function() {
beforeEach(function() {
this.clients = [];
for (let i = 0; i <= 5399; i++) {
this.clients[i] = {
id: i,
emit: sinon.stub()
}
@io.sockets.clients.returns @clients
@DrainManager.startDrain = sinon.stub()
};
}
this.io.sockets.clients.returns(this.clients);
return this.DrainManager.startDrain = sinon.stub();
});
it "should set a drain rate fast enough", (done)->
@DrainManager.startDrainTimeWindow(@io, 9)
@DrainManager.startDrain.calledWith(@io, 10).should.equal true
done()
return it("should set a drain rate fast enough", function(done){
this.DrainManager.startDrainTimeWindow(this.io, 9);
this.DrainManager.startDrain.calledWith(this.io, 10).should.equal(true);
return done();
});
});
describe "reconnectNClients", ->
beforeEach ->
@clients = []
for i in [0..9]
@clients[i] = {
id: i
return describe("reconnectNClients", function() {
beforeEach(function() {
this.clients = [];
for (let i = 0; i <= 9; i++) {
this.clients[i] = {
id: i,
emit: sinon.stub()
}
@io.sockets.clients.returns @clients
};
}
return this.io.sockets.clients.returns(this.clients);
});
describe "after first pass", ->
beforeEach ->
@DrainManager.reconnectNClients(@io, 3)
return describe("after first pass", function() {
beforeEach(function() {
return this.DrainManager.reconnectNClients(this.io, 3);
});
it "should reconnect the first 3 clients", ->
for i in [0..2]
@clients[i].emit.calledWith("reconnectGracefully").should.equal true
it("should reconnect the first 3 clients", function() {
return [0, 1, 2].map((i) =>
this.clients[i].emit.calledWith("reconnectGracefully").should.equal(true));
});
it "should not reconnect any more clients", ->
for i in [3..9]
@clients[i].emit.calledWith("reconnectGracefully").should.equal false
it("should not reconnect any more clients", function() {
return [3, 4, 5, 6, 7, 8, 9].map((i) =>
this.clients[i].emit.calledWith("reconnectGracefully").should.equal(false));
});
describe "after second pass", ->
beforeEach ->
@DrainManager.reconnectNClients(@io, 3)
return describe("after second pass", function() {
beforeEach(function() {
return this.DrainManager.reconnectNClients(this.io, 3);
});
it "should reconnect the next 3 clients", ->
for i in [3..5]
@clients[i].emit.calledWith("reconnectGracefully").should.equal true
it("should reconnect the next 3 clients", function() {
return [3, 4, 5].map((i) =>
this.clients[i].emit.calledWith("reconnectGracefully").should.equal(true));
});
it "should not reconnect any more clients", ->
for i in [6..9]
@clients[i].emit.calledWith("reconnectGracefully").should.equal false
it("should not reconnect any more clients", function() {
return [6, 7, 8, 9].map((i) =>
this.clients[i].emit.calledWith("reconnectGracefully").should.equal(false));
});
it "should not reconnect the first 3 clients again", ->
for i in [0..2]
@clients[i].emit.calledOnce.should.equal true
it("should not reconnect the first 3 clients again", function() {
return [0, 1, 2].map((i) =>
this.clients[i].emit.calledOnce.should.equal(true));
});
describe "after final pass", ->
beforeEach ->
@DrainManager.reconnectNClients(@io, 100)
return describe("after final pass", function() {
beforeEach(function() {
return this.DrainManager.reconnectNClients(this.io, 100);
});
it "should not reconnect the first 6 clients again", ->
for i in [0..5]
@clients[i].emit.calledOnce.should.equal true
it("should not reconnect the first 6 clients again", function() {
return [0, 1, 2, 3, 4, 5].map((i) =>
this.clients[i].emit.calledOnce.should.equal(true));
});
it "should log out that it reached the end", ->
@logger.log
return it("should log out that it reached the end", function() {
return this.logger.log
.calledWith("All clients have been told to reconnectGracefully")
.should.equal true
.should.equal(true);
});
});
});
});
});
});

View file

@ -1,76 +1,101 @@
require('chai').should()
expect = require("chai").expect
SandboxedModule = require('sandboxed-module')
modulePath = '../../../app/js/EventLogger'
sinon = require("sinon")
tk = require "timekeeper"
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
require('chai').should();
const {
expect
} = require("chai");
const SandboxedModule = require('sandboxed-module');
const modulePath = '../../../app/js/EventLogger';
const sinon = require("sinon");
const tk = require("timekeeper");
describe 'EventLogger', ->
beforeEach ->
@start = Date.now()
tk.freeze(new Date(@start))
@EventLogger = SandboxedModule.require modulePath, requires:
"logger-sharelatex": @logger = {error: sinon.stub(), warn: sinon.stub()}
"metrics-sharelatex": @metrics = {inc: sinon.stub()}
@channel = "applied-ops"
@id_1 = "random-hostname:abc-1"
@message_1 = "message-1"
@id_2 = "random-hostname:abc-2"
@message_2 = "message-2"
describe('EventLogger', function() {
beforeEach(function() {
this.start = Date.now();
tk.freeze(new Date(this.start));
this.EventLogger = SandboxedModule.require(modulePath, { requires: {
"logger-sharelatex": (this.logger = {error: sinon.stub(), warn: sinon.stub()}),
"metrics-sharelatex": (this.metrics = {inc: sinon.stub()})
}
});
this.channel = "applied-ops";
this.id_1 = "random-hostname:abc-1";
this.message_1 = "message-1";
this.id_2 = "random-hostname:abc-2";
return this.message_2 = "message-2";
});
afterEach ->
tk.reset()
afterEach(() => tk.reset());
describe 'checkEventOrder', ->
return describe('checkEventOrder', function() {
describe 'when the events are in order', ->
beforeEach ->
@EventLogger.checkEventOrder(@channel, @id_1, @message_1)
@status = @EventLogger.checkEventOrder(@channel, @id_2, @message_2)
describe('when the events are in order', function() {
beforeEach(function() {
this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1);
return this.status = this.EventLogger.checkEventOrder(this.channel, this.id_2, this.message_2);
});
it 'should accept events in order', ->
expect(@status).to.be.undefined
it('should accept events in order', function() {
return expect(this.status).to.be.undefined;
});
it 'should increment the valid event metric', ->
@metrics.inc.calledWith("event.#{@channel}.valid", 1)
.should.equal.true
return it('should increment the valid event metric', function() {
return this.metrics.inc.calledWith(`event.${this.channel}.valid`, 1)
.should.equal.true;
});
});
describe 'when there is a duplicate events', ->
beforeEach ->
@EventLogger.checkEventOrder(@channel, @id_1, @message_1)
@status = @EventLogger.checkEventOrder(@channel, @id_1, @message_1)
describe('when there is a duplicate events', function() {
beforeEach(function() {
this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1);
return this.status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1);
});
it 'should return "duplicate" for the same event', ->
expect(@status).to.equal "duplicate"
it('should return "duplicate" for the same event', function() {
return expect(this.status).to.equal("duplicate");
});
it 'should increment the duplicate event metric', ->
@metrics.inc.calledWith("event.#{@channel}.duplicate", 1)
.should.equal.true
return it('should increment the duplicate event metric', function() {
return this.metrics.inc.calledWith(`event.${this.channel}.duplicate`, 1)
.should.equal.true;
});
});
describe 'when there are out of order events', ->
beforeEach ->
@EventLogger.checkEventOrder(@channel, @id_1, @message_1)
@EventLogger.checkEventOrder(@channel, @id_2, @message_2)
@status = @EventLogger.checkEventOrder(@channel, @id_1, @message_1)
describe('when there are out of order events', function() {
beforeEach(function() {
this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1);
this.EventLogger.checkEventOrder(this.channel, this.id_2, this.message_2);
return this.status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1);
});
it 'should return "out-of-order" for the event', ->
expect(@status).to.equal "out-of-order"
it('should return "out-of-order" for the event', function() {
return expect(this.status).to.equal("out-of-order");
});
it 'should increment the out-of-order event metric', ->
@metrics.inc.calledWith("event.#{@channel}.out-of-order", 1)
.should.equal.true
return it('should increment the out-of-order event metric', function() {
return this.metrics.inc.calledWith(`event.${this.channel}.out-of-order`, 1)
.should.equal.true;
});
});
describe 'after MAX_STALE_TIME_IN_MS', ->
it 'should flush old entries', ->
@EventLogger.MAX_EVENTS_BEFORE_CLEAN = 10
@EventLogger.checkEventOrder(@channel, @id_1, @message_1)
for i in [1..8]
status = @EventLogger.checkEventOrder(@channel, @id_1, @message_1)
expect(status).to.equal "duplicate"
# the next event should flush the old entries aboce
@EventLogger.MAX_STALE_TIME_IN_MS=1000
tk.freeze(new Date(@start + 5 * 1000))
# because we flushed the entries this should not be a duplicate
@EventLogger.checkEventOrder(@channel, 'other-1', @message_2)
status = @EventLogger.checkEventOrder(@channel, @id_1, @message_1)
expect(status).to.be.undefined
return describe('after MAX_STALE_TIME_IN_MS', () => it('should flush old entries', function() {
let status;
this.EventLogger.MAX_EVENTS_BEFORE_CLEAN = 10;
this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1);
for (let i = 1; i <= 8; i++) {
status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1);
expect(status).to.equal("duplicate");
}
// the next event should flush the old entries aboce
this.EventLogger.MAX_STALE_TIME_IN_MS=1000;
tk.freeze(new Date(this.start + (5 * 1000)));
// because we flushed the entries this should not be a duplicate
this.EventLogger.checkEventOrder(this.channel, 'other-1', this.message_2);
status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1);
return expect(status).to.be.undefined;
}));
});
});

View file

@ -1,288 +1,359 @@
chai = require('chai')
expect = chai.expect
should = chai.should()
sinon = require("sinon")
modulePath = "../../../app/js/RoomManager.js"
SandboxedModule = require('sandboxed-module')
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const chai = require('chai');
const {
expect
} = chai;
const should = chai.should();
const sinon = require("sinon");
const modulePath = "../../../app/js/RoomManager.js";
const SandboxedModule = require('sandboxed-module');
describe 'RoomManager', ->
beforeEach ->
@project_id = "project-id-123"
@doc_id = "doc-id-456"
@other_doc_id = "doc-id-789"
@client = {namespace: {name: ''}, id: "first-client"}
@RoomManager = SandboxedModule.require modulePath, requires:
"settings-sharelatex": @settings = {}
"logger-sharelatex": @logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() }
"metrics-sharelatex": @metrics = { gauge: sinon.stub() }
@RoomManager._clientsInRoom = sinon.stub()
@RoomManager._clientAlreadyInRoom = sinon.stub()
@RoomEvents = @RoomManager.eventSource()
sinon.spy(@RoomEvents, 'emit')
sinon.spy(@RoomEvents, 'once')
describe('RoomManager', function() {
beforeEach(function() {
this.project_id = "project-id-123";
this.doc_id = "doc-id-456";
this.other_doc_id = "doc-id-789";
this.client = {namespace: {name: ''}, id: "first-client"};
this.RoomManager = SandboxedModule.require(modulePath, { requires: {
"settings-sharelatex": (this.settings = {}),
"logger-sharelatex": (this.logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() }),
"metrics-sharelatex": (this.metrics = { gauge: sinon.stub() })
}
});
this.RoomManager._clientsInRoom = sinon.stub();
this.RoomManager._clientAlreadyInRoom = sinon.stub();
this.RoomEvents = this.RoomManager.eventSource();
sinon.spy(this.RoomEvents, 'emit');
return sinon.spy(this.RoomEvents, 'once');
});
describe "emitOnCompletion", ->
describe "when a subscribe errors", ->
afterEach () ->
process.removeListener("unhandledRejection", @onUnhandled)
describe("emitOnCompletion", () => describe("when a subscribe errors", function() {
afterEach(function() {
return process.removeListener("unhandledRejection", this.onUnhandled);
});
beforeEach (done) ->
@onUnhandled = (error) =>
@unhandledError = error
done(new Error("unhandledRejection: #{error.message}"))
process.on("unhandledRejection", @onUnhandled)
beforeEach(function(done) {
this.onUnhandled = error => {
this.unhandledError = error;
return done(new Error(`unhandledRejection: ${error.message}`));
};
process.on("unhandledRejection", this.onUnhandled);
reject = undefined
subscribePromise = new Promise((_, r) -> reject = r)
promises = [subscribePromise]
eventName = "project-subscribed-123"
@RoomEvents.once eventName, () ->
setTimeout(done, 100)
@RoomManager.emitOnCompletion(promises, eventName)
setTimeout(() -> reject(new Error("subscribe failed")))
let reject = undefined;
const subscribePromise = new Promise((_, r) => reject = r);
const promises = [subscribePromise];
const eventName = "project-subscribed-123";
this.RoomEvents.once(eventName, () => setTimeout(done, 100));
this.RoomManager.emitOnCompletion(promises, eventName);
return setTimeout(() => reject(new Error("subscribe failed")));
});
it "should keep going", () ->
expect(@unhandledError).to.not.exist
return it("should keep going", function() {
return expect(this.unhandledError).to.not.exist;
});
}));
describe "joinProject", ->
describe("joinProject", function() {
describe "when the project room is empty", ->
describe("when the project room is empty", function() {
beforeEach (done) ->
@RoomManager._clientsInRoom
.withArgs(@client, @project_id)
.onFirstCall().returns(0)
@client.join = sinon.stub()
@callback = sinon.stub()
@RoomEvents.on 'project-active', (id) =>
setTimeout () =>
@RoomEvents.emit "project-subscribed-#{id}"
, 100
@RoomManager.joinProject @client, @project_id, (err) =>
@callback(err)
done()
beforeEach(function(done) {
this.RoomManager._clientsInRoom
.withArgs(this.client, this.project_id)
.onFirstCall().returns(0);
this.client.join = sinon.stub();
this.callback = sinon.stub();
this.RoomEvents.on('project-active', id => {
return setTimeout(() => {
return this.RoomEvents.emit(`project-subscribed-${id}`);
}
, 100);
});
return this.RoomManager.joinProject(this.client, this.project_id, err => {
this.callback(err);
return done();
});
});
it "should emit a 'project-active' event with the id", ->
@RoomEvents.emit.calledWithExactly('project-active', @project_id).should.equal true
it("should emit a 'project-active' event with the id", function() {
return this.RoomEvents.emit.calledWithExactly('project-active', this.project_id).should.equal(true);
});
it "should listen for the 'project-subscribed-id' event", ->
@RoomEvents.once.calledWith("project-subscribed-#{@project_id}").should.equal true
it("should listen for the 'project-subscribed-id' event", function() {
return this.RoomEvents.once.calledWith(`project-subscribed-${this.project_id}`).should.equal(true);
});
it "should join the room using the id", ->
@client.join.calledWithExactly(@project_id).should.equal true
return it("should join the room using the id", function() {
return this.client.join.calledWithExactly(this.project_id).should.equal(true);
});
});
describe "when there are other clients in the project room", ->
return describe("when there are other clients in the project room", function() {
beforeEach ->
@RoomManager._clientsInRoom
.withArgs(@client, @project_id)
beforeEach(function() {
this.RoomManager._clientsInRoom
.withArgs(this.client, this.project_id)
.onFirstCall().returns(123)
.onSecondCall().returns(124)
@client.join = sinon.stub()
@RoomManager.joinProject @client, @project_id
.onSecondCall().returns(124);
this.client.join = sinon.stub();
return this.RoomManager.joinProject(this.client, this.project_id);
});
it "should join the room using the id", ->
@client.join.called.should.equal true
it("should join the room using the id", function() {
return this.client.join.called.should.equal(true);
});
it "should not emit any events", ->
@RoomEvents.emit.called.should.equal false
return it("should not emit any events", function() {
return this.RoomEvents.emit.called.should.equal(false);
});
});
});
describe "joinDoc", ->
describe("joinDoc", function() {
describe "when the doc room is empty", ->
describe("when the doc room is empty", function() {
beforeEach (done) ->
@RoomManager._clientsInRoom
.withArgs(@client, @doc_id)
.onFirstCall().returns(0)
@client.join = sinon.stub()
@callback = sinon.stub()
@RoomEvents.on 'doc-active', (id) =>
setTimeout () =>
@RoomEvents.emit "doc-subscribed-#{id}"
, 100
@RoomManager.joinDoc @client, @doc_id, (err) =>
@callback(err)
done()
beforeEach(function(done) {
this.RoomManager._clientsInRoom
.withArgs(this.client, this.doc_id)
.onFirstCall().returns(0);
this.client.join = sinon.stub();
this.callback = sinon.stub();
this.RoomEvents.on('doc-active', id => {
return setTimeout(() => {
return this.RoomEvents.emit(`doc-subscribed-${id}`);
}
, 100);
});
return this.RoomManager.joinDoc(this.client, this.doc_id, err => {
this.callback(err);
return done();
});
});
it "should emit a 'doc-active' event with the id", ->
@RoomEvents.emit.calledWithExactly('doc-active', @doc_id).should.equal true
it("should emit a 'doc-active' event with the id", function() {
return this.RoomEvents.emit.calledWithExactly('doc-active', this.doc_id).should.equal(true);
});
it "should listen for the 'doc-subscribed-id' event", ->
@RoomEvents.once.calledWith("doc-subscribed-#{@doc_id}").should.equal true
it("should listen for the 'doc-subscribed-id' event", function() {
return this.RoomEvents.once.calledWith(`doc-subscribed-${this.doc_id}`).should.equal(true);
});
it "should join the room using the id", ->
@client.join.calledWithExactly(@doc_id).should.equal true
return it("should join the room using the id", function() {
return this.client.join.calledWithExactly(this.doc_id).should.equal(true);
});
});
describe "when there are other clients in the doc room", ->
return describe("when there are other clients in the doc room", function() {
beforeEach ->
@RoomManager._clientsInRoom
.withArgs(@client, @doc_id)
beforeEach(function() {
this.RoomManager._clientsInRoom
.withArgs(this.client, this.doc_id)
.onFirstCall().returns(123)
.onSecondCall().returns(124)
@client.join = sinon.stub()
@RoomManager.joinDoc @client, @doc_id
.onSecondCall().returns(124);
this.client.join = sinon.stub();
return this.RoomManager.joinDoc(this.client, this.doc_id);
});
it "should join the room using the id", ->
@client.join.called.should.equal true
it("should join the room using the id", function() {
return this.client.join.called.should.equal(true);
});
it "should not emit any events", ->
@RoomEvents.emit.called.should.equal false
return it("should not emit any events", function() {
return this.RoomEvents.emit.called.should.equal(false);
});
});
});
describe "leaveDoc", ->
describe("leaveDoc", function() {
describe "when doc room will be empty after this client has left", ->
describe("when doc room will be empty after this client has left", function() {
beforeEach ->
@RoomManager._clientAlreadyInRoom
.withArgs(@client, @doc_id)
.returns(true)
@RoomManager._clientsInRoom
.withArgs(@client, @doc_id)
.onCall(0).returns(0)
@client.leave = sinon.stub()
@RoomManager.leaveDoc @client, @doc_id
beforeEach(function() {
this.RoomManager._clientAlreadyInRoom
.withArgs(this.client, this.doc_id)
.returns(true);
this.RoomManager._clientsInRoom
.withArgs(this.client, this.doc_id)
.onCall(0).returns(0);
this.client.leave = sinon.stub();
return this.RoomManager.leaveDoc(this.client, this.doc_id);
});
it "should leave the room using the id", ->
@client.leave.calledWithExactly(@doc_id).should.equal true
it("should leave the room using the id", function() {
return this.client.leave.calledWithExactly(this.doc_id).should.equal(true);
});
it "should emit a 'doc-empty' event with the id", ->
@RoomEvents.emit.calledWithExactly('doc-empty', @doc_id).should.equal true
return it("should emit a 'doc-empty' event with the id", function() {
return this.RoomEvents.emit.calledWithExactly('doc-empty', this.doc_id).should.equal(true);
});
});
describe "when there are other clients in the doc room", ->
describe("when there are other clients in the doc room", function() {
beforeEach ->
@RoomManager._clientAlreadyInRoom
.withArgs(@client, @doc_id)
.returns(true)
@RoomManager._clientsInRoom
.withArgs(@client, @doc_id)
.onCall(0).returns(123)
@client.leave = sinon.stub()
@RoomManager.leaveDoc @client, @doc_id
beforeEach(function() {
this.RoomManager._clientAlreadyInRoom
.withArgs(this.client, this.doc_id)
.returns(true);
this.RoomManager._clientsInRoom
.withArgs(this.client, this.doc_id)
.onCall(0).returns(123);
this.client.leave = sinon.stub();
return this.RoomManager.leaveDoc(this.client, this.doc_id);
});
it "should leave the room using the id", ->
@client.leave.calledWithExactly(@doc_id).should.equal true
it("should leave the room using the id", function() {
return this.client.leave.calledWithExactly(this.doc_id).should.equal(true);
});
it "should not emit any events", ->
@RoomEvents.emit.called.should.equal false
return it("should not emit any events", function() {
return this.RoomEvents.emit.called.should.equal(false);
});
});
describe "when the client is not in the doc room", ->
return describe("when the client is not in the doc room", function() {
beforeEach ->
@RoomManager._clientAlreadyInRoom
.withArgs(@client, @doc_id)
.returns(false)
@RoomManager._clientsInRoom
.withArgs(@client, @doc_id)
.onCall(0).returns(0)
@client.leave = sinon.stub()
@RoomManager.leaveDoc @client, @doc_id
beforeEach(function() {
this.RoomManager._clientAlreadyInRoom
.withArgs(this.client, this.doc_id)
.returns(false);
this.RoomManager._clientsInRoom
.withArgs(this.client, this.doc_id)
.onCall(0).returns(0);
this.client.leave = sinon.stub();
return this.RoomManager.leaveDoc(this.client, this.doc_id);
});
it "should not leave the room", ->
@client.leave.called.should.equal false
it("should not leave the room", function() {
return this.client.leave.called.should.equal(false);
});
it "should not emit any events", ->
@RoomEvents.emit.called.should.equal false
return it("should not emit any events", function() {
return this.RoomEvents.emit.called.should.equal(false);
});
});
});
describe "leaveProjectAndDocs", ->
return describe("leaveProjectAndDocs", () => describe("when the client is connected to the project and multiple docs", function() {
describe "when the client is connected to the project and multiple docs", ->
beforeEach(function() {
this.RoomManager._roomsClientIsIn = sinon.stub().returns([this.project_id, this.doc_id, this.other_doc_id]);
this.client.join = sinon.stub();
return this.client.leave = sinon.stub();
});
beforeEach ->
@RoomManager._roomsClientIsIn = sinon.stub().returns [@project_id, @doc_id, @other_doc_id]
@client.join = sinon.stub()
@client.leave = sinon.stub()
describe("when this is the only client connected", function() {
describe "when this is the only client connected", ->
beforeEach(function(done) {
// first call is for the join,
// second for the leave
this.RoomManager._clientsInRoom
.withArgs(this.client, this.doc_id)
.onCall(0).returns(0)
.onCall(1).returns(0);
this.RoomManager._clientsInRoom
.withArgs(this.client, this.other_doc_id)
.onCall(0).returns(0)
.onCall(1).returns(0);
this.RoomManager._clientsInRoom
.withArgs(this.client, this.project_id)
.onCall(0).returns(0)
.onCall(1).returns(0);
this.RoomManager._clientAlreadyInRoom
.withArgs(this.client, this.doc_id)
.returns(true)
.withArgs(this.client, this.other_doc_id)
.returns(true)
.withArgs(this.client, this.project_id)
.returns(true);
this.RoomEvents.on('project-active', id => {
return setTimeout(() => {
return this.RoomEvents.emit(`project-subscribed-${id}`);
}
, 100);
});
this.RoomEvents.on('doc-active', id => {
return setTimeout(() => {
return this.RoomEvents.emit(`doc-subscribed-${id}`);
}
, 100);
});
// put the client in the rooms
return this.RoomManager.joinProject(this.client, this.project_id, () => {
return this.RoomManager.joinDoc(this.client, this.doc_id, () => {
return this.RoomManager.joinDoc(this.client, this.other_doc_id, () => {
// now leave the project
this.RoomManager.leaveProjectAndDocs(this.client);
return done();
});
});
});
});
beforeEach (done) ->
# first call is for the join,
# second for the leave
@RoomManager._clientsInRoom
.withArgs(@client, @doc_id)
.onCall(0).returns(0)
.onCall(1).returns(0)
@RoomManager._clientsInRoom
.withArgs(@client, @other_doc_id)
.onCall(0).returns(0)
.onCall(1).returns(0)
@RoomManager._clientsInRoom
.withArgs(@client, @project_id)
.onCall(0).returns(0)
.onCall(1).returns(0)
@RoomManager._clientAlreadyInRoom
.withArgs(@client, @doc_id)
.returns(true)
.withArgs(@client, @other_doc_id)
.returns(true)
.withArgs(@client, @project_id)
.returns(true)
@RoomEvents.on 'project-active', (id) =>
setTimeout () =>
@RoomEvents.emit "project-subscribed-#{id}"
, 100
@RoomEvents.on 'doc-active', (id) =>
setTimeout () =>
@RoomEvents.emit "doc-subscribed-#{id}"
, 100
# put the client in the rooms
@RoomManager.joinProject @client, @project_id, () =>
@RoomManager.joinDoc @client, @doc_id, () =>
@RoomManager.joinDoc @client, @other_doc_id, () =>
# now leave the project
@RoomManager.leaveProjectAndDocs @client
done()
it("should leave all the docs", function() {
this.client.leave.calledWithExactly(this.doc_id).should.equal(true);
return this.client.leave.calledWithExactly(this.other_doc_id).should.equal(true);
});
it "should leave all the docs", ->
@client.leave.calledWithExactly(@doc_id).should.equal true
@client.leave.calledWithExactly(@other_doc_id).should.equal true
it("should leave the project", function() {
return this.client.leave.calledWithExactly(this.project_id).should.equal(true);
});
it "should leave the project", ->
@client.leave.calledWithExactly(@project_id).should.equal true
it("should emit a 'doc-empty' event with the id for each doc", function() {
this.RoomEvents.emit.calledWithExactly('doc-empty', this.doc_id).should.equal(true);
return this.RoomEvents.emit.calledWithExactly('doc-empty', this.other_doc_id).should.equal(true);
});
it "should emit a 'doc-empty' event with the id for each doc", ->
@RoomEvents.emit.calledWithExactly('doc-empty', @doc_id).should.equal true
@RoomEvents.emit.calledWithExactly('doc-empty', @other_doc_id).should.equal true
return it("should emit a 'project-empty' event with the id for the project", function() {
return this.RoomEvents.emit.calledWithExactly('project-empty', this.project_id).should.equal(true);
});
});
it "should emit a 'project-empty' event with the id for the project", ->
@RoomEvents.emit.calledWithExactly('project-empty', @project_id).should.equal true
return describe("when other clients are still connected", function() {
describe "when other clients are still connected", ->
beforeEach(function() {
this.RoomManager._clientsInRoom
.withArgs(this.client, this.doc_id)
.onFirstCall().returns(123)
.onSecondCall().returns(122);
this.RoomManager._clientsInRoom
.withArgs(this.client, this.other_doc_id)
.onFirstCall().returns(123)
.onSecondCall().returns(122);
this.RoomManager._clientsInRoom
.withArgs(this.client, this.project_id)
.onFirstCall().returns(123)
.onSecondCall().returns(122);
this.RoomManager._clientAlreadyInRoom
.withArgs(this.client, this.doc_id)
.returns(true)
.withArgs(this.client, this.other_doc_id)
.returns(true)
.withArgs(this.client, this.project_id)
.returns(true);
return this.RoomManager.leaveProjectAndDocs(this.client);
});
beforeEach ->
@RoomManager._clientsInRoom
.withArgs(@client, @doc_id)
.onFirstCall().returns(123)
.onSecondCall().returns(122)
@RoomManager._clientsInRoom
.withArgs(@client, @other_doc_id)
.onFirstCall().returns(123)
.onSecondCall().returns(122)
@RoomManager._clientsInRoom
.withArgs(@client, @project_id)
.onFirstCall().returns(123)
.onSecondCall().returns(122)
@RoomManager._clientAlreadyInRoom
.withArgs(@client, @doc_id)
.returns(true)
.withArgs(@client, @other_doc_id)
.returns(true)
.withArgs(@client, @project_id)
.returns(true)
@RoomManager.leaveProjectAndDocs @client
it("should leave all the docs", function() {
this.client.leave.calledWithExactly(this.doc_id).should.equal(true);
return this.client.leave.calledWithExactly(this.other_doc_id).should.equal(true);
});
it "should leave all the docs", ->
@client.leave.calledWithExactly(@doc_id).should.equal true
@client.leave.calledWithExactly(@other_doc_id).should.equal true
it("should leave the project", function() {
return this.client.leave.calledWithExactly(this.project_id).should.equal(true);
});
it "should leave the project", ->
@client.leave.calledWithExactly(@project_id).should.equal true
it "should not emit any events", ->
@RoomEvents.emit.called.should.equal false
return it("should not emit any events", function() {
return this.RoomEvents.emit.called.should.equal(false);
});
});
}));
});

View file

@ -1,34 +1,51 @@
require('chai').should()
expect = require("chai").expect
SandboxedModule = require('sandboxed-module')
modulePath = '../../../app/js/SafeJsonParse'
sinon = require("sinon")
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
require('chai').should();
const {
expect
} = require("chai");
const SandboxedModule = require('sandboxed-module');
const modulePath = '../../../app/js/SafeJsonParse';
const sinon = require("sinon");
describe 'SafeJsonParse', ->
beforeEach ->
@SafeJsonParse = SandboxedModule.require modulePath, requires:
"settings-sharelatex": @Settings = {
describe('SafeJsonParse', function() {
beforeEach(function() {
return this.SafeJsonParse = SandboxedModule.require(modulePath, { requires: {
"settings-sharelatex": (this.Settings = {
maxUpdateSize: 16 * 1024
}
"logger-sharelatex": @logger = {error: sinon.stub()}
}),
"logger-sharelatex": (this.logger = {error: sinon.stub()})
}
});});
describe "parse", ->
it "should parse documents correctly", (done) ->
@SafeJsonParse.parse '{"foo": "bar"}', (error, parsed) ->
expect(parsed).to.deep.equal {foo: "bar"}
done()
return describe("parse", function() {
it("should parse documents correctly", function(done) {
return this.SafeJsonParse.parse('{"foo": "bar"}', function(error, parsed) {
expect(parsed).to.deep.equal({foo: "bar"});
return done();
});
});
it "should return an error on bad data", (done) ->
@SafeJsonParse.parse 'blah', (error, parsed) ->
expect(error).to.exist
done()
it("should return an error on bad data", function(done) {
return this.SafeJsonParse.parse('blah', function(error, parsed) {
expect(error).to.exist;
return done();
});
});
it "should return an error on oversized data", (done) ->
# we have a 2k overhead on top of max size
big_blob = Array(16*1024).join("A")
data = "{\"foo\": \"#{big_blob}\"}"
@Settings.maxUpdateSize = 2 * 1024
@SafeJsonParse.parse data, (error, parsed) =>
@logger.error.called.should.equal true
expect(error).to.exist
done()
return it("should return an error on oversized data", function(done) {
// we have a 2k overhead on top of max size
const big_blob = Array(16*1024).join("A");
const data = `{\"foo\": \"${big_blob}\"}`;
this.Settings.maxUpdateSize = 2 * 1024;
return this.SafeJsonParse.parse(data, (error, parsed) => {
this.logger.error.called.should.equal(true);
expect(error).to.exist;
return done();
});
});
});
});

View file

@ -1,126 +1,170 @@
{EventEmitter} = require('events')
{expect} = require('chai')
SandboxedModule = require('sandboxed-module')
modulePath = '../../../app/js/SessionSockets'
sinon = require('sinon')
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const {EventEmitter} = require('events');
const {expect} = require('chai');
const SandboxedModule = require('sandboxed-module');
const modulePath = '../../../app/js/SessionSockets';
const sinon = require('sinon');
describe 'SessionSockets', ->
before ->
@SessionSocketsModule = SandboxedModule.require modulePath
@io = new EventEmitter()
@id1 = Math.random().toString()
@id2 = Math.random().toString()
redisResponses =
error: [new Error('Redis: something went wrong'), null]
describe('SessionSockets', function() {
before(function() {
this.SessionSocketsModule = SandboxedModule.require(modulePath);
this.io = new EventEmitter();
this.id1 = Math.random().toString();
this.id2 = Math.random().toString();
const redisResponses = {
error: [new Error('Redis: something went wrong'), null],
unknownId: [null, null]
redisResponses[@id1] = [null, {user: {_id: '123'}}]
redisResponses[@id2] = [null, {user: {_id: 'abc'}}]
};
redisResponses[this.id1] = [null, {user: {_id: '123'}}];
redisResponses[this.id2] = [null, {user: {_id: 'abc'}}];
@sessionStore =
get: sinon.stub().callsFake (id, fn) ->
fn.apply(null, redisResponses[id])
@cookieParser = (req, res, next) ->
req.signedCookies = req._signedCookies
next()
@SessionSockets = @SessionSocketsModule(@io, @sessionStore, @cookieParser, 'ol.sid')
@checkSocket = (socket, fn) =>
@SessionSockets.once('connection', fn)
@io.emit('connection', socket)
this.sessionStore = {
get: sinon.stub().callsFake((id, fn) => fn.apply(null, redisResponses[id]))
};
this.cookieParser = function(req, res, next) {
req.signedCookies = req._signedCookies;
return next();
};
this.SessionSockets = this.SessionSocketsModule(this.io, this.sessionStore, this.cookieParser, 'ol.sid');
return this.checkSocket = (socket, fn) => {
this.SessionSockets.once('connection', fn);
return this.io.emit('connection', socket);
};
});
describe 'without cookies', ->
before ->
@socket = {handshake: {}}
describe('without cookies', function() {
before(function() {
return this.socket = {handshake: {}};});
it 'should return a lookup error', (done) ->
@checkSocket @socket, (error) ->
expect(error).to.exist
expect(error.message).to.equal('could not look up session by key')
done()
it('should return a lookup error', function(done) {
return this.checkSocket(this.socket, function(error) {
expect(error).to.exist;
expect(error.message).to.equal('could not look up session by key');
return done();
});
});
it 'should not query redis', (done) ->
@checkSocket @socket, () =>
expect(@sessionStore.get.called).to.equal(false)
done()
return it('should not query redis', function(done) {
return this.checkSocket(this.socket, () => {
expect(this.sessionStore.get.called).to.equal(false);
return done();
});
});
});
describe 'with a different cookie', ->
before ->
@socket = {handshake: {_signedCookies: {other: 1}}}
describe('with a different cookie', function() {
before(function() {
return this.socket = {handshake: {_signedCookies: {other: 1}}};});
it 'should return a lookup error', (done) ->
@checkSocket @socket, (error) ->
expect(error).to.exist
expect(error.message).to.equal('could not look up session by key')
done()
it('should return a lookup error', function(done) {
return this.checkSocket(this.socket, function(error) {
expect(error).to.exist;
expect(error.message).to.equal('could not look up session by key');
return done();
});
});
it 'should not query redis', (done) ->
@checkSocket @socket, () =>
expect(@sessionStore.get.called).to.equal(false)
done()
return it('should not query redis', function(done) {
return this.checkSocket(this.socket, () => {
expect(this.sessionStore.get.called).to.equal(false);
return done();
});
});
});
describe 'with a valid cookie and a failing session lookup', ->
before ->
@socket = {handshake: {_signedCookies: {'ol.sid': 'error'}}}
describe('with a valid cookie and a failing session lookup', function() {
before(function() {
return this.socket = {handshake: {_signedCookies: {'ol.sid': 'error'}}};});
it 'should query redis', (done) ->
@checkSocket @socket, () =>
expect(@sessionStore.get.called).to.equal(true)
done()
it('should query redis', function(done) {
return this.checkSocket(this.socket, () => {
expect(this.sessionStore.get.called).to.equal(true);
return done();
});
});
it 'should return a redis error', (done) ->
@checkSocket @socket, (error) ->
expect(error).to.exist
expect(error.message).to.equal('Redis: something went wrong')
done()
return it('should return a redis error', function(done) {
return this.checkSocket(this.socket, function(error) {
expect(error).to.exist;
expect(error.message).to.equal('Redis: something went wrong');
return done();
});
});
});
describe 'with a valid cookie and no matching session', ->
before ->
@socket = {handshake: {_signedCookies: {'ol.sid': 'unknownId'}}}
describe('with a valid cookie and no matching session', function() {
before(function() {
return this.socket = {handshake: {_signedCookies: {'ol.sid': 'unknownId'}}};});
it 'should query redis', (done) ->
@checkSocket @socket, () =>
expect(@sessionStore.get.called).to.equal(true)
done()
it('should query redis', function(done) {
return this.checkSocket(this.socket, () => {
expect(this.sessionStore.get.called).to.equal(true);
return done();
});
});
it 'should return a lookup error', (done) ->
@checkSocket @socket, (error) ->
expect(error).to.exist
expect(error.message).to.equal('could not look up session by key')
done()
return it('should return a lookup error', function(done) {
return this.checkSocket(this.socket, function(error) {
expect(error).to.exist;
expect(error.message).to.equal('could not look up session by key');
return done();
});
});
});
describe 'with a valid cookie and a matching session', ->
before ->
@socket = {handshake: {_signedCookies: {'ol.sid': @id1}}}
describe('with a valid cookie and a matching session', function() {
before(function() {
return this.socket = {handshake: {_signedCookies: {'ol.sid': this.id1}}};});
it 'should query redis', (done) ->
@checkSocket @socket, () =>
expect(@sessionStore.get.called).to.equal(true)
done()
it('should query redis', function(done) {
return this.checkSocket(this.socket, () => {
expect(this.sessionStore.get.called).to.equal(true);
return done();
});
});
it 'should not return an error', (done) ->
@checkSocket @socket, (error) ->
expect(error).to.not.exist
done()
it('should not return an error', function(done) {
return this.checkSocket(this.socket, function(error) {
expect(error).to.not.exist;
return done();
});
});
it 'should return the session', (done) ->
@checkSocket @socket, (error, s, session) ->
expect(session).to.deep.equal({user: {_id: '123'}})
done()
return it('should return the session', function(done) {
return this.checkSocket(this.socket, function(error, s, session) {
expect(session).to.deep.equal({user: {_id: '123'}});
return done();
});
});
});
describe 'with a different valid cookie and matching session', ->
before ->
@socket = {handshake: {_signedCookies: {'ol.sid': @id2}}}
return describe('with a different valid cookie and matching session', function() {
before(function() {
return this.socket = {handshake: {_signedCookies: {'ol.sid': this.id2}}};});
it 'should query redis', (done) ->
@checkSocket @socket, () =>
expect(@sessionStore.get.called).to.equal(true)
done()
it('should query redis', function(done) {
return this.checkSocket(this.socket, () => {
expect(this.sessionStore.get.called).to.equal(true);
return done();
});
});
it 'should not return an error', (done) ->
@checkSocket @socket, (error) ->
expect(error).to.not.exist
done()
it('should not return an error', function(done) {
return this.checkSocket(this.socket, function(error) {
expect(error).to.not.exist;
return done();
});
});
it 'should return the other session', (done) ->
@checkSocket @socket, (error, s, session) ->
expect(session).to.deep.equal({user: {_id: 'abc'}})
done()
return it('should return the other session', function(done) {
return this.checkSocket(this.socket, function(error, s, session) {
expect(session).to.deep.equal({user: {_id: 'abc'}});
return done();
});
});
});
});

View file

@ -1,84 +1,111 @@
chai = require('chai')
should = chai.should()
sinon = require("sinon")
modulePath = "../../../app/js/WebApiManager.js"
SandboxedModule = require('sandboxed-module')
{ CodedError } = require('../../../app/js/Errors')
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const chai = require('chai');
const should = chai.should();
const sinon = require("sinon");
const modulePath = "../../../app/js/WebApiManager.js";
const SandboxedModule = require('sandboxed-module');
const { CodedError } = require('../../../app/js/Errors');
describe 'WebApiManager', ->
beforeEach ->
@project_id = "project-id-123"
@user_id = "user-id-123"
@user = {_id: @user_id}
@callback = sinon.stub()
@WebApiManager = SandboxedModule.require modulePath, requires:
"request": @request = {}
"settings-sharelatex": @settings =
apis:
web:
url: "http://web.example.com"
user: "username"
describe('WebApiManager', function() {
beforeEach(function() {
this.project_id = "project-id-123";
this.user_id = "user-id-123";
this.user = {_id: this.user_id};
this.callback = sinon.stub();
return this.WebApiManager = SandboxedModule.require(modulePath, { requires: {
"request": (this.request = {}),
"settings-sharelatex": (this.settings = {
apis: {
web: {
url: "http://web.example.com",
user: "username",
pass: "password"
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
}
}
}),
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() })
}
});});
describe "joinProject", ->
describe "successfully", ->
beforeEach ->
@response = {
project: { name: "Test project" }
return describe("joinProject", function() {
describe("successfully", function() {
beforeEach(function() {
this.response = {
project: { name: "Test project" },
privilegeLevel: "owner",
isRestrictedUser: true
}
@request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @response)
@WebApiManager.joinProject @project_id, @user, @callback
};
this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.response);
return this.WebApiManager.joinProject(this.project_id, this.user, this.callback);
});
it "should send a request to web to join the project", ->
@request.post
it("should send a request to web to join the project", function() {
return this.request.post
.calledWith({
url: "#{@settings.apis.web.url}/project/#{@project_id}/join"
qs:
user_id: @user_id
auth:
user: @settings.apis.web.user
pass: @settings.apis.web.pass
url: `${this.settings.apis.web.url}/project/${this.project_id}/join`,
qs: {
user_id: this.user_id
},
auth: {
user: this.settings.apis.web.user,
pass: this.settings.apis.web.pass,
sendImmediately: true
json: true
jar: false
},
json: true,
jar: false,
headers: {}
})
.should.equal true
.should.equal(true);
});
it "should return the project, privilegeLevel, and restricted flag", ->
@callback
.calledWith(null, @response.project, @response.privilegeLevel, @response.isRestrictedUser)
.should.equal true
return it("should return the project, privilegeLevel, and restricted flag", function() {
return this.callback
.calledWith(null, this.response.project, this.response.privilegeLevel, this.response.isRestrictedUser)
.should.equal(true);
});
});
describe "with an error from web", ->
beforeEach ->
@request.post = sinon.stub().callsArgWith(1, null, {statusCode: 500}, null)
@WebApiManager.joinProject @project_id, @user_id, @callback
describe("with an error from web", function() {
beforeEach(function() {
this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 500}, null);
return this.WebApiManager.joinProject(this.project_id, this.user_id, this.callback);
});
it "should call the callback with an error", ->
@callback
return it("should call the callback with an error", function() {
return this.callback
.calledWith(sinon.match({message: "non-success status code from web: 500"}))
.should.equal true
.should.equal(true);
});
});
describe "with no data from web", ->
beforeEach ->
@request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, null)
@WebApiManager.joinProject @project_id, @user_id, @callback
describe("with no data from web", function() {
beforeEach(function() {
this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, null);
return this.WebApiManager.joinProject(this.project_id, this.user_id, this.callback);
});
it "should call the callback with an error", ->
@callback
return it("should call the callback with an error", function() {
return this.callback
.calledWith(sinon.match({message: "no data returned from joinProject request"}))
.should.equal true
.should.equal(true);
});
});
describe "when the project is over its rate limit", ->
beforeEach ->
@request.post = sinon.stub().callsArgWith(1, null, {statusCode: 429}, null)
@WebApiManager.joinProject @project_id, @user_id, @callback
return describe("when the project is over its rate limit", function() {
beforeEach(function() {
this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 429}, null);
return this.WebApiManager.joinProject(this.project_id, this.user_id, this.callback);
});
it "should call the callback with a TooManyRequests error code", ->
@callback
return it("should call the callback with a TooManyRequests error code", function() {
return this.callback
.calledWith(sinon.match({message: "rate-limit hit when joining project", code: "TooManyRequests"}))
.should.equal true
.should.equal(true);
});
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -1,161 +1,204 @@
SandboxedModule = require('sandboxed-module')
sinon = require('sinon')
require('chai').should()
modulePath = require('path').join __dirname, '../../../app/js/WebsocketLoadBalancer'
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module');
const sinon = require('sinon');
require('chai').should();
const modulePath = require('path').join(__dirname, '../../../app/js/WebsocketLoadBalancer');
describe "WebsocketLoadBalancer", ->
beforeEach ->
@rclient = {}
@RoomEvents = {on: sinon.stub()}
@WebsocketLoadBalancer = SandboxedModule.require modulePath, requires:
"./RedisClientManager":
describe("WebsocketLoadBalancer", function() {
beforeEach(function() {
this.rclient = {};
this.RoomEvents = {on: sinon.stub()};
this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, { requires: {
"./RedisClientManager": {
createClientList: () => []
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"./SafeJsonParse": @SafeJsonParse =
parse: (data, cb) => cb null, JSON.parse(data)
"./EventLogger": {checkEventOrder: sinon.stub()}
"./HealthCheckManager": {check: sinon.stub()}
"./RoomManager" : @RoomManager = {eventSource: sinon.stub().returns @RoomEvents}
"./ChannelManager": @ChannelManager = {publish: sinon.stub()}
"./ConnectedUsersManager": @ConnectedUsersManager = {refreshClient: sinon.stub()}
@io = {}
@WebsocketLoadBalancer.rclientPubList = [{publish: sinon.stub()}]
@WebsocketLoadBalancer.rclientSubList = [{
subscribe: sinon.stub()
},
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
"./SafeJsonParse": (this.SafeJsonParse =
{parse: (data, cb) => cb(null, JSON.parse(data))}),
"./EventLogger": {checkEventOrder: sinon.stub()},
"./HealthCheckManager": {check: sinon.stub()},
"./RoomManager" : (this.RoomManager = {eventSource: sinon.stub().returns(this.RoomEvents)}),
"./ChannelManager": (this.ChannelManager = {publish: sinon.stub()}),
"./ConnectedUsersManager": (this.ConnectedUsersManager = {refreshClient: sinon.stub()})
}
});
this.io = {};
this.WebsocketLoadBalancer.rclientPubList = [{publish: sinon.stub()}];
this.WebsocketLoadBalancer.rclientSubList = [{
subscribe: sinon.stub(),
on: sinon.stub()
}]
}];
@room_id = "room-id"
@message = "otUpdateApplied"
@payload = ["argument one", 42]
this.room_id = "room-id";
this.message = "otUpdateApplied";
return this.payload = ["argument one", 42];});
describe "emitToRoom", ->
beforeEach ->
@WebsocketLoadBalancer.emitToRoom(@room_id, @message, @payload...)
describe("emitToRoom", function() {
beforeEach(function() {
return this.WebsocketLoadBalancer.emitToRoom(this.room_id, this.message, ...Array.from(this.payload));
});
it "should publish the message to redis", ->
@ChannelManager.publish
.calledWith(@WebsocketLoadBalancer.rclientPubList[0], "editor-events", @room_id, JSON.stringify(
room_id: @room_id,
message: @message
payload: @payload
))
.should.equal true
return it("should publish the message to redis", function() {
return this.ChannelManager.publish
.calledWith(this.WebsocketLoadBalancer.rclientPubList[0], "editor-events", this.room_id, JSON.stringify({
room_id: this.room_id,
message: this.message,
payload: this.payload
}))
.should.equal(true);
});
});
describe "emitToAll", ->
beforeEach ->
@WebsocketLoadBalancer.emitToRoom = sinon.stub()
@WebsocketLoadBalancer.emitToAll @message, @payload...
describe("emitToAll", function() {
beforeEach(function() {
this.WebsocketLoadBalancer.emitToRoom = sinon.stub();
return this.WebsocketLoadBalancer.emitToAll(this.message, ...Array.from(this.payload));
});
it "should emit to the room 'all'", ->
@WebsocketLoadBalancer.emitToRoom
.calledWith("all", @message, @payload...)
.should.equal true
return it("should emit to the room 'all'", function() {
return this.WebsocketLoadBalancer.emitToRoom
.calledWith("all", this.message, ...Array.from(this.payload))
.should.equal(true);
});
});
describe "listenForEditorEvents", ->
beforeEach ->
@WebsocketLoadBalancer._processEditorEvent = sinon.stub()
@WebsocketLoadBalancer.listenForEditorEvents()
describe("listenForEditorEvents", function() {
beforeEach(function() {
this.WebsocketLoadBalancer._processEditorEvent = sinon.stub();
return this.WebsocketLoadBalancer.listenForEditorEvents();
});
it "should subscribe to the editor-events channel", ->
@WebsocketLoadBalancer.rclientSubList[0].subscribe
it("should subscribe to the editor-events channel", function() {
return this.WebsocketLoadBalancer.rclientSubList[0].subscribe
.calledWith("editor-events")
.should.equal true
.should.equal(true);
});
it "should process the events with _processEditorEvent", ->
@WebsocketLoadBalancer.rclientSubList[0].on
return it("should process the events with _processEditorEvent", function() {
return this.WebsocketLoadBalancer.rclientSubList[0].on
.calledWith("message", sinon.match.func)
.should.equal true
.should.equal(true);
});
});
describe "_processEditorEvent", ->
describe "with bad JSON", ->
beforeEach ->
@isRestrictedUser = false
@SafeJsonParse.parse = sinon.stub().callsArgWith 1, new Error("oops")
@WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", "blah")
return describe("_processEditorEvent", function() {
describe("with bad JSON", function() {
beforeEach(function() {
this.isRestrictedUser = false;
this.SafeJsonParse.parse = sinon.stub().callsArgWith(1, new Error("oops"));
return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", "blah");
});
it "should log an error", ->
@logger.error.called.should.equal true
return it("should log an error", function() {
return this.logger.error.called.should.equal(true);
});
});
describe "with a designated room", ->
beforeEach ->
@io.sockets =
describe("with a designated room", function() {
beforeEach(function() {
this.io.sockets = {
clients: sinon.stub().returns([
{id: 'client-id-1', emit: @emit1 = sinon.stub(), ol_context: {}}
{id: 'client-id-2', emit: @emit2 = sinon.stub(), ol_context: {}}
{id: 'client-id-1', emit: @emit3 = sinon.stub(), ol_context: {}} # duplicate client
{id: 'client-id-1', emit: (this.emit1 = sinon.stub()), ol_context: {}},
{id: 'client-id-2', emit: (this.emit2 = sinon.stub()), ol_context: {}},
{id: 'client-id-1', emit: (this.emit3 = sinon.stub()), ol_context: {}} // duplicate client
])
data = JSON.stringify
room_id: @room_id
message: @message
payload: @payload
@WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", data)
};
const data = JSON.stringify({
room_id: this.room_id,
message: this.message,
payload: this.payload
});
return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data);
});
it "should send the message to all (unique) clients in the room", ->
@io.sockets.clients
.calledWith(@room_id)
.should.equal true
@emit1.calledWith(@message, @payload...).should.equal true
@emit2.calledWith(@message, @payload...).should.equal true
@emit3.called.should.equal false # duplicate client should be ignored
return it("should send the message to all (unique) clients in the room", function() {
this.io.sockets.clients
.calledWith(this.room_id)
.should.equal(true);
this.emit1.calledWith(this.message, ...Array.from(this.payload)).should.equal(true);
this.emit2.calledWith(this.message, ...Array.from(this.payload)).should.equal(true);
return this.emit3.called.should.equal(false);
});
}); // duplicate client should be ignored
describe "with a designated room, and restricted clients, not restricted message", ->
beforeEach ->
@io.sockets =
describe("with a designated room, and restricted clients, not restricted message", function() {
beforeEach(function() {
this.io.sockets = {
clients: sinon.stub().returns([
{id: 'client-id-1', emit: @emit1 = sinon.stub(), ol_context: {}}
{id: 'client-id-2', emit: @emit2 = sinon.stub(), ol_context: {}}
{id: 'client-id-1', emit: @emit3 = sinon.stub(), ol_context: {}} # duplicate client
{id: 'client-id-4', emit: @emit4 = sinon.stub(), ol_context: {is_restricted_user: true}}
{id: 'client-id-1', emit: (this.emit1 = sinon.stub()), ol_context: {}},
{id: 'client-id-2', emit: (this.emit2 = sinon.stub()), ol_context: {}},
{id: 'client-id-1', emit: (this.emit3 = sinon.stub()), ol_context: {}}, // duplicate client
{id: 'client-id-4', emit: (this.emit4 = sinon.stub()), ol_context: {is_restricted_user: true}}
])
data = JSON.stringify
room_id: @room_id
message: @message
payload: @payload
@WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", data)
};
const data = JSON.stringify({
room_id: this.room_id,
message: this.message,
payload: this.payload
});
return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data);
});
it "should send the message to all (unique) clients in the room", ->
@io.sockets.clients
.calledWith(@room_id)
.should.equal true
@emit1.calledWith(@message, @payload...).should.equal true
@emit2.calledWith(@message, @payload...).should.equal true
@emit3.called.should.equal false # duplicate client should be ignored
@emit4.called.should.equal true # restricted client, but should be called
return it("should send the message to all (unique) clients in the room", function() {
this.io.sockets.clients
.calledWith(this.room_id)
.should.equal(true);
this.emit1.calledWith(this.message, ...Array.from(this.payload)).should.equal(true);
this.emit2.calledWith(this.message, ...Array.from(this.payload)).should.equal(true);
this.emit3.called.should.equal(false); // duplicate client should be ignored
return this.emit4.called.should.equal(true);
});
}); // restricted client, but should be called
describe "with a designated room, and restricted clients, restricted message", ->
beforeEach ->
@io.sockets =
describe("with a designated room, and restricted clients, restricted message", function() {
beforeEach(function() {
this.io.sockets = {
clients: sinon.stub().returns([
{id: 'client-id-1', emit: @emit1 = sinon.stub(), ol_context: {}}
{id: 'client-id-2', emit: @emit2 = sinon.stub(), ol_context: {}}
{id: 'client-id-1', emit: @emit3 = sinon.stub(), ol_context: {}} # duplicate client
{id: 'client-id-4', emit: @emit4 = sinon.stub(), ol_context: {is_restricted_user: true}}
{id: 'client-id-1', emit: (this.emit1 = sinon.stub()), ol_context: {}},
{id: 'client-id-2', emit: (this.emit2 = sinon.stub()), ol_context: {}},
{id: 'client-id-1', emit: (this.emit3 = sinon.stub()), ol_context: {}}, // duplicate client
{id: 'client-id-4', emit: (this.emit4 = sinon.stub()), ol_context: {is_restricted_user: true}}
])
data = JSON.stringify
room_id: @room_id
message: @restrictedMessage = 'new-comment'
payload: @payload
@WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", data)
};
const data = JSON.stringify({
room_id: this.room_id,
message: (this.restrictedMessage = 'new-comment'),
payload: this.payload
});
return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data);
});
it "should send the message to all (unique) clients in the room, who are not restricted", ->
@io.sockets.clients
.calledWith(@room_id)
.should.equal true
@emit1.calledWith(@restrictedMessage, @payload...).should.equal true
@emit2.calledWith(@restrictedMessage, @payload...).should.equal true
@emit3.called.should.equal false # duplicate client should be ignored
@emit4.called.should.equal false # restricted client, should not be called
return it("should send the message to all (unique) clients in the room, who are not restricted", function() {
this.io.sockets.clients
.calledWith(this.room_id)
.should.equal(true);
this.emit1.calledWith(this.restrictedMessage, ...Array.from(this.payload)).should.equal(true);
this.emit2.calledWith(this.restrictedMessage, ...Array.from(this.payload)).should.equal(true);
this.emit3.called.should.equal(false); // duplicate client should be ignored
return this.emit4.called.should.equal(false);
});
}); // restricted client, should not be called
describe "when emitting to all", ->
beforeEach ->
@io.sockets =
emit: @emit = sinon.stub()
data = JSON.stringify
room_id: "all"
message: @message
payload: @payload
@WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", data)
return describe("when emitting to all", function() {
beforeEach(function() {
this.io.sockets =
{emit: (this.emit = sinon.stub())};
const data = JSON.stringify({
room_id: "all",
message: this.message,
payload: this.payload
});
return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data);
});
it "should send the message to all clients", ->
@emit.calledWith(@message, @payload...).should.equal true
return it("should send the message to all clients", function() {
return this.emit.calledWith(this.message, ...Array.from(this.payload)).should.equal(true);
});
});
});
});

View file

@ -1,13 +1,16 @@
sinon = require('sinon')
let MockClient;
const sinon = require('sinon');
idCounter = 0
let idCounter = 0;
module.exports = class MockClient
constructor: () ->
@ol_context = {}
@join = sinon.stub()
@emit = sinon.stub()
@disconnect = sinon.stub()
@id = idCounter++
@publicId = idCounter++
disconnect: () ->
module.exports = (MockClient = class MockClient {
constructor() {
this.ol_context = {};
this.join = sinon.stub();
this.emit = sinon.stub();
this.disconnect = sinon.stub();
this.id = idCounter++;
this.publicId = idCounter++;
}
disconnect() {}
});