mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
537e97be73
See the docs of OError.tag: https://github.com/overleaf/o-error#long-stack-traces-with-oerrortag (currently at 221dd902e7bfa0ee92de1ea5a3cbf3152c3ceeb4) I am tagging all errors at each async hop. Most of the controller code will only ever see already tagged errors -- or new errors created in our app code. They should have enough info that we do not need to tag them again.
1667 lines
52 KiB
JavaScript
1667 lines
52 KiB
JavaScript
/* eslint-disable
|
|
camelcase,
|
|
no-return-assign,
|
|
no-throw-literal,
|
|
no-unused-vars,
|
|
*/
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
// Fix any style issues and re-enable lint.
|
|
/*
|
|
* 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 chai = require('chai')
|
|
const should = chai.should()
|
|
const sinon = require('sinon')
|
|
const { expect } = chai
|
|
const modulePath = '../../../app/js/WebsocketController.js'
|
|
const SandboxedModule = require('sandboxed-module')
|
|
const tk = require('timekeeper')
|
|
|
|
describe('WebsocketController', function () {
|
|
beforeEach(function () {
|
|
tk.freeze(new Date())
|
|
this.project_id = 'project-id-123'
|
|
this.user = {
|
|
_id: (this.user_id = 'user-id-123'),
|
|
first_name: 'James',
|
|
last_name: 'Allen',
|
|
email: 'james@example.com',
|
|
signUpDate: new Date('2014-01-01'),
|
|
loginCount: 42
|
|
}
|
|
this.callback = sinon.stub()
|
|
this.client = {
|
|
disconnected: false,
|
|
id: (this.client_id = 'mock-client-id-123'),
|
|
publicId: `other-id-${Math.random()}`,
|
|
ol_context: {},
|
|
joinLeaveEpoch: 0,
|
|
join: sinon.stub(),
|
|
leave: sinon.stub()
|
|
}
|
|
return (this.WebsocketController = SandboxedModule.require(modulePath, {
|
|
requires: {
|
|
'./WebApiManager': (this.WebApiManager = {}),
|
|
'./AuthorizationManager': (this.AuthorizationManager = {}),
|
|
'./DocumentUpdaterManager': (this.DocumentUpdaterManager = {}),
|
|
'./ConnectedUsersManager': (this.ConnectedUsersManager = {}),
|
|
'./WebsocketLoadBalancer': (this.WebsocketLoadBalancer = {}),
|
|
'logger-sharelatex': (this.logger = {
|
|
log: sinon.stub(),
|
|
error: sinon.stub(),
|
|
warn: sinon.stub()
|
|
}),
|
|
'metrics-sharelatex': (this.metrics = {
|
|
inc: sinon.stub(),
|
|
set: sinon.stub()
|
|
}),
|
|
'./RoomManager': (this.RoomManager = {})
|
|
}
|
|
}))
|
|
})
|
|
|
|
afterEach(function () {
|
|
return tk.reset()
|
|
})
|
|
|
|
describe('joinProject', function () {
|
|
describe('when authorised', function () {
|
|
beforeEach(function () {
|
|
this.client.id = 'mock-client-id'
|
|
this.project = {
|
|
name: 'Test Project',
|
|
owner: {
|
|
_id: (this.owner_id = 'mock-owner-id-123')
|
|
}
|
|
}
|
|
this.privilegeLevel = 'owner'
|
|
this.ConnectedUsersManager.updateUserPosition = sinon
|
|
.stub()
|
|
.callsArgAsync(4)
|
|
this.isRestrictedUser = true
|
|
this.WebApiManager.joinProject = sinon
|
|
.stub()
|
|
.callsArgWith(
|
|
2,
|
|
null,
|
|
this.project,
|
|
this.privilegeLevel,
|
|
this.isRestrictedUser
|
|
)
|
|
this.RoomManager.joinProject = sinon.stub().callsArg(2)
|
|
return this.WebsocketController.joinProject(
|
|
this.client,
|
|
this.user,
|
|
this.project_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should load the project from web', function () {
|
|
return this.WebApiManager.joinProject
|
|
.calledWith(this.project_id, this.user)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should join the project room', function () {
|
|
return this.RoomManager.joinProject
|
|
.calledWith(this.client, this.project_id)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should set the privilege level on the client', function () {
|
|
return this.client.ol_context.privilege_level.should.equal(
|
|
this.privilegeLevel
|
|
)
|
|
})
|
|
it("should set the user's id on the client", function () {
|
|
return this.client.ol_context.user_id.should.equal(this.user._id)
|
|
})
|
|
it("should set the user's email on the client", function () {
|
|
return this.client.ol_context.email.should.equal(this.user.email)
|
|
})
|
|
it("should set the user's first_name on the client", function () {
|
|
return this.client.ol_context.first_name.should.equal(
|
|
this.user.first_name
|
|
)
|
|
})
|
|
it("should set the user's last_name on the client", function () {
|
|
return this.client.ol_context.last_name.should.equal(
|
|
this.user.last_name
|
|
)
|
|
})
|
|
it("should set the user's sign up date on the client", function () {
|
|
return this.client.ol_context.signup_date.should.equal(
|
|
this.user.signUpDate
|
|
)
|
|
})
|
|
it("should set the user's login_count on the client", function () {
|
|
return this.client.ol_context.login_count.should.equal(
|
|
this.user.loginCount
|
|
)
|
|
})
|
|
it('should set the connected time on the client', function () {
|
|
return this.client.ol_context.connected_time.should.equal(new Date())
|
|
})
|
|
it('should set the project_id on the client', function () {
|
|
return this.client.ol_context.project_id.should.equal(this.project_id)
|
|
})
|
|
it('should set the project owner id on the client', function () {
|
|
return this.client.ol_context.owner_id.should.equal(this.owner_id)
|
|
})
|
|
it('should set the is_restricted_user flag on the client', function () {
|
|
return this.client.ol_context.is_restricted_user.should.equal(
|
|
this.isRestrictedUser
|
|
)
|
|
})
|
|
it('should call the callback with the project, privilegeLevel and protocolVersion', function () {
|
|
return this.callback
|
|
.calledWith(
|
|
null,
|
|
this.project,
|
|
this.privilegeLevel,
|
|
this.WebsocketController.PROTOCOL_VERSION
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should mark the user as connected in ConnectedUsersManager', function () {
|
|
return this.ConnectedUsersManager.updateUserPosition
|
|
.calledWith(this.project_id, this.client.publicId, this.user, null)
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should increment the join-project metric', function () {
|
|
return this.metrics.inc
|
|
.calledWith('editor.join-project')
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when not authorized', function () {
|
|
beforeEach(function () {
|
|
this.WebApiManager.joinProject = sinon
|
|
.stub()
|
|
.callsArgWith(2, null, null, null)
|
|
return this.WebsocketController.joinProject(
|
|
this.client,
|
|
this.user,
|
|
this.project_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an error', function () {
|
|
return this.callback
|
|
.calledWith(sinon.match({ message: 'not authorized' }))
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should not log an error', function () {
|
|
return this.logger.error.called.should.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('when the subscribe failed', function () {
|
|
beforeEach(function () {
|
|
this.client.id = 'mock-client-id'
|
|
this.project = {
|
|
name: 'Test Project',
|
|
owner: {
|
|
_id: (this.owner_id = 'mock-owner-id-123')
|
|
}
|
|
}
|
|
this.privilegeLevel = 'owner'
|
|
this.ConnectedUsersManager.updateUserPosition = sinon
|
|
.stub()
|
|
.callsArgAsync(4)
|
|
this.isRestrictedUser = true
|
|
this.WebApiManager.joinProject = sinon
|
|
.stub()
|
|
.callsArgWith(
|
|
2,
|
|
null,
|
|
this.project,
|
|
this.privilegeLevel,
|
|
this.isRestrictedUser
|
|
)
|
|
this.RoomManager.joinProject = sinon
|
|
.stub()
|
|
.callsArgWith(2, new Error('subscribe failed'))
|
|
return this.WebsocketController.joinProject(
|
|
this.client,
|
|
this.user,
|
|
this.project_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
return it('should return an error', function () {
|
|
this.callback
|
|
.calledWith(sinon.match({ message: 'subscribe failed' }))
|
|
.should.equal(true)
|
|
return this.callback.args[0][0].message.should.equal('subscribe failed')
|
|
})
|
|
})
|
|
|
|
describe('when the client has disconnected', function () {
|
|
beforeEach(function () {
|
|
this.client.disconnected = true
|
|
this.WebApiManager.joinProject = sinon.stub().callsArg(2)
|
|
return this.WebsocketController.joinProject(
|
|
this.client,
|
|
this.user,
|
|
this.project_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should not call WebApiManager.joinProject', function () {
|
|
return expect(this.WebApiManager.joinProject.called).to.equal(false)
|
|
})
|
|
|
|
it('should call the callback with no details', function () {
|
|
return expect(this.callback.args[0]).to.deep.equal([])
|
|
})
|
|
|
|
return it('should increment the editor.join-project.disconnected metric with a status', function () {
|
|
return expect(
|
|
this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {
|
|
status: 'immediately'
|
|
})
|
|
).to.equal(true)
|
|
})
|
|
})
|
|
|
|
return describe('when the client disconnects while WebApiManager.joinProject is running', function () {
|
|
beforeEach(function () {
|
|
this.WebApiManager.joinProject = (project, user, cb) => {
|
|
this.client.disconnected = true
|
|
return cb(
|
|
null,
|
|
this.project,
|
|
this.privilegeLevel,
|
|
this.isRestrictedUser
|
|
)
|
|
}
|
|
|
|
return this.WebsocketController.joinProject(
|
|
this.client,
|
|
this.user,
|
|
this.project_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should call the callback with no details', function () {
|
|
return expect(this.callback.args[0]).to.deep.equal([])
|
|
})
|
|
|
|
return it('should increment the editor.join-project.disconnected metric with a status', function () {
|
|
return expect(
|
|
this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {
|
|
status: 'after-web-api-call'
|
|
})
|
|
).to.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('leaveProject', function () {
|
|
beforeEach(function () {
|
|
this.DocumentUpdaterManager.flushProjectToMongoAndDelete = sinon
|
|
.stub()
|
|
.callsArg(1)
|
|
this.ConnectedUsersManager.markUserAsDisconnected = sinon
|
|
.stub()
|
|
.callsArg(2)
|
|
this.WebsocketLoadBalancer.emitToRoom = sinon.stub()
|
|
this.RoomManager.leaveProjectAndDocs = sinon.stub()
|
|
this.clientsInRoom = []
|
|
this.io = {
|
|
sockets: {
|
|
clients: (room_id) => {
|
|
if (room_id !== this.project_id) {
|
|
throw 'expected room_id to be project_id'
|
|
}
|
|
return this.clientsInRoom
|
|
}
|
|
}
|
|
}
|
|
this.client.ol_context.project_id = this.project_id
|
|
this.client.ol_context.user_id = this.user_id
|
|
this.WebsocketController.FLUSH_IF_EMPTY_DELAY = 0
|
|
return tk.reset()
|
|
}) // Allow setTimeout to work.
|
|
|
|
describe('when the client did not joined a project yet', function () {
|
|
beforeEach(function (done) {
|
|
this.client.ol_context = {}
|
|
return this.WebsocketController.leaveProject(this.io, this.client, done)
|
|
})
|
|
|
|
it('should bail out when calling leaveProject', function () {
|
|
this.WebsocketLoadBalancer.emitToRoom.called.should.equal(false)
|
|
this.RoomManager.leaveProjectAndDocs.called.should.equal(false)
|
|
return this.ConnectedUsersManager.markUserAsDisconnected.called.should.equal(
|
|
false
|
|
)
|
|
})
|
|
|
|
return it('should not inc any metric', function () {
|
|
return this.metrics.inc.called.should.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('when the project is empty', function () {
|
|
beforeEach(function (done) {
|
|
this.clientsInRoom = []
|
|
return this.WebsocketController.leaveProject(this.io, this.client, done)
|
|
})
|
|
|
|
it('should end clientTracking.clientDisconnected to the project room', function () {
|
|
return this.WebsocketLoadBalancer.emitToRoom
|
|
.calledWith(
|
|
this.project_id,
|
|
'clientTracking.clientDisconnected',
|
|
this.client.publicId
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should mark the user as disconnected', function () {
|
|
return this.ConnectedUsersManager.markUserAsDisconnected
|
|
.calledWith(this.project_id, this.client.publicId)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should flush the project in the document updater', function () {
|
|
return this.DocumentUpdaterManager.flushProjectToMongoAndDelete
|
|
.calledWith(this.project_id)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should increment the leave-project metric', function () {
|
|
return this.metrics.inc
|
|
.calledWith('editor.leave-project')
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should track the disconnection in RoomManager', function () {
|
|
return this.RoomManager.leaveProjectAndDocs
|
|
.calledWith(this.client)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the project is not empty', function () {
|
|
beforeEach(function (done) {
|
|
this.clientsInRoom = ['mock-remaining-client']
|
|
this.io = {
|
|
sockets: {
|
|
clients: (room_id) => {
|
|
if (room_id !== this.project_id) {
|
|
throw 'expected room_id to be project_id'
|
|
}
|
|
return this.clientsInRoom
|
|
}
|
|
}
|
|
}
|
|
return this.WebsocketController.leaveProject(this.io, this.client, done)
|
|
})
|
|
|
|
return it('should not flush the project in the document updater', function () {
|
|
return this.DocumentUpdaterManager.flushProjectToMongoAndDelete.called.should.equal(
|
|
false
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when client has not authenticated', function () {
|
|
beforeEach(function (done) {
|
|
this.client.ol_context.user_id = null
|
|
this.client.ol_context.project_id = null
|
|
return this.WebsocketController.leaveProject(this.io, this.client, done)
|
|
})
|
|
|
|
it('should not end clientTracking.clientDisconnected to the project room', function () {
|
|
return this.WebsocketLoadBalancer.emitToRoom
|
|
.calledWith(
|
|
this.project_id,
|
|
'clientTracking.clientDisconnected',
|
|
this.client.publicId
|
|
)
|
|
.should.equal(false)
|
|
})
|
|
|
|
it('should not mark the user as disconnected', function () {
|
|
return this.ConnectedUsersManager.markUserAsDisconnected
|
|
.calledWith(this.project_id, this.client.publicId)
|
|
.should.equal(false)
|
|
})
|
|
|
|
it('should not flush the project in the document updater', function () {
|
|
return this.DocumentUpdaterManager.flushProjectToMongoAndDelete
|
|
.calledWith(this.project_id)
|
|
.should.equal(false)
|
|
})
|
|
|
|
return it('should not increment the leave-project metric', function () {
|
|
return this.metrics.inc
|
|
.calledWith('editor.leave-project')
|
|
.should.equal(false)
|
|
})
|
|
})
|
|
|
|
return describe('when client has not joined a project', function () {
|
|
beforeEach(function (done) {
|
|
this.client.ol_context.user_id = this.user_id
|
|
this.client.ol_context.project_id = null
|
|
return this.WebsocketController.leaveProject(this.io, this.client, done)
|
|
})
|
|
|
|
it('should not end clientTracking.clientDisconnected to the project room', function () {
|
|
return this.WebsocketLoadBalancer.emitToRoom
|
|
.calledWith(
|
|
this.project_id,
|
|
'clientTracking.clientDisconnected',
|
|
this.client.publicId
|
|
)
|
|
.should.equal(false)
|
|
})
|
|
|
|
it('should not mark the user as disconnected', function () {
|
|
return this.ConnectedUsersManager.markUserAsDisconnected
|
|
.calledWith(this.project_id, this.client.publicId)
|
|
.should.equal(false)
|
|
})
|
|
|
|
it('should not flush the project in the document updater', function () {
|
|
return this.DocumentUpdaterManager.flushProjectToMongoAndDelete
|
|
.calledWith(this.project_id)
|
|
.should.equal(false)
|
|
})
|
|
|
|
return it('should not increment the leave-project metric', function () {
|
|
return this.metrics.inc
|
|
.calledWith('editor.leave-project')
|
|
.should.equal(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('joinDoc', function () {
|
|
beforeEach(function () {
|
|
this.doc_id = 'doc-id-123'
|
|
this.doc_lines = ['doc', 'lines']
|
|
this.version = 42
|
|
this.ops = ['mock', 'ops']
|
|
this.ranges = { mock: 'ranges' }
|
|
this.options = {}
|
|
|
|
this.client.ol_context.project_id = this.project_id
|
|
this.client.ol_context.is_restricted_user = false
|
|
this.AuthorizationManager.addAccessToDoc = sinon.stub().yields()
|
|
this.AuthorizationManager.assertClientCanViewProject = sinon
|
|
.stub()
|
|
.callsArgWith(1, null)
|
|
this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon
|
|
.stub()
|
|
.callsArgWith(2, null)
|
|
this.DocumentUpdaterManager.getDocument = sinon
|
|
.stub()
|
|
.callsArgWith(
|
|
3,
|
|
null,
|
|
this.doc_lines,
|
|
this.version,
|
|
this.ranges,
|
|
this.ops
|
|
)
|
|
return (this.RoomManager.joinDoc = sinon.stub().callsArg(2))
|
|
})
|
|
|
|
describe('works', function () {
|
|
beforeEach(function () {
|
|
return this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
this.options,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should inc the joinLeaveEpoch', function () {
|
|
expect(this.client.joinLeaveEpoch).to.equal(1)
|
|
})
|
|
|
|
it('should check that the client is authorized to view the project', function () {
|
|
return this.AuthorizationManager.assertClientCanViewProject
|
|
.calledWith(this.client)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the document from the DocumentUpdaterManager with fromVersion', function () {
|
|
return this.DocumentUpdaterManager.getDocument
|
|
.calledWith(this.project_id, this.doc_id, -1)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should add permissions for the client to access the doc', function () {
|
|
return this.AuthorizationManager.addAccessToDoc
|
|
.calledWith(this.client, this.doc_id)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should join the client to room for the doc_id', function () {
|
|
return this.RoomManager.joinDoc
|
|
.calledWith(this.client, this.doc_id)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback with the lines, version, ranges and ops', function () {
|
|
return this.callback
|
|
.calledWith(null, this.doc_lines, this.version, this.ops, this.ranges)
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should increment the join-doc metric', function () {
|
|
return this.metrics.inc.calledWith('editor.join-doc').should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with a fromVersion', function () {
|
|
beforeEach(function () {
|
|
this.fromVersion = 40
|
|
return this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
this.fromVersion,
|
|
this.options,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
return it('should get the document from the DocumentUpdaterManager with fromVersion', function () {
|
|
return this.DocumentUpdaterManager.getDocument
|
|
.calledWith(this.project_id, this.doc_id, this.fromVersion)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with doclines that need escaping', function () {
|
|
beforeEach(function () {
|
|
this.doc_lines.push(['räksmörgås'])
|
|
return this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
this.options,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
return it('should call the callback with the escaped lines', function () {
|
|
const escaped_lines = this.callback.args[0][1]
|
|
const escaped_word = escaped_lines.pop()
|
|
escaped_word.should.equal('räksmörgås')
|
|
// Check that unescaping works
|
|
return decodeURIComponent(escape(escaped_word)).should.equal(
|
|
'räksmörgås'
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('with comments that need encoding', function () {
|
|
beforeEach(function () {
|
|
this.ranges.comments = [{ op: { c: 'räksmörgås' } }]
|
|
return this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
{ encodeRanges: true },
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
return it('should call the callback with the encoded comment', function () {
|
|
const encoded_comments = this.callback.args[0][4]
|
|
const encoded_comment = encoded_comments.comments.pop()
|
|
const encoded_comment_text = encoded_comment.op.c
|
|
return encoded_comment_text.should.equal('räksmörgås')
|
|
})
|
|
})
|
|
|
|
describe('with changes that need encoding', function () {
|
|
it('should call the callback with the encoded insert change', function () {
|
|
this.ranges.changes = [{ op: { i: 'räksmörgås' } }]
|
|
this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
{ encodeRanges: true },
|
|
this.callback
|
|
)
|
|
|
|
const encoded_changes = this.callback.args[0][4]
|
|
const encoded_change = encoded_changes.changes.pop()
|
|
const encoded_change_text = encoded_change.op.i
|
|
return encoded_change_text.should.equal('räksmörgås')
|
|
})
|
|
|
|
return it('should call the callback with the encoded delete change', function () {
|
|
this.ranges.changes = [{ op: { d: 'räksmörgås' } }]
|
|
this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
{ encodeRanges: true },
|
|
this.callback
|
|
)
|
|
|
|
const encoded_changes = this.callback.args[0][4]
|
|
const encoded_change = encoded_changes.changes.pop()
|
|
const encoded_change_text = encoded_change.op.d
|
|
return encoded_change_text.should.equal('räksmörgås')
|
|
})
|
|
})
|
|
|
|
describe('when not authorized', function () {
|
|
beforeEach(function () {
|
|
this.AuthorizationManager.assertClientCanViewProject = sinon
|
|
.stub()
|
|
.callsArgWith(1, (this.err = new Error('not authorized')))
|
|
return this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
this.options,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should call the callback with an error', function () {
|
|
return this.callback
|
|
.calledWith(sinon.match({ message: 'not authorized' }))
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should not call the DocumentUpdaterManager', function () {
|
|
return this.DocumentUpdaterManager.getDocument.called.should.equal(
|
|
false
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('with a restricted client', function () {
|
|
beforeEach(function () {
|
|
this.ranges.comments = [{ op: { a: 1 } }, { op: { a: 2 } }]
|
|
this.client.ol_context.is_restricted_user = true
|
|
return this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
this.options,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
return it('should overwrite ranges.comments with an empty list', function () {
|
|
const ranges = this.callback.args[0][4]
|
|
return expect(ranges.comments).to.deep.equal([])
|
|
})
|
|
})
|
|
|
|
describe('when the client has disconnected', function () {
|
|
beforeEach(function () {
|
|
this.client.disconnected = true
|
|
return this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
this.options,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should call the callback with no details', function () {
|
|
return expect(this.callback.args[0]).to.deep.equal([])
|
|
})
|
|
|
|
it('should increment the editor.join-doc.disconnected metric with a status', function () {
|
|
return expect(
|
|
this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
|
|
status: 'immediately'
|
|
})
|
|
).to.equal(true)
|
|
})
|
|
|
|
return it('should not get the document', function () {
|
|
return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(
|
|
false
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the client disconnects while auth checks are running', function () {
|
|
beforeEach(function (done) {
|
|
this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
|
|
new Error()
|
|
)
|
|
this.DocumentUpdaterManager.checkDocument = (
|
|
project_id,
|
|
doc_id,
|
|
cb
|
|
) => {
|
|
this.client.disconnected = true
|
|
cb()
|
|
}
|
|
|
|
this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
this.options,
|
|
(...args) => {
|
|
this.callback(...args)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should call the callback with no details', function () {
|
|
expect(this.callback.called).to.equal(true)
|
|
expect(this.callback.args[0]).to.deep.equal([])
|
|
})
|
|
|
|
it('should increment the editor.join-doc.disconnected metric with a status', function () {
|
|
expect(
|
|
this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
|
|
status: 'after-client-auth-check'
|
|
})
|
|
).to.equal(true)
|
|
})
|
|
|
|
it('should not get the document', function () {
|
|
expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('when the client starts a parallel joinDoc request', function () {
|
|
beforeEach(function (done) {
|
|
this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
|
|
new Error()
|
|
)
|
|
this.DocumentUpdaterManager.checkDocument = (
|
|
project_id,
|
|
doc_id,
|
|
cb
|
|
) => {
|
|
this.DocumentUpdaterManager.checkDocument = sinon.stub().yields()
|
|
this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
{},
|
|
() => {}
|
|
)
|
|
cb()
|
|
}
|
|
|
|
this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
this.options,
|
|
(...args) => {
|
|
this.callback(...args)
|
|
// make sure the other joinDoc request completed
|
|
setTimeout(done, 5)
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should call the callback with an error', function () {
|
|
expect(this.callback.called).to.equal(true)
|
|
expect(this.callback.args[0][0].message).to.equal(
|
|
'joinLeaveEpoch mismatch'
|
|
)
|
|
})
|
|
|
|
it('should get the document once (the parallel request wins)', function () {
|
|
expect(this.DocumentUpdaterManager.getDocument.callCount).to.equal(1)
|
|
})
|
|
})
|
|
|
|
describe('when the client starts a parallel leaveDoc request', function () {
|
|
beforeEach(function (done) {
|
|
this.RoomManager.leaveDoc = sinon.stub()
|
|
|
|
this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
|
|
new Error()
|
|
)
|
|
this.DocumentUpdaterManager.checkDocument = (
|
|
project_id,
|
|
doc_id,
|
|
cb
|
|
) => {
|
|
this.WebsocketController.leaveDoc(this.client, this.doc_id, () => {})
|
|
cb()
|
|
}
|
|
|
|
this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
this.options,
|
|
(...args) => {
|
|
this.callback(...args)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should call the callback with an error', function () {
|
|
expect(this.callback.called).to.equal(true)
|
|
expect(this.callback.args[0][0].message).to.equal(
|
|
'joinLeaveEpoch mismatch'
|
|
)
|
|
})
|
|
|
|
it('should not get the document', function () {
|
|
expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('when the client disconnects while RoomManager.joinDoc is running', function () {
|
|
beforeEach(function () {
|
|
this.RoomManager.joinDoc = (client, doc_id, cb) => {
|
|
this.client.disconnected = true
|
|
return cb()
|
|
}
|
|
|
|
return this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
this.options,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should call the callback with no details', function () {
|
|
return expect(this.callback.args[0]).to.deep.equal([])
|
|
})
|
|
|
|
it('should increment the editor.join-doc.disconnected metric with a status', function () {
|
|
return expect(
|
|
this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
|
|
status: 'after-joining-room'
|
|
})
|
|
).to.equal(true)
|
|
})
|
|
|
|
return it('should not get the document', function () {
|
|
return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(
|
|
false
|
|
)
|
|
})
|
|
})
|
|
|
|
return describe('when the client disconnects while DocumentUpdaterManager.getDocument is running', function () {
|
|
beforeEach(function () {
|
|
this.DocumentUpdaterManager.getDocument = (
|
|
project_id,
|
|
doc_id,
|
|
fromVersion,
|
|
callback
|
|
) => {
|
|
this.client.disconnected = true
|
|
return callback(
|
|
null,
|
|
this.doc_lines,
|
|
this.version,
|
|
this.ranges,
|
|
this.ops
|
|
)
|
|
}
|
|
|
|
return this.WebsocketController.joinDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
-1,
|
|
this.options,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should call the callback with no details', function () {
|
|
return expect(this.callback.args[0]).to.deep.equal([])
|
|
})
|
|
|
|
return it('should increment the editor.join-doc.disconnected metric with a status', function () {
|
|
return expect(
|
|
this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {
|
|
status: 'after-doc-updater-call'
|
|
})
|
|
).to.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('leaveDoc', function () {
|
|
beforeEach(function () {
|
|
this.doc_id = 'doc-id-123'
|
|
this.client.ol_context.project_id = this.project_id
|
|
this.RoomManager.leaveDoc = sinon.stub()
|
|
return this.WebsocketController.leaveDoc(
|
|
this.client,
|
|
this.doc_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should inc the joinLeaveEpoch', function () {
|
|
expect(this.client.joinLeaveEpoch).to.equal(1)
|
|
})
|
|
|
|
it('should remove the client from the doc_id room', function () {
|
|
return this.RoomManager.leaveDoc
|
|
.calledWith(this.client, this.doc_id)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback', function () {
|
|
return this.callback.called.should.equal(true)
|
|
})
|
|
|
|
return it('should increment the leave-doc metric', function () {
|
|
return this.metrics.inc.calledWith('editor.leave-doc').should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('getConnectedUsers', function () {
|
|
beforeEach(function () {
|
|
this.client.ol_context.project_id = this.project_id
|
|
this.users = ['mock', 'users']
|
|
this.WebsocketLoadBalancer.emitToRoom = sinon.stub()
|
|
return (this.ConnectedUsersManager.getConnectedUsers = sinon
|
|
.stub()
|
|
.callsArgWith(1, null, this.users))
|
|
})
|
|
|
|
describe('when authorized', function () {
|
|
beforeEach(function (done) {
|
|
this.AuthorizationManager.assertClientCanViewProject = sinon
|
|
.stub()
|
|
.callsArgWith(1, null)
|
|
return this.WebsocketController.getConnectedUsers(
|
|
this.client,
|
|
(...args) => {
|
|
this.callback(...Array.from(args || []))
|
|
return done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should check that the client is authorized to view the project', function () {
|
|
return this.AuthorizationManager.assertClientCanViewProject
|
|
.calledWith(this.client)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should broadcast a request to update the client list', function () {
|
|
return this.WebsocketLoadBalancer.emitToRoom
|
|
.calledWith(this.project_id, 'clientTracking.refresh')
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should get the connected users for the project', function () {
|
|
return this.ConnectedUsersManager.getConnectedUsers
|
|
.calledWith(this.project_id)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should return the users', function () {
|
|
return this.callback.calledWith(null, this.users).should.equal(true)
|
|
})
|
|
|
|
return it('should increment the get-connected-users metric', function () {
|
|
return this.metrics.inc
|
|
.calledWith('editor.get-connected-users')
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when not authorized', function () {
|
|
beforeEach(function () {
|
|
this.AuthorizationManager.assertClientCanViewProject = sinon
|
|
.stub()
|
|
.callsArgWith(1, (this.err = new Error('not authorized')))
|
|
return this.WebsocketController.getConnectedUsers(
|
|
this.client,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should not get the connected users for the project', function () {
|
|
return this.ConnectedUsersManager.getConnectedUsers.called.should.equal(
|
|
false
|
|
)
|
|
})
|
|
|
|
return it('should return an error', function () {
|
|
return this.callback.calledWith(this.err).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when restricted user', function () {
|
|
beforeEach(function () {
|
|
this.client.ol_context.is_restricted_user = true
|
|
this.AuthorizationManager.assertClientCanViewProject = sinon
|
|
.stub()
|
|
.callsArgWith(1, null)
|
|
return this.WebsocketController.getConnectedUsers(
|
|
this.client,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an empty array of users', function () {
|
|
return this.callback.calledWith(null, []).should.equal(true)
|
|
})
|
|
|
|
return it('should not get the connected users for the project', function () {
|
|
return this.ConnectedUsersManager.getConnectedUsers.called.should.equal(
|
|
false
|
|
)
|
|
})
|
|
})
|
|
|
|
return describe('when the client has disconnected', function () {
|
|
beforeEach(function () {
|
|
this.client.disconnected = true
|
|
this.AuthorizationManager.assertClientCanViewProject = sinon.stub()
|
|
return this.WebsocketController.getConnectedUsers(
|
|
this.client,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should call the callback with no details', function () {
|
|
return expect(this.callback.args[0]).to.deep.equal([])
|
|
})
|
|
|
|
return it('should not check permissions', function () {
|
|
return expect(
|
|
this.AuthorizationManager.assertClientCanViewProject.called
|
|
).to.equal(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('updateClientPosition', function () {
|
|
beforeEach(function () {
|
|
this.WebsocketLoadBalancer.emitToRoom = sinon.stub()
|
|
this.ConnectedUsersManager.updateUserPosition = sinon
|
|
.stub()
|
|
.callsArgAsync(4)
|
|
this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon
|
|
.stub()
|
|
.callsArgWith(2, null)
|
|
return (this.update = {
|
|
doc_id: (this.doc_id = 'doc-id-123'),
|
|
row: (this.row = 42),
|
|
column: (this.column = 37)
|
|
})
|
|
})
|
|
|
|
describe('with a logged in user', function () {
|
|
beforeEach(function (done) {
|
|
this.client.ol_context = {
|
|
project_id: this.project_id,
|
|
first_name: (this.first_name = 'Douglas'),
|
|
last_name: (this.last_name = 'Adams'),
|
|
email: (this.email = 'joe@example.com'),
|
|
user_id: (this.user_id = 'user-id-123')
|
|
}
|
|
|
|
this.populatedCursorData = {
|
|
doc_id: this.doc_id,
|
|
id: this.client.publicId,
|
|
name: `${this.first_name} ${this.last_name}`,
|
|
row: this.row,
|
|
column: this.column,
|
|
email: this.email,
|
|
user_id: this.user_id
|
|
}
|
|
this.WebsocketController.updateClientPosition(
|
|
this.client,
|
|
this.update,
|
|
done
|
|
)
|
|
})
|
|
|
|
it("should send the update to the project room with the user's name", function () {
|
|
return this.WebsocketLoadBalancer.emitToRoom
|
|
.calledWith(
|
|
this.project_id,
|
|
'clientTracking.clientUpdated',
|
|
this.populatedCursorData
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should send the cursor data to the connected user manager', function (done) {
|
|
this.ConnectedUsersManager.updateUserPosition
|
|
.calledWith(
|
|
this.project_id,
|
|
this.client.publicId,
|
|
{
|
|
_id: this.user_id,
|
|
email: this.email,
|
|
first_name: this.first_name,
|
|
last_name: this.last_name
|
|
},
|
|
{
|
|
row: this.row,
|
|
column: this.column,
|
|
doc_id: this.doc_id
|
|
}
|
|
)
|
|
.should.equal(true)
|
|
return done()
|
|
})
|
|
|
|
return it('should increment the update-client-position metric at 0.1 frequency', function () {
|
|
return this.metrics.inc
|
|
.calledWith('editor.update-client-position', 0.1)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with a logged in user who has no last_name set', function () {
|
|
beforeEach(function (done) {
|
|
this.client.ol_context = {
|
|
project_id: this.project_id,
|
|
first_name: (this.first_name = 'Douglas'),
|
|
last_name: undefined,
|
|
email: (this.email = 'joe@example.com'),
|
|
user_id: (this.user_id = 'user-id-123')
|
|
}
|
|
|
|
this.populatedCursorData = {
|
|
doc_id: this.doc_id,
|
|
id: this.client.publicId,
|
|
name: `${this.first_name}`,
|
|
row: this.row,
|
|
column: this.column,
|
|
email: this.email,
|
|
user_id: this.user_id
|
|
}
|
|
this.WebsocketController.updateClientPosition(
|
|
this.client,
|
|
this.update,
|
|
done
|
|
)
|
|
})
|
|
|
|
it("should send the update to the project room with the user's name", function () {
|
|
return this.WebsocketLoadBalancer.emitToRoom
|
|
.calledWith(
|
|
this.project_id,
|
|
'clientTracking.clientUpdated',
|
|
this.populatedCursorData
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should send the cursor data to the connected user manager', function (done) {
|
|
this.ConnectedUsersManager.updateUserPosition
|
|
.calledWith(
|
|
this.project_id,
|
|
this.client.publicId,
|
|
{
|
|
_id: this.user_id,
|
|
email: this.email,
|
|
first_name: this.first_name,
|
|
last_name: undefined
|
|
},
|
|
{
|
|
row: this.row,
|
|
column: this.column,
|
|
doc_id: this.doc_id
|
|
}
|
|
)
|
|
.should.equal(true)
|
|
return done()
|
|
})
|
|
|
|
return it('should increment the update-client-position metric at 0.1 frequency', function () {
|
|
return this.metrics.inc
|
|
.calledWith('editor.update-client-position', 0.1)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with a logged in user who has no first_name set', function () {
|
|
beforeEach(function (done) {
|
|
this.client.ol_context = {
|
|
project_id: this.project_id,
|
|
first_name: undefined,
|
|
last_name: (this.last_name = 'Adams'),
|
|
email: (this.email = 'joe@example.com'),
|
|
user_id: (this.user_id = 'user-id-123')
|
|
}
|
|
|
|
this.populatedCursorData = {
|
|
doc_id: this.doc_id,
|
|
id: this.client.publicId,
|
|
name: `${this.last_name}`,
|
|
row: this.row,
|
|
column: this.column,
|
|
email: this.email,
|
|
user_id: this.user_id
|
|
}
|
|
this.WebsocketController.updateClientPosition(
|
|
this.client,
|
|
this.update,
|
|
done
|
|
)
|
|
})
|
|
|
|
it("should send the update to the project room with the user's name", function () {
|
|
return this.WebsocketLoadBalancer.emitToRoom
|
|
.calledWith(
|
|
this.project_id,
|
|
'clientTracking.clientUpdated',
|
|
this.populatedCursorData
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should send the cursor data to the connected user manager', function (done) {
|
|
this.ConnectedUsersManager.updateUserPosition
|
|
.calledWith(
|
|
this.project_id,
|
|
this.client.publicId,
|
|
{
|
|
_id: this.user_id,
|
|
email: this.email,
|
|
first_name: undefined,
|
|
last_name: this.last_name
|
|
},
|
|
{
|
|
row: this.row,
|
|
column: this.column,
|
|
doc_id: this.doc_id
|
|
}
|
|
)
|
|
.should.equal(true)
|
|
return done()
|
|
})
|
|
|
|
return it('should increment the update-client-position metric at 0.1 frequency', function () {
|
|
return this.metrics.inc
|
|
.calledWith('editor.update-client-position', 0.1)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
describe('with a logged in user who has no names set', function () {
|
|
beforeEach(function (done) {
|
|
this.client.ol_context = {
|
|
project_id: this.project_id,
|
|
first_name: undefined,
|
|
last_name: undefined,
|
|
email: (this.email = 'joe@example.com'),
|
|
user_id: (this.user_id = 'user-id-123')
|
|
}
|
|
return this.WebsocketController.updateClientPosition(
|
|
this.client,
|
|
this.update,
|
|
done
|
|
)
|
|
})
|
|
|
|
return it('should send the update to the project name with no name', function () {
|
|
return this.WebsocketLoadBalancer.emitToRoom
|
|
.calledWith(this.project_id, 'clientTracking.clientUpdated', {
|
|
doc_id: this.doc_id,
|
|
id: this.client.publicId,
|
|
user_id: this.user_id,
|
|
name: '',
|
|
row: this.row,
|
|
column: this.column,
|
|
email: this.email
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with an anonymous user', function () {
|
|
beforeEach(function (done) {
|
|
this.client.ol_context = {
|
|
project_id: this.project_id
|
|
}
|
|
return this.WebsocketController.updateClientPosition(
|
|
this.client,
|
|
this.update,
|
|
done
|
|
)
|
|
})
|
|
|
|
it('should send the update to the project room with no name', function () {
|
|
return this.WebsocketLoadBalancer.emitToRoom
|
|
.calledWith(this.project_id, 'clientTracking.clientUpdated', {
|
|
doc_id: this.doc_id,
|
|
id: this.client.publicId,
|
|
name: '',
|
|
row: this.row,
|
|
column: this.column
|
|
})
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should not send cursor data to the connected user manager', function (done) {
|
|
this.ConnectedUsersManager.updateUserPosition.called.should.equal(false)
|
|
return done()
|
|
})
|
|
})
|
|
|
|
return describe('when the client has disconnected', function () {
|
|
beforeEach(function (done) {
|
|
this.client.disconnected = true
|
|
this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub()
|
|
return this.WebsocketController.updateClientPosition(
|
|
this.client,
|
|
this.update,
|
|
(...args) => {
|
|
this.callback(...args)
|
|
done(args[0])
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should call the callback with no details', function () {
|
|
return expect(this.callback.args[0]).to.deep.equal([])
|
|
})
|
|
|
|
return it('should not check permissions', function () {
|
|
return expect(
|
|
this.AuthorizationManager.assertClientCanViewProjectAndDoc.called
|
|
).to.equal(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('applyOtUpdate', function () {
|
|
beforeEach(function () {
|
|
this.update = { op: { p: 12, t: 'foo' } }
|
|
this.client.ol_context.user_id = this.user_id
|
|
this.client.ol_context.project_id = this.project_id
|
|
this.WebsocketController._assertClientCanApplyUpdate = sinon
|
|
.stub()
|
|
.yields()
|
|
return (this.DocumentUpdaterManager.queueChange = sinon
|
|
.stub()
|
|
.callsArg(3))
|
|
})
|
|
|
|
describe('succesfully', function () {
|
|
beforeEach(function () {
|
|
return this.WebsocketController.applyOtUpdate(
|
|
this.client,
|
|
this.doc_id,
|
|
this.update,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should set the source of the update to the client id', function () {
|
|
return this.update.meta.source.should.equal(this.client.publicId)
|
|
})
|
|
|
|
it('should set the user_id of the update to the user id', function () {
|
|
return this.update.meta.user_id.should.equal(this.user_id)
|
|
})
|
|
|
|
it('should queue the update', function () {
|
|
return this.DocumentUpdaterManager.queueChange
|
|
.calledWith(this.project_id, this.doc_id, this.update)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should call the callback', function () {
|
|
return this.callback.called.should.equal(true)
|
|
})
|
|
|
|
return it('should increment the doc updates', function () {
|
|
return this.metrics.inc
|
|
.calledWith('editor.doc-update')
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('unsuccessfully', function () {
|
|
beforeEach(function () {
|
|
this.client.disconnect = sinon.stub()
|
|
this.DocumentUpdaterManager.queueChange = sinon
|
|
.stub()
|
|
.callsArgWith(3, (this.error = new Error('Something went wrong')))
|
|
return this.WebsocketController.applyOtUpdate(
|
|
this.client,
|
|
this.doc_id,
|
|
this.update,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should disconnect the client', function () {
|
|
return this.client.disconnect.called.should.equal(true)
|
|
})
|
|
|
|
it('should not log an error', function () {
|
|
return this.logger.error.called.should.equal(false)
|
|
})
|
|
|
|
return it('should call the callback with the error', function () {
|
|
return this.callback.calledWith(this.error).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when not authorized', function () {
|
|
beforeEach(function () {
|
|
this.client.disconnect = sinon.stub()
|
|
this.WebsocketController._assertClientCanApplyUpdate = sinon
|
|
.stub()
|
|
.yields((this.error = new Error('not authorized')))
|
|
return this.WebsocketController.applyOtUpdate(
|
|
this.client,
|
|
this.doc_id,
|
|
this.update,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
// This happens in a setTimeout to allow the client a chance to receive the error first.
|
|
// I'm not sure how to unit test, but it is acceptance tested.
|
|
// it "should disconnect the client", ->
|
|
// @client.disconnect.called.should.equal true
|
|
|
|
it('should log a warning', function () {
|
|
return this.logger.warn.called.should.equal(true)
|
|
})
|
|
|
|
return it('should call the callback with the error', function () {
|
|
return this.callback.calledWith(this.error).should.equal(true)
|
|
})
|
|
})
|
|
|
|
return describe('update_too_large', function () {
|
|
beforeEach(function (done) {
|
|
this.client.disconnect = sinon.stub()
|
|
this.client.emit = sinon.stub()
|
|
this.client.ol_context.user_id = this.user_id
|
|
this.client.ol_context.project_id = this.project_id
|
|
const error = new Error('update is too large')
|
|
error.updateSize = 7372835
|
|
this.DocumentUpdaterManager.queueChange = sinon
|
|
.stub()
|
|
.callsArgWith(3, error)
|
|
this.WebsocketController.applyOtUpdate(
|
|
this.client,
|
|
this.doc_id,
|
|
this.update,
|
|
this.callback
|
|
)
|
|
return setTimeout(() => done(), 1)
|
|
})
|
|
|
|
it('should call the callback with no error', function () {
|
|
this.callback.called.should.equal(true)
|
|
return this.callback.args[0].should.deep.equal([])
|
|
})
|
|
|
|
it('should log a warning with the size and context', function () {
|
|
this.logger.warn.called.should.equal(true)
|
|
return this.logger.warn.args[0].should.deep.equal([
|
|
{
|
|
user_id: this.user_id,
|
|
project_id: this.project_id,
|
|
doc_id: this.doc_id,
|
|
updateSize: 7372835
|
|
},
|
|
'update is too large'
|
|
])
|
|
})
|
|
|
|
describe('after 100ms', function () {
|
|
beforeEach(function (done) {
|
|
return setTimeout(done, 100)
|
|
})
|
|
|
|
it('should send an otUpdateError the client', function () {
|
|
return this.client.emit.calledWith('otUpdateError').should.equal(true)
|
|
})
|
|
|
|
return it('should disconnect the client', function () {
|
|
return this.client.disconnect.called.should.equal(true)
|
|
})
|
|
})
|
|
|
|
return describe('when the client disconnects during the next 100ms', function () {
|
|
beforeEach(function (done) {
|
|
this.client.disconnected = true
|
|
return setTimeout(done, 100)
|
|
})
|
|
|
|
it('should not send an otUpdateError the client', function () {
|
|
return this.client.emit
|
|
.calledWith('otUpdateError')
|
|
.should.equal(false)
|
|
})
|
|
|
|
it('should not disconnect the client', function () {
|
|
return this.client.disconnect.called.should.equal(false)
|
|
})
|
|
|
|
return it('should increment the editor.doc-update.disconnected metric with a status', function () {
|
|
return expect(
|
|
this.metrics.inc.calledWith('editor.doc-update.disconnected', 1, {
|
|
status: 'at-otUpdateError'
|
|
})
|
|
).to.equal(true)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
return describe('_assertClientCanApplyUpdate', function () {
|
|
beforeEach(function () {
|
|
this.edit_update = {
|
|
op: [
|
|
{ i: 'foo', p: 42 },
|
|
{ c: 'bar', p: 132 }
|
|
]
|
|
} // comments may still be in an edit op
|
|
this.comment_update = { op: [{ c: 'bar', p: 132 }] }
|
|
this.AuthorizationManager.assertClientCanEditProjectAndDoc = sinon.stub()
|
|
return (this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub())
|
|
})
|
|
|
|
describe('with a read-write client', function () {
|
|
return it('should return successfully', function (done) {
|
|
this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(null)
|
|
return this.WebsocketController._assertClientCanApplyUpdate(
|
|
this.client,
|
|
this.doc_id,
|
|
this.edit_update,
|
|
(error) => {
|
|
expect(error).to.be.null
|
|
return done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('with a read-only client and an edit op', function () {
|
|
return it('should return an error', function (done) {
|
|
this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(
|
|
new Error('not authorized')
|
|
)
|
|
this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null)
|
|
return this.WebsocketController._assertClientCanApplyUpdate(
|
|
this.client,
|
|
this.doc_id,
|
|
this.edit_update,
|
|
(error) => {
|
|
expect(error.message).to.equal('not authorized')
|
|
return done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('with a read-only client and a comment op', function () {
|
|
return it('should return successfully', function (done) {
|
|
this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(
|
|
new Error('not authorized')
|
|
)
|
|
this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null)
|
|
return this.WebsocketController._assertClientCanApplyUpdate(
|
|
this.client,
|
|
this.doc_id,
|
|
this.comment_update,
|
|
(error) => {
|
|
expect(error).to.be.null
|
|
return done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
return describe('with a totally unauthorized client', function () {
|
|
return it('should return an error', function (done) {
|
|
this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(
|
|
new Error('not authorized')
|
|
)
|
|
this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(
|
|
new Error('not authorized')
|
|
)
|
|
return this.WebsocketController._assertClientCanApplyUpdate(
|
|
this.client,
|
|
this.doc_id,
|
|
this.comment_update,
|
|
(error) => {
|
|
expect(error.message).to.equal('not authorized')
|
|
return done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|