Clean up unused real-time code in web

This commit is contained in:
James Allen 2015-02-05 16:37:37 +00:00
parent 3aad31069c
commit d7afb4e513
19 changed files with 106 additions and 1144 deletions

View file

@ -6,10 +6,8 @@ logger.logger.serializers.project = require("./app/js/infrastructure/LoggerSeria
metrics = require("metrics-sharelatex")
metrics.initialize("web")
Server = require("./app/js/infrastructure/Server")
BackgroundTasks = require("./app/js/infrastructure/BackgroundTasks")
Errors = require "./app/js/errors"
argv = require("optimist")
.options("user", {alias : "u", description : "Run the server with permissions of the specified user"})
.options("group", {alias : "g", description : "Run the server with permissions of the specified group"})
@ -32,8 +30,6 @@ if Settings.catchErrors
process.on "uncaughtException", (error) ->
logger.error err: error, "uncaughtException"
BackgroundTasks.run()
port = Settings.port or Settings.internal?.web?.port or 3000
host = Settings.internal.web.host or "localhost"
Server.server.listen port, host, ->

View file

@ -1,13 +0,0 @@
ConnectedUsersManager = require("./ConnectedUsersManager")
logger = require("logger-sharelatex")
module.exports =
getConnectedUsers: (req, res)->
project_id = req.params.Project_id
ConnectedUsersManager.getConnectedUsers project_id, (err, users)->
if err?
logger.err err:err, project_id:project_id, "problem getting connected users"
return res.send 500
res.send(users)

View file

@ -1,79 +0,0 @@
_ = require("underscore")
async = require("async")
Settings = require('settings-sharelatex')
logger = require("logger-sharelatex")
redis = require("redis-sharelatex")
rclient = redis.createClient(Settings.redis.web)
ONE_HOUR_IN_S = 60 * 60
ONE_DAY_IN_S = ONE_HOUR_IN_S * 24
FOUR_DAYS_IN_S = ONE_DAY_IN_S * 4
USER_TIMEOUT_IN_S = ONE_HOUR_IN_S / 4
buildProjectSetKey = (project_id)-> return "clients_in_project:#{project_id}"
buildUserKey = (project_id, client_id)-> return "connected_user:#{project_id}:#{client_id}"
module.exports =
# Use the same method for when a user connects, and when a user sends a cursor
# update. This way we don't care if the connected_user key has expired when
# we receive a cursor update.
updateUserPosition: (project_id, client_id, user, cursorData, callback = (err)->)->
logger.log project_id:project_id, client_id:client_id, "marking user as connected"
multi = rclient.multi()
multi.sadd buildProjectSetKey(project_id), client_id
multi.expire buildProjectSetKey(project_id), FOUR_DAYS_IN_S
multi.hset buildUserKey(project_id, client_id), "last_updated_at", Date.now()
multi.hset buildUserKey(project_id, client_id), "user_id", user._id
multi.hset buildUserKey(project_id, client_id), "first_name", user.first_name
multi.hset buildUserKey(project_id, client_id), "last_name", user.last_name
multi.hset buildUserKey(project_id, client_id), "email", user.email
if cursorData?
multi.hset buildUserKey(project_id, client_id), "cursorData", JSON.stringify(cursorData)
multi.expire buildUserKey(project_id, client_id), USER_TIMEOUT_IN_S
multi.exec (err)->
if err?
logger.err err:err, project_id:project_id, client_id:client_id, "problem marking user as connected"
callback(err)
markUserAsDisconnected: (project_id, client_id, callback)->
logger.log project_id:project_id, client_id:client_id, "marking user as disconnected"
multi = rclient.multi()
multi.srem buildProjectSetKey(project_id), client_id
multi.expire buildProjectSetKey(project_id), FOUR_DAYS_IN_S
multi.del buildUserKey(project_id, client_id)
multi.exec callback
_getConnectedUser: (project_id, client_id, callback)->
rclient.hgetall buildUserKey(project_id, client_id), (err, result)->
if !result?
result =
connected : false
client_id:client_id
else
result.connected = true
result.client_id = client_id
if result.cursorData?
result.cursorData = JSON.parse(result.cursorData)
callback err, result
getConnectedUsers: (project_id, callback)->
self = @
rclient.smembers buildProjectSetKey(project_id), (err, results)->
jobs = results.map (client_id)->
(cb)->
self._getConnectedUser(project_id, client_id, cb)
async.series jobs, (err, users)->
users = _.filter users, (user)->
user.connected
callback err, users

View file

@ -6,17 +6,13 @@ ProjectEntityHandler = require('../Project/ProjectEntityHandler')
ProjectOptionsHandler = require('../Project/ProjectOptionsHandler')
ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
ProjectDeleter = require("../Project/ProjectDeleter")
ProjectGetter = require('../Project/ProjectGetter')
UserGetter = require('../User/UserGetter')
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
LimitationsManager = require("../Subscription/LimitationsManager")
AuthorizationManager = require("../Security/AuthorizationManager")
EditorRealTimeController = require("./EditorRealTimeController")
TrackChangesManager = require("../TrackChanges/TrackChangesManager")
Settings = require('settings-sharelatex')
async = require('async')
ConnectedUsersManager = require("../ConnectedUsers/ConnectedUsersManager")
LockManager = require("../../infrastructure/LockManager")
_ = require('underscore')
redis = require("redis-sharelatex")
@ -24,144 +20,6 @@ rclientPub = redis.createClient(Settings.redis.web)
rclientSub = redis.createClient(Settings.redis.web)
module.exports = EditorController =
protocolVersion: 2
reportError: (client, clientError, callback = () ->) ->
client.get "project_id", (error, project_id) ->
client.get "user_id", (error, user_id) ->
logger.error err: clientError, project_id: project_id, user_id: user_id, "client error"
callback()
joinProject: (client, user, project_id, callback) ->
logger.log user_id:user._id, project_id:project_id, "user joining project"
Metrics.inc "editor.join-project"
EditorController.buildJoinProjectView project_id, user._id, (error, project, privilegeLevel, protocolVersion) ->
return callback(error) if error?
if !privilegeLevel
callback new Error("Not authorized")
else
client.join(project_id)
client.set("project_id", project_id)
client.set("owner_id", project.owner._id)
client.set("user_id", user._id)
client.set("first_name", user.first_name)
client.set("last_name", user.last_name)
client.set("email", user.email)
client.set("connected_time", new Date())
client.set("signup_date", user.signUpDate)
client.set("login_count", user.loginCount)
AuthorizationManager.setPrivilegeLevelOnClient client, privilegeLevel
callback null, project, privilegeLevel, EditorController.protocolVersion
# can be done after the connection has happened
ConnectedUsersManager.updateUserPosition project_id, client.id, user, null, ->
# Only show the 'renamed or deleted' message once
if project.deletedByExternalDataSource
ProjectDeleter.unmarkAsDeletedByExternalSource project_id
buildJoinProjectView: (project_id, user_id, callback = (error, project, privilegeLevel) ->) ->
ProjectGetter.getProjectWithoutDocLines project_id, (error, project) ->
return callback(error) if error?
return callback(new Error("not found")) if !project?
ProjectGetter.populateProjectWithUsers project, (error, project) ->
return callback(error) if error?
UserGetter.getUser user_id, { isAdmin: true }, (error, user) ->
return callback(error) if error?
AuthorizationManager.getPrivilegeLevelForProject project, user, (error, canAccess, privilegeLevel) ->
return callback(error) if error?
if !canAccess
callback null, null, false
else
callback(null,
ProjectEditorHandler.buildProjectModelView(project),
privilegeLevel
)
leaveProject: (client, user) ->
self = @
client.get "project_id", (error, project_id) ->
return if error? or !project_id?
EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientDisconnected", client.id)
ConnectedUsersManager.markUserAsDisconnected project_id, client.id, ->
logger.log user_id:user._id, project_id:project_id, "user leaving project"
self.flushProjectIfEmpty(project_id)
joinDoc: (client, project_id, doc_id, fromVersion, callback = (error, docLines, version) ->) ->
# fromVersion is optional
if typeof fromVersion == "function"
callback = fromVersion
fromVersion = -1
client.get "user_id", (error, user_id) ->
logger.log user_id: user_id, project_id: project_id, doc_id: doc_id, "user joining doc"
Metrics.inc "editor.join-doc"
client.join doc_id
DocumentUpdaterHandler.getDocument project_id, doc_id, fromVersion, (err, docLines, version, ops)->
# Encode any binary bits of data so it can go via WebSockets
# See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
if docLines?
docLines = for line in docLines
if line.text?
try
line.text = unescape(encodeURIComponent(line.text))
catch err
logger.err err:err, project_id:project_id, doc_id:doc_id, fromVersion:fromVersion, line:line, "error encoding line.text uri component"
else
try
line = unescape(encodeURIComponent(line))
catch err
logger.err err:err, project_id:project_id, doc_id:doc_id, fromVersion:fromVersion, line:line, "error encoding line uri component"
line
callback(err, docLines, version, ops)
leaveDoc: (client, project_id, doc_id, callback = (error) ->) ->
client.get "user_id", (error, user_id) ->
logger.log user_id: user_id, project_id: project_id, doc_id: doc_id, "user leaving doc"
Metrics.inc "editor.leave-doc"
client.leave doc_id
callback()
flushProjectIfEmpty: (project_id, callback = ->)->
setTimeout (->
io = require('../../infrastructure/Server').io
peopleStillInProject = io.sockets.clients(project_id).length
logger.log project_id: project_id, connectedCount: peopleStillInProject, "flushing if empty"
if peopleStillInProject == 0
DocumentUpdaterHandler.flushProjectToMongoAndDelete(project_id)
TrackChangesManager.flushProject(project_id)
callback()
), 500
updateClientPosition: (client, cursorData, callback = (error) ->) ->
async.parallel {
project_id: (cb)-> client.get "project_id", cb
first_name: (cb)-> client.get "first_name", cb
last_name: (cb)-> client.get "last_name", cb
email: (cb)-> client.get "email", cb
user_id: (cb)-> client.get "user_id", cb
}, (err, results)->
{first_name, last_name, user_id, email, project_id} = results
cursorData.id = client.id
cursorData.user_id = user_id if user_id?
cursorData.email = email if email?
if first_name? and last_name?
cursorData.name = first_name + " " + last_name
ConnectedUsersManager.updateUserPosition(project_id, client.id, {
first_name: first_name,
last_name: last_name,
email: email,
user_id: user_id
}, {
row: cursorData.row,
column: cursorData.column,
doc_id: cursorData.doc_id
}, ->)
else
cursorData.name = "Anonymous"
EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData)
addUserToProject: (project_id, email, privileges, callback = (error, collaborator_added)->)->
email = email.toLowerCase()
LimitationsManager.isCollaboratorLimitReached project_id, (error, limit_reached) =>

View file

@ -3,6 +3,10 @@ ProjectDeleter = require "../Project/ProjectDeleter"
logger = require "logger-sharelatex"
EditorRealTimeController = require "./EditorRealTimeController"
EditorController = require "./EditorController"
ProjectGetter = require('../Project/ProjectGetter')
UserGetter = require('../User/UserGetter')
AuthorizationManager = require("../Security/AuthorizationManager")
ProjectEditorHandler = require('../Project/ProjectEditorHandler')
Metrics = require('../../infrastructure/Metrics')
module.exports = EditorHttpController =
@ -11,7 +15,7 @@ module.exports = EditorHttpController =
user_id = req.query.user_id
logger.log {user_id, project_id}, "join project request"
Metrics.inc "editor.join-project"
EditorController.buildJoinProjectView project_id, user_id, (error, project, privilegeLevel) ->
EditorHttpController._buildJoinProjectView project_id, user_id, (error, project, privilegeLevel) ->
return next(error) if error?
res.json {
project: project
@ -21,6 +25,24 @@ module.exports = EditorHttpController =
if project?.deletedByExternalDataSource
ProjectDeleter.unmarkAsDeletedByExternalSource project_id
_buildJoinProjectView: (project_id, user_id, callback = (error, project, privilegeLevel) ->) ->
ProjectGetter.getProjectWithoutDocLines project_id, (error, project) ->
return callback(error) if error?
return callback(new Error("not found")) if !project?
ProjectGetter.populateProjectWithUsers project, (error, project) ->
return callback(error) if error?
UserGetter.getUser user_id, { isAdmin: true }, (error, user) ->
return callback(error) if error?
AuthorizationManager.getPrivilegeLevelForProject project, user, (error, canAccess, privilegeLevel) ->
return callback(error) if error?
if !canAccess
callback null, null, false
else
callback(null,
ProjectEditorHandler.buildProjectModelView(project),
privilegeLevel
)
restoreDoc: (req, res, next) ->
project_id = req.params.Project_id
doc_id = req.params.doc_id

View file

@ -16,15 +16,3 @@ module.exports = EditorRealTimeController =
emitToAll: (message, payload...) ->
@emitToRoom "all", message, payload...
listenForEditorEvents: () ->
@rclientSub.subscribe "editor-events"
@rclientSub.on "message", @_processEditorEvent.bind(@)
_processEditorEvent: (channel, message) ->
io = require('../../infrastructure/Server').io
message = JSON.parse(message)
if message.room_id == "all"
io.sockets.emit(message.message, message.payload...)
else
io.sockets.in(message.room_id).emit(message.message, message.payload...)

View file

@ -1,61 +0,0 @@
logger = require "logger-sharelatex"
metrics = require('../../infrastructure/Metrics')
Settings = require 'settings-sharelatex'
redis = require("redis-sharelatex")
rclient = redis.createClient(Settings.redis.web)
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
EditorRealTimeController = require("./EditorRealTimeController")
module.exports = EditorUpdatesController =
_applyUpdate: (client, project_id, doc_id, update, callback = (error) ->) ->
metrics.inc "editor.doc-update", 0.3
metrics.set "editor.active-projects", project_id, 0.3
client.get "user_id", (error, user_id) ->
metrics.set "editor.active-users", user_id, 0.3
logger.log doc_id: doc_id, project_id: project_id, client_id: update.meta?.source, version: update.v, "sending update to doc updater"
DocumentUpdaterHandler.queueChange project_id, doc_id, update, (error) ->
if error?
logger.error err:error, project_id: project_id, doc_id: doc_id, client_id: update.meta?.source, version: update.v, "document was not available for update"
client.disconnect()
callback(error)
applyOtUpdate: (client, project_id, doc_id, update) ->
update.meta ||= {}
update.meta.source = client.id
client.get "user_id", (error, user_id) ->
update.meta.user_id = user_id
EditorUpdatesController._applyUpdate client, project_id, doc_id, update
listenForUpdatesFromDocumentUpdater: () ->
rclient.subscribe "applied-ops"
rclient.on "message", @_processMessageFromDocumentUpdater.bind(@)
_processMessageFromDocumentUpdater: (channel, message) ->
message = JSON.parse message
if message.op?
@_applyUpdateFromDocumentUpdater(message.doc_id, message.op)
else if message.error?
@_processErrorFromDocumentUpdater(message.doc_id, message.error, message)
_applyUpdateFromDocumentUpdater: (doc_id, update) ->
io = require('../../infrastructure/Server').io
for client in io.sockets.clients(doc_id)
if client.id == update.meta.source
logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, "distributing update to sender"
client.emit "otUpdateApplied", v: update.v, doc: update.doc
else
logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, client_id: client.id, "distributing update to collaborator"
client.emit "otUpdateApplied", update
_processErrorFromDocumentUpdater: (doc_id, error, message) ->
io = require('../../infrastructure/Server').io
logger.error err: error, doc_id: doc_id, "error from document updater"
for client in io.sockets.clients(doc_id)
client.emit "otUpdateError", error, message
client.disconnect()

View file

@ -1,7 +0,0 @@
EditorUpdatesController = require("../Features/Editor/EditorUpdatesController")
EditorRealTimeController = require("../Features/Editor/EditorRealTimeController")
module.exports = BackgroundTasks =
run: () ->
EditorUpdatesController.listenForUpdatesFromDocumentUpdater()
EditorRealTimeController.listenForEditorEvents()

View file

@ -5,11 +5,8 @@ logger = require 'logger-sharelatex'
metrics = require('./Metrics')
crawlerLogger = require('./CrawlerLogger')
expressLocals = require('./ExpressLocals')
socketIoConfig = require('./SocketIoConfig')
Router = require('../router')
metrics.inc("startup")
SessionSockets = require('session.socket.io')
redis = require("redis-sharelatex")
rclient = redis.createClient(Settings.redis.web)
@ -119,13 +116,8 @@ app.get "/profile", (req, res) ->
logger.info ("creating HTTP server").yellow
server = require('http').createServer(app)
io = require('socket.io').listen(server)
sessionSockets = new SessionSockets(io, sessionStore, cookieParser, cookieKey)
router = new Router(app, io, sessionSockets)
socketIoConfig.configure(io)
router = new Router(app)
module.exports =
io: io
app: app
server: server

View file

@ -1,20 +0,0 @@
SocketIoRedisStore = require('socket.io/lib/stores/redis')
module.exports =
configure: (io)->
io.configure ->
io.enable('browser client minification')
io.enable('browser client etag')
# Fix for Safari 5 error of "Error during WebSocket handshake: location mismatch"
# See http://answers.dotcloud.com/question/578/problem-with-websocket-over-ssl-in-safari-with
io.set('match origin protocol', true)
# gzip uses a Node 0.8.x method of calling the gzip program which
# doesn't work with 0.6.x
#io.enable('browser client gzip')
io.set('transports', ['websocket', 'flashsocket', 'htmlfile', 'xhr-polling', 'jsonp-polling'])
io.set('log level', 1)
io.configure 'production', ->
io.set('log level', 1)

View file

@ -7,7 +7,6 @@ SecurityManager = require('./managers/SecurityManager')
AuthorizationManager = require('./Features/Security/AuthorizationManager')
EditorController = require("./Features/Editor/EditorController")
EditorRouter = require("./Features/Editor/EditorRouter")
EditorUpdatesController = require("./Features/Editor/EditorUpdatesController")
Settings = require('settings-sharelatex')
TpdsController = require('./Features/ThirdPartyDataStore/TpdsController')
SubscriptionRouter = require './Features/Subscription/SubscriptionRouter'
@ -34,7 +33,6 @@ StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter")
ChatController = require("./Features/Chat/ChatController")
BlogController = require("./Features/Blog/BlogController")
WikiController = require("./Features/Wiki/WikiController")
ConnectedUsersController = require("./Features/ConnectedUsers/ConnectedUsersController")
DropboxRouter = require "./Features/Dropbox/DropboxRouter"
dropboxHandler = require "./Features/Dropbox/DropboxHandler"
Modules = require "./infrastructure/Modules"
@ -50,7 +48,7 @@ httpAuth = require('express').basicAuth (user, pass)->
return isValid
module.exports = class Router
constructor: (app, io, socketSessions)->
constructor: (app)->
app.use(app.router)
app.get '/login', UserPagesController.loginPage
@ -126,8 +124,6 @@ module.exports = class Router
app.get "/project/:Project_id/doc/:doc_id/diff", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.get '/project/:Project_id/connected_users', SecurityManager.requestCanAccessProject, ConnectedUsersController.getConnectedUsers
app.get '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject
app.get '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
@ -226,65 +222,3 @@ module.exports = class Router
res.send(204)
app.get '*', ErrorController.notFound
socketSessions.on 'connection', (err, client, session)->
metrics.inc('socket-io.connection')
# This is not ideal - we should come up with a better way of handling
# anonymous users, but various logging lines rely on user._id
if !session or !session.user?
user = {_id: "anonymous-user"}
else
user = session.user
client.on 'joinProject', (data, callback) ->
EditorController.joinProject(client, user, data.project_id, callback)
client.on 'disconnect', () ->
metrics.inc ('socket-io.disconnect')
EditorController.leaveProject client, user
client.on 'applyOtUpdate', (doc_id, update) ->
AuthorizationManager.ensureClientCanEditProject client, (error, project_id) =>
EditorUpdatesController.applyOtUpdate(client, project_id, doc_id, update)
client.on 'clientTracking.updatePosition', (cursorData) ->
AuthorizationManager.ensureClientCanViewProject client, (error, project_id) =>
EditorController.updateClientPosition(client, cursorData)
client.on 'leaveDoc', (doc_id, callback)->
AuthorizationManager.ensureClientCanViewProject client, (error, project_id) =>
EditorController.leaveDoc(client, project_id, doc_id, callback)
client.on 'joinDoc', (args...)->
AuthorizationManager.ensureClientCanViewProject client, (error, project_id) =>
EditorController.joinDoc(client, project_id, args...)
# The remaining can be done via HTTP
client.on 'addUserToProject', (email, newPrivalageLevel, callback)->
AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) =>
EditorController.addUserToProject project_id, email, newPrivalageLevel, callback
client.on 'removeUserFromProject', (user_id, callback)->
AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) =>
EditorController.removeUserFromProject(project_id, user_id, callback)
client.on 'getUserDropboxLinkStatus', (owner_id, callback)->
AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) =>
dropboxHandler.getUserRegistrationStatus owner_id, callback
# client.on 'publishProjectAsTemplate', (user_id, callback)->
# AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) =>
# TemplatesController.publishProject user_id, project_id, callback
#
# client.on 'unPublishProjectAsTemplate', (user_id, callback)->
# AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) =>
# TemplatesController.unPublishProject user_id, project_id, callback
#
# client.on 'updateProjectDescription', (description, callback)->
# AuthorizationManager.ensureClientCanEditProject client, (error, project_id) =>
# EditorController.updateProjectDescription project_id, description, callback
#
# client.on "getPublishedDetails", (user_id, callback)->
# AuthorizationManager.ensureClientCanViewProject client, (error, project_id) =>
# TemplatesController.getTemplateDetails user_id, project_id, callback

View file

@ -75,7 +75,7 @@ block content
.modal-footer
button.btn.btn-info(ng-click="done()") #{translate("ok")}
script(src='/socket.io/socket.io.js')
script(src='#{settings.websocketsUrl}/socket.io/socket.io.js')
//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
//- and doesn't prematurely end the script tag.

View file

@ -12,26 +12,24 @@ define [
@sendCursorPositionUpdate(position)
@$scope.$on "project:joined", () =>
ide.$http
.get "/project/#{@ide.$scope.project._id}/connected_users"
.success (connectedUsers) =>
@$scope.onlineUsers = {}
for user in connectedUsers or []
if user.client_id == @ide.socket.socket.sessionid
# Don't store myself
continue
# Store data in the same format returned by clientTracking.clientUpdated
@ide.socket.emit "clientTracking.getConnectedUsers", (error, connectedUsers) =>
@$scope.onlineUsers = {}
for user in connectedUsers or []
if user.client_id == @ide.socket.socket.sessionid
# Don't store myself
continue
# Store data in the same format returned by clientTracking.clientUpdated
@$scope.onlineUsers[user.client_id] = {
id: user.client_id
user_id: user.user_id
email: user.email
name: "#{user.first_name} #{user.last_name}"
doc_id: user.cursorData?.doc_id
row: user.cursorData?.row
column: user.cursorData?.column
}
@refreshOnlineUsers()
@$scope.onlineUsers[user.client_id] = {
id: user.client_id
user_id: user.user_id
email: user.email
name: "#{user.first_name} #{user.last_name}"
doc_id: user.cursorData?.doc_id
row: user.cursorData?.row
column: user.cursorData?.column
}
@refreshOnlineUsers()
@ide.socket.on "clientTracking.clientUpdated", (client) =>
if client.id != @ide.socket.socket.sessionid # Check it's not me!

View file

@ -1,46 +0,0 @@
should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('assert')
path = require('path')
sinon = require('sinon')
modulePath = path.join __dirname, "../../../../app/js/Features/ConnectedUsers/ConnectedUsersController"
expect = require("chai").expect
describe "ConnectedUsersController", ->
beforeEach ->
@settings = {}
@ConnectedUsersManager =
getConnectedUsers:sinon.stub()
@ConnectedUsersController = SandboxedModule.require modulePath, requires:
"settings-sharelatex":@settings
"./ConnectedUsersManager":@ConnectedUsersManager
"logger-sharelatex":
log:->
err:->
@project_id = "231312390128309"
@req =
params:
project_id:@project_id
@res = {}
describe "getConnectedUsers", ->
beforeEach ->
@connectedUsersData = [{user_id:"312321"}, {user_id:"3213213"}]
it "should get the connected user data for that project", (done)->
@ConnectedUsersManager.getConnectedUsers.callsArgWith(1, null, @connectedUsersData)
@res.send = (d)=>
d.should.deep.equal @connectedUsersData
done()
@ConnectedUsersController.getConnectedUsers @req, @res
it "should send a 500 on an error", (done)->
@ConnectedUsersManager.getConnectedUsers.callsArgWith(1, "error")
@res.send = (code)=>
code.should.equal 500
done()
@ConnectedUsersController.getConnectedUsers @req, @res

View file

@ -1,154 +0,0 @@
should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('assert')
path = require('path')
sinon = require('sinon')
modulePath = path.join __dirname, "../../../../app/js/Features/ConnectedUsers/ConnectedUsersManager"
expect = require("chai").expect
tk = require("timekeeper")
describe "ConnectedUsersManager", ->
beforeEach ->
@settings =
redis:
web:{}
@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())
@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"
}
@cursorData = { row: 12, column: 9, doc_id: '53c3b8c85fee64000023dc6e' }
afterEach ->
tk.reset()
describe "updateUserPosition", ->
beforeEach ->
@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 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 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 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 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 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 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 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 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()
describe "markUserAsDisconnected", ->
beforeEach ->
@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 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 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()
describe "_getConnectedUser", ->
it "should get the user returning connected if there is a value", (done)->
cursorData = JSON.stringify(cursorData:{row:1})
@rClient.hgetall.callsArgWith(1, null, {connected_at:new Date(), cursorData})
@ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=>
result.connected.should.equal true
result.client_id.should.equal @client_id
done()
it "should get the user returning connected if there is a value", (done)->
@rClient.hgetall.callsArgWith(1)
@ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=>
result.connected.should.equal false
result.client_id.should.equal @client_id
done()
describe "getConnectedUsers", ->
beforeEach ->
@users = ["1234", "5678", "9123"]
@rClient.smembers.callsArgWith(1, null, @users)
@ConnectedUsersManager._getConnectedUser = sinon.stub()
@ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[0]).callsArgWith(2, null, {connected:true, client_id:@users[0]})
@ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[1]).callsArgWith(2, null, {connected:false, client_id:@users[1]})
@ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[2]).callsArgWith(2, null, {connected:true, client_id:@users[2]})
it "should only return the users in the list which are still in redis", (done)->
@ConnectedUsersManager.getConnectedUsers @project_id, (err, users)=>
users.length.should.equal 2
users[0].should.deep.equal {client_id:@users[0], connected:true}
users[1].should.deep.equal {client_id:@users[2], connected:true}
done()

View file

@ -18,10 +18,6 @@ describe "EditorController", ->
@doc_id = "test-doc-id"
@source = "dropbox"
@projectModelView =
_id: @project_id
owner:{_id:"something"}
@user =
_id: @user_id = "user-id"
projects: {}
@ -37,12 +33,10 @@ describe "EditorController", ->
setSpellCheckLanguage: sinon.spy()
@ProjectEntityHandler =
flushProjectToThirdPartyDataStore:sinon.stub()
@ProjectEditorHandler =
buildProjectModelView : sinon.stub().returns(@projectModelView)
@ProjectEditorHandler = {}
@Project =
findPopulatedById: sinon.stub().callsArgWith(1, null, @project)
@LimitationsManager = {}
@AuthorizationManager = {}
@client = new MockClient()
@settings =
@ -57,9 +51,6 @@ describe "EditorController", ->
addUserToProject: sinon.stub().callsArgWith(3)
@ProjectDeleter =
deleteProject: sinon.stub()
@ConnectedUsersManager =
markUserAsDisconnected:sinon.stub()
updateUserPosition:sinon.stub()
@LockManager =
getLock : sinon.stub()
releaseLock : sinon.stub()
@ -70,308 +61,21 @@ describe "EditorController", ->
'../Project/ProjectOptionsHandler' : @ProjectOptionsHandler
'../Project/ProjectDetailsHandler': @ProjectDetailsHandler
'../Project/ProjectDeleter' : @ProjectDeleter
'../Project/ProjectGetter' : @ProjectGetter = {}
'../User/UserGetter': @UserGetter = {}
'../Collaborators/CollaboratorsHandler': @CollaboratorsHandler
'../DocumentUpdater/DocumentUpdaterHandler' : @DocumentUpdaterHandler
'../Subscription/LimitationsManager' : @LimitationsManager
'../Security/AuthorizationManager' : @AuthorizationManager
'../../models/Project' : Project: @Project
"settings-sharelatex":@settings
'../Dropbox/DropboxProjectLinker':@dropboxProjectLinker
'./EditorRealTimeController':@EditorRealTimeController = {}
"../../infrastructure/Metrics": @Metrics = { inc: sinon.stub() }
"../TrackChanges/TrackChangesManager": @TrackChangesManager = {}
"../ConnectedUsers/ConnectedUsersManager":@ConnectedUsersManager
"../../infrastructure/LockManager":@LockManager
'redis-sharelatex':createClient:-> auth:->
"logger-sharelatex": @logger =
log: sinon.stub()
err: sinon.stub()
describe "joinProject", ->
beforeEach ->
sinon.spy(@client, "set")
sinon.spy(@client, "get")
@AuthorizationManager.setPrivilegeLevelOnClient = sinon.stub()
@EditorRealTimeController.emitToRoom = sinon.stub()
@ConnectedUsersManager.updateUserPosition.callsArgWith(4)
@ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub()
describe "when authorized", ->
beforeEach ->
@EditorController.buildJoinProjectView = sinon.stub().callsArgWith(2, null, @projectModelView, "owner")
@EditorController.joinProject(@client, @user, @project_id, @callback)
it "should set the privilege level on the client", ->
@AuthorizationManager.setPrivilegeLevelOnClient
.calledWith(@client, "owner")
.should.equal.true
it "should add the client to the project channel", ->
@client.join.calledWith(@project_id).should.equal true
it "should set the project_id of the client", ->
@client.set.calledWith("project_id", @project_id).should.equal true
it "should mark the user as connected with the ConnectedUsersManager", ->
@ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.id, @user, null).should.equal true
it "should return the project model view, privilege level and protocol version", ->
@callback.calledWith(null, @projectModelView, "owner", @EditorController.protocolVersion).should.equal true
describe "when not authorized", ->
beforeEach ->
@EditorController.buildJoinProjectView = sinon.stub().callsArgWith(2, null, null, false)
@EditorController.joinProject(@client, @user, @project_id, @callback)
it "should not set the privilege level on the client", ->
@AuthorizationManager.setPrivilegeLevelOnClient
.called.should.equal false
it "should not add the client to the project channel", ->
@client.join.called.should.equal false
it "should not set the project_id of the client", ->
@client.set.called.should.equal false
it "should return an error", ->
@callback.calledWith(sinon.match.truthy).should.equal true
describe "when the project is marked as deleted", ->
beforeEach ->
@projectModelView.deletedByExternalDataSource = true
@EditorController.buildJoinProjectView = sinon.stub().callsArgWith(2, null, @projectModelView, "owner")
@EditorController.joinProject(@client, @user, @project_id, @callback)
it "should remove the flag to send a user a message about the project being deleted", ->
@ProjectDeleter.unmarkAsDeletedByExternalSource
.calledWith(@project_id)
.should.equal true
describe "buildJoinProjectView", ->
beforeEach ->
@ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project)
@ProjectGetter.populateProjectWithUsers = sinon.stub().callsArgWith(1, null, @project)
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
describe "when authorized", ->
beforeEach ->
@AuthorizationManager.getPrivilegeLevelForProject =
sinon.stub().callsArgWith(2, null, true, "owner")
@EditorController.buildJoinProjectView(@project_id, @user_id, @callback)
it "should find the project without doc lines", ->
@ProjectGetter.getProjectWithoutDocLines
.calledWith(@project_id)
.should.equal true
it "should populate the user references in the project", ->
@ProjectGetter.populateProjectWithUsers
.calledWith(@project)
.should.equal true
it "should look up the user", ->
@UserGetter.getUser
.calledWith(@user_id, { isAdmin: true })
.should.equal true
it "should check the privilege level", ->
@AuthorizationManager.getPrivilegeLevelForProject
.calledWith(@project, @user)
.should.equal true
it "should return the project model view, privilege level and protocol version", ->
@callback.calledWith(null, @projectModelView, "owner").should.equal true
describe "when not authorized", ->
beforeEach ->
@AuthorizationManager.getPrivilegeLevelForProject =
sinon.stub().callsArgWith(2, null, false, null)
@EditorController.buildJoinProjectView(@project_id, @user_id, @callback)
it "should return false in the callback", ->
@callback.calledWith(null, null, false).should.equal true
describe "leaveProject", ->
beforeEach ->
sinon.stub(@client, "set")
sinon.stub(@client, "get").callsArgWith(1, null, @project_id)
@EditorRealTimeController.emitToRoom = sinon.stub()
@EditorController.flushProjectIfEmpty = sinon.stub()
@EditorController.leaveProject @client, @user
@ConnectedUsersManager.markUserAsDisconnected.callsArgWith(2)
it "should call the flush project if empty function", ->
@EditorController.flushProjectIfEmpty
.calledWith(@project_id)
.should.equal true
it "should emit a clientDisconnect to the project room", ->
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, "clientTracking.clientDisconnected", @client.id)
.should.equal true
it "should mark the user as connected with the ConnectedUsersManager", ->
@ConnectedUsersManager.markUserAsDisconnected.calledWith(@project_id, @client.id).should.equal true
describe "joinDoc", ->
beforeEach ->
@client.join = sinon.stub()
@client.set("user_id", @user_id)
@fromVersion = 40
@docLines = ["foo", "bar"]
@ops = ["mock-op-1", "mock-op-2"]
@version = 42
@DocumentUpdaterHandler.getDocument = sinon.stub().callsArgWith(3, null, @docLines, @version, @ops)
describe "with a fromVersion", ->
beforeEach ->
@EditorController.joinDoc @client, @project_id, @doc_id, @fromVersion, @callback
it "should add the client to the socket.io room for the doc", ->
@client.join.calledWith(@doc_id).should.equal true
it "should get the document", ->
@DocumentUpdaterHandler.getDocument
.calledWith(@project_id, @doc_id, @fromVersion)
.should.equal true
it "should return the doclines and version and ops", ->
@callback.calledWith(null, @docLines, @version, @ops).should.equal true
it "should increment the join-doc metric", ->
@Metrics.inc.calledWith("editor.join-doc").should.equal true
it "should log out the request", ->
@logger.log
.calledWith(user_id: @user_id, project_id: @project_id, doc_id: @doc_id, "user joining doc")
.should.equal true
describe "without a fromVersion", ->
beforeEach ->
@EditorController.joinDoc @client, @project_id, @doc_id, @callback
it "should get the document with fromVersion=-1", ->
@DocumentUpdaterHandler.getDocument
.calledWith(@project_id, @doc_id, -1)
.should.equal true
it "should return the doclines and version and ops", ->
@callback.calledWith(null, @docLines, @version, @ops).should.equal true
describe "leaveDoc", ->
beforeEach ->
@client.leave = sinon.stub()
@client.set("user_id", @user_id)
@EditorController.leaveDoc @client, @project_id, @doc_id, @callback
it "should remove the client from the socket.io room for the doc", ->
@client.leave.calledWith(@doc_id).should.equal true
it "should increment the leave-doc metric", ->
@Metrics.inc.calledWith("editor.leave-doc").should.equal true
it "should log out the request", ->
@logger.log
.calledWith(user_id: @user_id, project_id: @project_id, doc_id: @doc_id, "user leaving doc")
.should.equal true
describe "flushProjectIfEmpty", ->
beforeEach ->
@DocumentUpdaterHandler.flushProjectToMongoAndDelete = sinon.stub()
@TrackChangesManager.flushProject = sinon.stub()
describe "when a project has no more users", ->
it "should do the flush after the config set timeout to ensure that a reconect didn't just happen", (done)->
@rooms[@project_id] = []
@EditorController.flushProjectIfEmpty @project_id, =>
@DocumentUpdaterHandler.flushProjectToMongoAndDelete.calledWith(@project_id).should.equal(true)
@TrackChangesManager.flushProject.calledWith(@project_id).should.equal true
done()
describe "when a project still has connected users", ->
it "should not flush the project", (done)->
@rooms[@project_id] = ["socket-id-1", "socket-id-2"]
@EditorController.flushProjectIfEmpty @project_id, =>
@DocumentUpdaterHandler.flushProjectToMongoAndDelete.calledWith(@project_id).should.equal(false)
@TrackChangesManager.flushProject.calledWith(@project_id).should.equal false
done()
describe "updateClientPosition", ->
beforeEach ->
@EditorRealTimeController.emitToRoom = sinon.stub()
@ConnectedUsersManager.updateUserPosition.callsArgWith(4)
@update = {
doc_id: @doc_id = "doc-id-123"
row: @row = 42
column: @column = 37
}
describe "with a logged in user", ->
beforeEach ->
@clientParams = {
project_id: @project_id
first_name: @first_name = "Douglas"
last_name: @last_name = "Adams"
email: @email = "joe@example.com"
user_id: @user_id = "user-id-123"
}
@client.get = (param, callback) => callback null, @clientParams[param]
@EditorController.updateClientPosition @client, @update
@populatedCursorData =
doc_id: @doc_id,
id: @client.id
name: "#{@first_name} #{@last_name}"
row: @row
column: @column
email: @email
user_id: @user_id
it "should send the update to the project room with the user's name", ->
@EditorRealTimeController.emitToRoom.calledWith(@project_id, "clientTracking.clientUpdated", @populatedCursorData).should.equal true
it "should send the cursor data to the connected user manager", (done)->
@ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.id, {
user_id: @user_id,
email: @email,
first_name: @first_name,
last_name: @last_name
}, {
row: @row
column: @column
doc_id: @doc_id
}).should.equal true
done()
describe "with an anonymous user", ->
beforeEach ->
@clientParams = {
project_id: @project_id
}
@client.get = (param, callback) => callback null, @clientParams[param]
@EditorController.updateClientPosition @client, @update
it "should send the update to the project room with an anonymous name", ->
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, "clientTracking.clientUpdated", {
doc_id: @doc_id,
id: @client.id
name: "Anonymous"
row: @row
column: @column
})
.should.equal true
it "should not send cursor data to the connected user manager", (done)->
@ConnectedUsersManager.updateUserPosition.called.should.equal false
done()
describe "addUserToProject", ->
beforeEach ->
@email = "Jane.Doe@example.com"

View file

@ -8,6 +8,10 @@ describe "EditorHttpController", ->
@EditorHttpController = SandboxedModule.require modulePath, requires:
'../Project/ProjectEntityHandler' : @ProjectEntityHandler = {}
'../Project/ProjectDeleter' : @ProjectDeleter = {}
'../Project/ProjectGetter' : @ProjectGetter = {}
'../User/UserGetter' : @UserGetter = {}
"../Security/AuthorizationManager": @AuthorizationManager = {}
'../Project/ProjectEditorHandler': @ProjectEditorHandler = {}
"./EditorRealTimeController": @EditorRealTimeController = {}
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"./EditorController": @EditorController = {}
@ -21,6 +25,7 @@ describe "EditorHttpController", ->
@res =
send: sinon.stub()
json: sinon.stub()
@callback = sinon.stub()
describe "joinProject", ->
beforeEach ->
@ -31,7 +36,7 @@ describe "EditorHttpController", ->
@projectView = {
_id: @project_id
}
@EditorController.buildJoinProjectView = sinon.stub().callsArgWith(2, null, @projectView, "owner")
@EditorHttpController._buildJoinProjectView = sinon.stub().callsArgWith(2, null, @projectView, "owner")
@ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub()
describe "successfully", ->
@ -39,7 +44,7 @@ describe "EditorHttpController", ->
@EditorHttpController.joinProject @req, @res
it "should get the project view", ->
@EditorController.buildJoinProjectView
@EditorHttpController._buildJoinProjectView
.calledWith(@project_id, @user_id)
.should.equal true
@ -71,6 +76,61 @@ describe "EditorHttpController", ->
.calledWith(@project_id)
.should.equal true
describe "_buildJoinProjectView", ->
beforeEach ->
@project =
_id: @project_id
owner_ref:{_id:"something"}
@user =
_id: @user_id = "user-id"
projects: {}
@projectModelView =
_id: @project_id
owner:{_id:"something"}
view: true
@ProjectEditorHandler.buildProjectModelView = sinon.stub().returns(@projectModelView)
@ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project)
@ProjectGetter.populateProjectWithUsers = sinon.stub().callsArgWith(1, null, @project)
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
describe "when authorized", ->
beforeEach ->
@AuthorizationManager.getPrivilegeLevelForProject =
sinon.stub().callsArgWith(2, null, true, "owner")
@EditorHttpController._buildJoinProjectView(@project_id, @user_id, @callback)
it "should find the project without doc lines", ->
@ProjectGetter.getProjectWithoutDocLines
.calledWith(@project_id)
.should.equal true
it "should populate the user references in the project", ->
@ProjectGetter.populateProjectWithUsers
.calledWith(@project)
.should.equal true
it "should look up the user", ->
@UserGetter.getUser
.calledWith(@user_id, { isAdmin: true })
.should.equal true
it "should check the privilege level", ->
@AuthorizationManager.getPrivilegeLevelForProject
.calledWith(@project, @user)
.should.equal true
it "should return the project model view, privilege level and protocol version", ->
@callback.calledWith(null, @projectModelView, "owner").should.equal true
describe "when not authorized", ->
beforeEach ->
@AuthorizationManager.getPrivilegeLevelForProject =
sinon.stub().callsArgWith(2, null, false, null)
@EditorHttpController._buildJoinProjectView(@project_id, @user_id, @callback)
it "should return false in the callback", ->
@callback.calledWith(null, null, false).should.equal true
describe "restoreDoc", ->
beforeEach ->
@req.params =

View file

@ -41,49 +41,3 @@ describe "EditorRealTimeController", ->
@EditorRealTimeController.emitToRoom
.calledWith("all", @message, @payload...)
.should.equal true
describe "listenForEditorEvents", ->
beforeEach ->
@EditorRealTimeController._processEditorEvent = sinon.stub()
@EditorRealTimeController.listenForEditorEvents()
it "should subscribe to the editor-events channel", ->
@EditorRealTimeController.rclientSub.subscribe
.calledWith("editor-events")
.should.equal true
it "should process the events with _processEditorEvent", ->
@EditorRealTimeController.rclientSub.on
.calledWith("message", sinon.match.func)
.should.equal true
describe "_processEditorEvent", ->
describe "with a designated room", ->
beforeEach ->
@io.sockets =
in: sinon.stub().returns(emit: @emit = sinon.stub())
data = JSON.stringify
room_id: @room_id
message: @message
payload: @payload
@EditorRealTimeController._processEditorEvent("editor-events", data)
it "should send the message to all clients in the room", ->
@io.sockets.in
.calledWith(@room_id)
.should.equal true
@emit.calledWith(@message, @payload...).should.equal true
describe "when emitting to all", ->
beforeEach ->
@io.sockets =
emit: @emit = sinon.stub()
data = JSON.stringify
room_id: "all"
message: @message
payload: @payload
@EditorRealTimeController._processEditorEvent("editor-events", data)
it "should send the message to all clients", ->
@emit.calledWith(@message, @payload...).should.equal true

View file

@ -1,164 +0,0 @@
SandboxedModule = require('sandboxed-module')
sinon = require('sinon')
require('chai').should()
modulePath = require('path').join __dirname, '../../../../app/js/Features/Editor/EditorUpdatesController'
MockClient = require "../helpers/MockClient"
assert = require('assert')
describe "EditorUpdatesController", ->
beforeEach ->
@project_id = "project-id-123"
@doc_id = "doc-id-123"
@client = new MockClient()
@callback = sinon.stub()
@EditorUpdatesController = SandboxedModule.require modulePath, requires:
"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub() }
"./EditorRealTimeController" : @EditorRealTimeController = {}
"../DocumentUpdater/DocumentUpdaterHandler" : @DocumentUpdaterHandler = {}
"../../infrastructure/Metrics" : @metrics = { set: sinon.stub(), inc: sinon.stub() }
"../../infrastructure/Server" : io: @io = {}
"redis-sharelatex" :
createClient: ()=>
@rclient = {auth:->}
describe "_applyUpdate", ->
beforeEach ->
@update = {op: {p: 12, t: "foo"}}
@client.set("user_id", @user_id = "user-id-123")
@DocumentUpdaterHandler.queueChange = sinon.stub().callsArg(3)
describe "succesfully", ->
beforeEach ->
@EditorUpdatesController._applyUpdate @client, @project_id, @doc_id, @update, @callback
it "should queue the update", ->
@DocumentUpdaterHandler.queueChange
.calledWith(@project_id, @doc_id, @update)
.should.equal true
it "should call the callback", ->
@callback.called.should.equal true
it "should update the active users metric", ->
@metrics.set.calledWith("editor.active-users", @user_id).should.equal true
it "should update the active projects metric", ->
@metrics.set.calledWith("editor.active-projects", @project_id).should.equal true
it "should increment the doc updates", ->
@metrics.inc.calledWith("editor.doc-update").should.equal true
describe "unsuccessfully", ->
beforeEach ->
@client.disconnect = sinon.stub()
@DocumentUpdaterHandler.queueChange = sinon.stub().callsArgWith(3, new Error("Something went wrong"))
@EditorUpdatesController._applyUpdate @client, @project_id, @doc_id, @update, @callback
it "should disconnect the client", ->
@client.disconnect.called.should.equal true
it "should log an error", ->
@logger.error.called.should.equal true
describe "applyOtUpdate", ->
beforeEach ->
@client.id = "client-id"
@client.set("user_id", @user_id = "user-id-123")
@update = {op: {p: 12, t: "foo"}}
@EditorUpdatesController._applyUpdate = sinon.stub()
@EditorUpdatesController.applyOtUpdate @client, @project_id, @doc_id, @update
it "should set the source of the update to the client id", ->
@update.meta.source.should.equal @client.id
it "should set the user_id of the update to the user id", ->
@update.meta.user_id.should.equal @user_id
it "should apply the update", ->
@EditorUpdatesController._applyUpdate
.calledWith(@client, @project_id, @doc_id, @update)
.should.equal true
describe "listenForUpdatesFromDocumentUpdater", ->
beforeEach ->
@rclient.subscribe = sinon.stub()
@rclient.on = sinon.stub()
@EditorUpdatesController.listenForUpdatesFromDocumentUpdater()
it "should subscribe to the doc-updater stream", ->
@rclient.subscribe.calledWith("applied-ops").should.equal true
it "should register a callback to handle updates", ->
@rclient.on.calledWith("message").should.equal true
describe "_processMessageFromDocumentUpdater", ->
describe "with update", ->
beforeEach ->
@message =
doc_id: @doc_id
op: {t: "foo", p: 12}
@EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub()
@EditorUpdatesController._processMessageFromDocumentUpdater "applied-ops", JSON.stringify(@message)
it "should apply the update", ->
@EditorUpdatesController._applyUpdateFromDocumentUpdater
.calledWith(@doc_id, @message.op)
.should.equal true
describe "with error", ->
beforeEach ->
@message =
doc_id: @doc_id
error: "Something went wrong"
@EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub()
@EditorUpdatesController._processMessageFromDocumentUpdater "applied-ops", JSON.stringify(@message)
it "should process the error", ->
@EditorUpdatesController._processErrorFromDocumentUpdater
.calledWith(@doc_id, @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.id
v: @version = 42
doc: @doc_id
@io.sockets =
clients: sinon.stub().returns([@sourceClient, @otherClients...])
@EditorUpdatesController._applyUpdateFromDocumentUpdater @doc_id, @update
it "should send a version bump to the source client", ->
@sourceClient.emit
.calledWith("otUpdateApplied", v: @version, doc: @doc_id)
.should.equal true
it "should get the clients connected to the document", ->
@io.sockets.clients
.calledWith(@doc_id)
.should.equal true
it "should send the full update to the other clients", ->
for client in @otherClients
client.emit
.calledWith("otUpdateApplied", @update)
.should.equal true
describe "_processErrorFromDocumentUpdater", ->
beforeEach ->
@clients = [new MockClient(), new MockClient()]
@io.sockets =
clients: sinon.stub().returns(@clients)
@EditorUpdatesController._processErrorFromDocumentUpdater @doc_id, "Something went wrong"
it "should log out an error", ->
@logger.error.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