mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Create joinDoc socket.io end point
This commit is contained in:
parent
919b192e16
commit
eb8ccc0298
13 changed files with 516 additions and 56 deletions
12
services/real-time/app/coffee/AuthorizationManager.coffee
Normal file
12
services/real-time/app/coffee/AuthorizationManager.coffee
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module.exports = AuthorizationManager =
|
||||||
|
assertClientCanViewProject: (client, callback = (error) ->) ->
|
||||||
|
AuthorizationManager._assertClientHasPrivilegeLevel client, ["readOnly", "readAndWrite", "owner"], callback
|
||||||
|
|
||||||
|
_assertClientHasPrivilegeLevel: (client, allowedLevels, callback = (error) ->) ->
|
||||||
|
client.get "privilege_level", (error, privilegeLevel) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
allowed = (privilegeLevel in allowedLevels)
|
||||||
|
if allowed
|
||||||
|
callback null
|
||||||
|
else
|
||||||
|
callback new Error("not authorized")
|
26
services/real-time/app/coffee/DocumentUpdaterManager.coffee
Normal file
26
services/real-time/app/coffee/DocumentUpdaterManager.coffee
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
request = require "request"
|
||||||
|
logger = require "logger-sharelatex"
|
||||||
|
settings = require "settings-sharelatex"
|
||||||
|
|
||||||
|
module.exports = DocumentUpdaterManager =
|
||||||
|
getDocument: (project_id, doc_id, fromVersion, callback = (error, exists, doclines, version) ->) ->
|
||||||
|
#timer = new metrics.Timer("get-document")
|
||||||
|
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}"
|
||||||
|
logger.log {project_id, doc_id, fromVersion}, "getting doc from document updater"
|
||||||
|
request.get url, (err, res, body) ->
|
||||||
|
#timer.done()
|
||||||
|
if err?
|
||||||
|
logger.error {err, url, project_id, doc_id}, "error getting doc from doc updater"
|
||||||
|
return callback(err)
|
||||||
|
if 200 <= res.statusCode < 300
|
||||||
|
logger.log {project_id, doc_id}, "got doc from document document updater"
|
||||||
|
try
|
||||||
|
body = JSON.parse(body)
|
||||||
|
catch error
|
||||||
|
return callback(error)
|
||||||
|
callback null, body?.lines, body?.version, body?.ops
|
||||||
|
else
|
||||||
|
err = new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
||||||
|
err.statusCode = res.statusCode
|
||||||
|
logger.error {err, project_id, doc_id, url}, "doc updater returned a non-success status code: #{res.statusCode}"
|
||||||
|
callback err
|
|
@ -3,6 +3,23 @@ logger = require "logger-sharelatex"
|
||||||
WebsocketController = require "./WebsocketController"
|
WebsocketController = require "./WebsocketController"
|
||||||
|
|
||||||
module.exports = Router =
|
module.exports = Router =
|
||||||
|
# We don't want to send raw errors back to the client, in case they
|
||||||
|
# contain sensitive data. Instead we log them out, and send a generic
|
||||||
|
# JSON object which can be serialized over socket.io
|
||||||
|
_createCallbackWithErrorFilter: (client, method, callback) ->
|
||||||
|
return (err, args...) ->
|
||||||
|
if err?
|
||||||
|
|
||||||
|
err = {message: "Something went wrong"}
|
||||||
|
callback err, args...
|
||||||
|
|
||||||
|
# Used in error reporting
|
||||||
|
_getClientData: (client, callback = (error, data) ->) ->
|
||||||
|
client.get "user_id", (error, user_id) ->
|
||||||
|
client.get "project_id", (error, project_id) ->
|
||||||
|
client.get "doc_id", (error, doc_id) ->
|
||||||
|
callback null, { id: client.id, user_id, project_id, doc_id }
|
||||||
|
|
||||||
configure: (app, io, session) ->
|
configure: (app, io, session) ->
|
||||||
session.on 'connection', (error, client, session) ->
|
session.on 'connection', (error, client, session) ->
|
||||||
if error?
|
if error?
|
||||||
|
@ -12,7 +29,7 @@ module.exports = Router =
|
||||||
|
|
||||||
Metrics.inc('socket-io.connection')
|
Metrics.inc('socket-io.connection')
|
||||||
|
|
||||||
logger.log session: session, "got session"
|
logger.log session: session, client_id: client.id, "client connected"
|
||||||
|
|
||||||
user = session.user
|
user = session.user
|
||||||
if !user? or !user._id?
|
if !user? or !user._id?
|
||||||
|
@ -21,5 +38,28 @@ module.exports = Router =
|
||||||
return
|
return
|
||||||
|
|
||||||
client.on "joinProject", (data = {}, callback) ->
|
client.on "joinProject", (data = {}, callback) ->
|
||||||
WebsocketController.joinProject(client, user, data?.project_id, callback)
|
WebsocketController.joinProject client, user, data.project_id, (err, args...) ->
|
||||||
|
if err?
|
||||||
|
Router._getClientData client, (_, client) ->
|
||||||
|
logger.error {err, client, project_id: data.project_id}, "server side error in joinProject"
|
||||||
|
# Don't return raw error to prevent leaking server side info
|
||||||
|
return callback {message: "Something went wrong"}
|
||||||
|
else
|
||||||
|
callback(null, args...)
|
||||||
|
|
||||||
|
|
||||||
|
client.on "joinDoc", (doc_id, fromVersion, callback) ->
|
||||||
|
# fromVersion is optional
|
||||||
|
if typeof fromVersion == "function"
|
||||||
|
callback = fromVersion
|
||||||
|
fromVersion = -1
|
||||||
|
|
||||||
|
WebsocketController.joinDoc client, doc_id, fromVersion, (err, args...) ->
|
||||||
|
if err?
|
||||||
|
Router._getClientData client, (_, client) ->
|
||||||
|
logger.error {err, client, doc_id, fromVersion}, "server side error in joinDoc"
|
||||||
|
# Don't return raw error to prevent leaking server side info
|
||||||
|
return callback {message: "Something went wrong"}
|
||||||
|
else
|
||||||
|
callback(null, args...)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
logger = require "logger-sharelatex"
|
logger = require "logger-sharelatex"
|
||||||
WebApiManager = require "./WebApiManager"
|
WebApiManager = require "./WebApiManager"
|
||||||
|
AuthorizationManager = require "./AuthorizationManager"
|
||||||
|
DocumentUpdaterManager = require "./DocumentUpdaterManager"
|
||||||
|
|
||||||
module.exports = WebsocketController =
|
module.exports = WebsocketController =
|
||||||
# If the protocol version changes when the client reconnects,
|
# If the protocol version changes when the client reconnects,
|
||||||
|
@ -9,17 +11,16 @@ module.exports = WebsocketController =
|
||||||
|
|
||||||
joinProject: (client, user, project_id, callback = (error, project, privilegeLevel, protocolVersion) ->) ->
|
joinProject: (client, user, project_id, callback = (error, project, privilegeLevel, protocolVersion) ->) ->
|
||||||
user_id = user?._id
|
user_id = user?._id
|
||||||
logger.log {user_id, project_id}, "user joining project"
|
logger.log {user_id, project_id, client_id: client.id}, "user joining project"
|
||||||
WebApiManager.joinProject project_id, user_id, (error, project, privilegeLevel) ->
|
WebApiManager.joinProject project_id, user_id, (error, project, privilegeLevel) ->
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
|
|
||||||
if !privilegeLevel or privilegeLevel == ""
|
if !privilegeLevel or privilegeLevel == ""
|
||||||
err = new Error("not authorized")
|
err = new Error("not authorized")
|
||||||
logger.error {err, project_id, user_id}, "user is not authorized to join project"
|
logger.error {err, project_id, user_id, client_id: client.id}, "user is not authorized to join project"
|
||||||
# Don't send an error object since socket.io can apparently
|
return callback(err)
|
||||||
# only serialize JSON.
|
|
||||||
return callback({message: err.message})
|
|
||||||
|
|
||||||
|
client.set("privilege_level", privilegeLevel)
|
||||||
client.set("user_id", user_id)
|
client.set("user_id", user_id)
|
||||||
client.set("project_id", project_id)
|
client.set("project_id", project_id)
|
||||||
client.set("owner_id", project?.owner?._id)
|
client.set("owner_id", project?.owner?._id)
|
||||||
|
@ -31,3 +32,29 @@ module.exports = WebsocketController =
|
||||||
client.set("login_count", user?.loginCount)
|
client.set("login_count", user?.loginCount)
|
||||||
|
|
||||||
callback null, project, privilegeLevel, WebsocketController.PROTOCOL_VERSION
|
callback null, project, privilegeLevel, WebsocketController.PROTOCOL_VERSION
|
||||||
|
|
||||||
|
joinDoc: (client, doc_id, fromVersion = -1, callback = (error, doclines, version, ops) ->) ->
|
||||||
|
client.get "user_id", (error, user_id) ->
|
||||||
|
client.get "project_id", (error, project_id) ->
|
||||||
|
logger.log {user_id, project_id, doc_id, fromVersion, client_id: client.id}, "client joining doc"
|
||||||
|
|
||||||
|
AuthorizationManager.assertClientCanViewProject client, (error) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
client.get "project_id", (error, project_id) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
return callback(new Error("no project_id found on client")) if !project_id?
|
||||||
|
DocumentUpdaterManager.getDocument project_id, doc_id, fromVersion, (error, lines, version, ops) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
# 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
|
||||||
|
escapedLines = []
|
||||||
|
for line in lines
|
||||||
|
try
|
||||||
|
line = unescape(encodeURIComponent(line))
|
||||||
|
catch err
|
||||||
|
logger.err {err, project_id, doc_id, fromVersion, line, client_id: client.id}, "error encoding line uri component"
|
||||||
|
return callback(err)
|
||||||
|
escapedLines.push line
|
||||||
|
client.join(doc_id)
|
||||||
|
callback null, escapedLines, version, ops
|
||||||
|
|
|
@ -13,6 +13,8 @@ module.exports =
|
||||||
apis:
|
apis:
|
||||||
web:
|
web:
|
||||||
url: "http://localhost:3000"
|
url: "http://localhost:3000"
|
||||||
|
documentupdater:
|
||||||
|
url: "http://localhost:3003"
|
||||||
|
|
||||||
security:
|
security:
|
||||||
sessionSecret: "secret-please-change"
|
sessionSecret: "secret-please-change"
|
||||||
|
|
118
services/real-time/test/acceptance/coffee/JoinDocTests.coffee
Normal file
118
services/real-time/test/acceptance/coffee/JoinDocTests.coffee
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
chai = require("chai")
|
||||||
|
expect = chai.expect
|
||||||
|
chai.should()
|
||||||
|
|
||||||
|
RealTimeClient = require "./helpers/RealTimeClient"
|
||||||
|
MockDocUpdaterServer = require "./helpers/MockDocUpdaterServer"
|
||||||
|
FixturesManager = require "./helpers/FixturesManager"
|
||||||
|
|
||||||
|
describe "joinDoc", ->
|
||||||
|
before ->
|
||||||
|
@lines = ["test", "doc", "lines"]
|
||||||
|
@version = 42
|
||||||
|
@ops = ["mock", "doc", "ops"]
|
||||||
|
|
||||||
|
describe "when authorised readAndWrite", ->
|
||||||
|
before (done) ->
|
||||||
|
FixturesManager.setUpProject {
|
||||||
|
privilegeLevel: "readAndWrite"
|
||||||
|
}, (error, data) =>
|
||||||
|
throw error if error?
|
||||||
|
{@project_id, @user_id} = data
|
||||||
|
FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (error, data) =>
|
||||||
|
throw error if error?
|
||||||
|
{@doc_id} = data
|
||||||
|
@client = RealTimeClient.connect()
|
||||||
|
@client.emit "joinProject", project_id: @project_id, (error) =>
|
||||||
|
throw error if error?
|
||||||
|
@client.emit "joinDoc", @doc_id, (error, @returnedArgs...) =>
|
||||||
|
throw error if error?
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "should get the doc from the doc updater", ->
|
||||||
|
MockDocUpdaterServer.getDocument
|
||||||
|
.calledWith(@project_id, @doc_id, -1)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should return the doc lines, version and ops", ->
|
||||||
|
@returnedArgs.should.deep.equal [@lines, @version, @ops]
|
||||||
|
|
||||||
|
describe "when authorised readOnly", ->
|
||||||
|
before (done) ->
|
||||||
|
FixturesManager.setUpProject {
|
||||||
|
privilegeLevel: "readOnly"
|
||||||
|
}, (error, data) =>
|
||||||
|
throw error if error?
|
||||||
|
{@project_id, @user_id} = data
|
||||||
|
FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (error, data) =>
|
||||||
|
throw error if error?
|
||||||
|
{@doc_id} = data
|
||||||
|
@client = RealTimeClient.connect()
|
||||||
|
@client.emit "joinProject", project_id: @project_id, (error) =>
|
||||||
|
throw error if error?
|
||||||
|
@client.emit "joinDoc", @doc_id, (error, @returnedArgs...) =>
|
||||||
|
throw error if error?
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "should get the doc from the doc updater", ->
|
||||||
|
MockDocUpdaterServer.getDocument
|
||||||
|
.calledWith(@project_id, @doc_id, -1)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should return the doc lines, version and ops", ->
|
||||||
|
@returnedArgs.should.deep.equal [@lines, @version, @ops]
|
||||||
|
|
||||||
|
describe "when authorised as owner", ->
|
||||||
|
before (done) ->
|
||||||
|
FixturesManager.setUpProject {
|
||||||
|
privilegeLevel: "owner"
|
||||||
|
}, (error, data) =>
|
||||||
|
throw error if error?
|
||||||
|
{@project_id, @user_id} = data
|
||||||
|
FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (error, data) =>
|
||||||
|
throw error if error?
|
||||||
|
{@doc_id} = data
|
||||||
|
@client = RealTimeClient.connect()
|
||||||
|
@client.emit "joinProject", project_id: @project_id, (error) =>
|
||||||
|
throw error if error?
|
||||||
|
@client.emit "joinDoc", @doc_id, (error, @returnedArgs...) =>
|
||||||
|
throw error if error?
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "should get the doc from the doc updater", ->
|
||||||
|
MockDocUpdaterServer.getDocument
|
||||||
|
.calledWith(@project_id, @doc_id, -1)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should return the doc lines, version and ops", ->
|
||||||
|
@returnedArgs.should.deep.equal [@lines, @version, @ops]
|
||||||
|
|
||||||
|
# It is impossible to write an acceptance test to test joining an unauthorized
|
||||||
|
# project, since joinProject already catches that. If you can join a project,
|
||||||
|
# then you can join a doc in that project.
|
||||||
|
|
||||||
|
describe "with a fromVersion", ->
|
||||||
|
before (done) ->
|
||||||
|
@fromVersion = 36
|
||||||
|
FixturesManager.setUpProject {
|
||||||
|
privilegeLevel: "readAndWrite"
|
||||||
|
}, (error, data) =>
|
||||||
|
throw error if error?
|
||||||
|
{@project_id, @user_id} = data
|
||||||
|
FixturesManager.setUpDoc @project_id, {@lines, @version, @ops}, (error, data) =>
|
||||||
|
throw error if error?
|
||||||
|
{@doc_id} = data
|
||||||
|
@client = RealTimeClient.connect()
|
||||||
|
@client.emit "joinProject", project_id: @project_id, (error) =>
|
||||||
|
throw error if error?
|
||||||
|
@client.emit "joinDoc", @doc_id, @fromVersion, (error, @returnedArgs...) =>
|
||||||
|
throw error if error?
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "should get the doc from the doc updater with the fromVersion", ->
|
||||||
|
MockDocUpdaterServer.getDocument
|
||||||
|
.calledWith(@project_id, @doc_id, @fromVersion)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should return the doc lines, version and ops", ->
|
||||||
|
@returnedArgs.should.deep.equal [@lines, @version, @ops]
|
|
@ -3,33 +3,30 @@ expect = chai.expect
|
||||||
chai.should()
|
chai.should()
|
||||||
|
|
||||||
RealTimeClient = require "./helpers/RealTimeClient"
|
RealTimeClient = require "./helpers/RealTimeClient"
|
||||||
MockWebClient = require "./helpers/MockWebClient"
|
MockWebServer = require "./helpers/MockWebServer"
|
||||||
|
FixturesManager = require "./helpers/FixturesManager"
|
||||||
|
|
||||||
|
|
||||||
describe "joinProject", ->
|
describe "joinProject", ->
|
||||||
describe "when authorized", ->
|
describe "when authorized", ->
|
||||||
before (done) ->
|
before (done) ->
|
||||||
@user_id = "mock-user-id"
|
FixturesManager.setUpProject {
|
||||||
@project_id = "mock-project-id"
|
privilegeLevel: "owner"
|
||||||
privileges = {}
|
project: {
|
||||||
privileges[@user_id] = "owner"
|
name: "Test Project"
|
||||||
MockWebClient.createMockProject(@project_id, privileges, {
|
}
|
||||||
name: "Test Project"
|
}, (error, data) =>
|
||||||
})
|
|
||||||
MockWebClient.run (error) =>
|
|
||||||
throw error if error?
|
throw error if error?
|
||||||
RealTimeClient.setSession {
|
{@user_id, @project_id} = data
|
||||||
user: { _id: @user_id }
|
@client = RealTimeClient.connect()
|
||||||
}, (error) =>
|
@client.emit "joinProject", {
|
||||||
|
project_id: @project_id
|
||||||
|
}, (error, @project, @privilegeLevel, @protocolVersion) =>
|
||||||
throw error if error?
|
throw error if error?
|
||||||
@client = RealTimeClient.connect()
|
done()
|
||||||
@client.emit "joinProject", {
|
|
||||||
project_id: @project_id
|
|
||||||
}, (error, @project, @privilegeLevel, @protocolVersion) =>
|
|
||||||
throw error if error?
|
|
||||||
done()
|
|
||||||
|
|
||||||
it "should get the project from web", ->
|
it "should get the project from web", ->
|
||||||
MockWebClient.joinProject
|
MockWebServer.joinProject
|
||||||
.calledWith(@project_id, @user_id)
|
.calledWith(@project_id, @user_id)
|
||||||
.should.equal true
|
.should.equal true
|
||||||
|
|
||||||
|
@ -46,23 +43,20 @@ describe "joinProject", ->
|
||||||
|
|
||||||
describe "when not authorized", ->
|
describe "when not authorized", ->
|
||||||
before (done) ->
|
before (done) ->
|
||||||
@user_id = "mock-user-id-2"
|
FixturesManager.setUpProject {
|
||||||
@project_id = "mock-project-id-2"
|
privilegeLevel: null
|
||||||
privileges = {}
|
project: {
|
||||||
MockWebClient.createMockProject(@project_id, privileges, {
|
name: "Test Project"
|
||||||
name: "Test Project"
|
}
|
||||||
})
|
}, (error, data) =>
|
||||||
MockWebClient.run (error) =>
|
|
||||||
throw error if error?
|
throw error if error?
|
||||||
RealTimeClient.setSession {
|
{@user_id, @project_id} = data
|
||||||
user: { _id: @user_id }
|
@client = RealTimeClient.connect()
|
||||||
}, (error) =>
|
@client.emit "joinProject", {
|
||||||
throw error if error?
|
project_id: @project_id
|
||||||
@client = RealTimeClient.connect()
|
}, (@error, @project, @privilegeLevel, @protocolVersion) =>
|
||||||
@client.emit "joinProject", {
|
done()
|
||||||
project_id: @project_id
|
|
||||||
}, (@error, @project, @privilegeLevel, @protocolVersion) =>
|
|
||||||
done()
|
|
||||||
|
|
||||||
it "should return an error", ->
|
it "should return an error", ->
|
||||||
@error.message.should.equal "not authorized"
|
# We don't return specific errors
|
||||||
|
@error.message.should.equal "Something went wrong"
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
RealTimeClient = require "./RealTimeClient"
|
||||||
|
MockWebServer = require "./MockWebServer"
|
||||||
|
MockDocUpdaterServer = require "./MockDocUpdaterServer"
|
||||||
|
|
||||||
|
module.exports = FixturesManager =
|
||||||
|
setUpProject: (options = {}, callback = (error, data) ->) ->
|
||||||
|
options.user_id ||= FixturesManager.getRandomId()
|
||||||
|
options.project_id ||= FixturesManager.getRandomId()
|
||||||
|
options.project ||= { name: "Test Project" }
|
||||||
|
{project_id, user_id, privilegeLevel, project} = options
|
||||||
|
|
||||||
|
privileges = {}
|
||||||
|
privileges[user_id] = privilegeLevel
|
||||||
|
|
||||||
|
MockWebServer.createMockProject(project_id, privileges, project)
|
||||||
|
MockWebServer.run (error) =>
|
||||||
|
throw error if error?
|
||||||
|
RealTimeClient.setSession {
|
||||||
|
user: { _id: user_id }
|
||||||
|
}, (error) =>
|
||||||
|
throw error if error?
|
||||||
|
callback null, {project_id, user_id, privilegeLevel, project}
|
||||||
|
|
||||||
|
setUpDoc: (project_id, options = {}, callback = (error, data) ->) ->
|
||||||
|
options.doc_id ||= FixturesManager.getRandomId()
|
||||||
|
options.lines ||= ["doc", "lines"]
|
||||||
|
options.version ||= 42
|
||||||
|
options.ops ||= ["mock", "ops"]
|
||||||
|
{doc_id, lines, version, ops} = options
|
||||||
|
|
||||||
|
MockDocUpdaterServer.createMockDoc project_id, doc_id, {lines, version, ops}
|
||||||
|
MockDocUpdaterServer.run (error) =>
|
||||||
|
throw error if error?
|
||||||
|
callback null, {project_id, doc_id, lines, version, ops}
|
||||||
|
|
||||||
|
getRandomId: () ->
|
||||||
|
return require("crypto")
|
||||||
|
.createHash("sha1")
|
||||||
|
.update(Math.random().toString())
|
||||||
|
.digest("hex")
|
||||||
|
.slice(0,24)
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
sinon = require "sinon"
|
||||||
|
express = require "express"
|
||||||
|
|
||||||
|
module.exports = MockDocUpdaterServer =
|
||||||
|
docs: {}
|
||||||
|
|
||||||
|
createMockDoc: (project_id, doc_id, data) ->
|
||||||
|
MockDocUpdaterServer.docs["#{project_id}:#{doc_id}"] = data
|
||||||
|
|
||||||
|
getDocument: (project_id, doc_id, fromVersion, callback = (error, data) ->) ->
|
||||||
|
callback(
|
||||||
|
null, MockDocUpdaterServer.docs["#{project_id}:#{doc_id}"]
|
||||||
|
)
|
||||||
|
|
||||||
|
getDocumentRequest: (req, res, next) ->
|
||||||
|
{project_id, doc_id} = req.params
|
||||||
|
{fromVersion} = req.query
|
||||||
|
fromVersion = parseInt(fromVersion, 10)
|
||||||
|
MockDocUpdaterServer.getDocument project_id, doc_id, fromVersion, (error, data) ->
|
||||||
|
return next(error) if error?
|
||||||
|
res.json data
|
||||||
|
|
||||||
|
running: false
|
||||||
|
run: (callback = (error) ->) ->
|
||||||
|
if MockDocUpdaterServer.running
|
||||||
|
return callback()
|
||||||
|
app = express()
|
||||||
|
app.get "/project/:project_id/doc/:doc_id", MockDocUpdaterServer.getDocumentRequest
|
||||||
|
app.listen 3003, (error) ->
|
||||||
|
MockDocUpdaterServer.running = true
|
||||||
|
callback(error)
|
||||||
|
|
||||||
|
sinon.spy MockDocUpdaterServer, "getDocument"
|
|
@ -1,25 +1,25 @@
|
||||||
sinon = require "sinon"
|
sinon = require "sinon"
|
||||||
express = require "express"
|
express = require "express"
|
||||||
|
|
||||||
module.exports = MockWebClient =
|
module.exports = MockWebServer =
|
||||||
projects: {}
|
projects: {}
|
||||||
privileges: {}
|
privileges: {}
|
||||||
|
|
||||||
createMockProject: (project_id, privileges, project) ->
|
createMockProject: (project_id, privileges, project) ->
|
||||||
MockWebClient.privileges[project_id] = privileges
|
MockWebServer.privileges[project_id] = privileges
|
||||||
MockWebClient.projects[project_id] = project
|
MockWebServer.projects[project_id] = project
|
||||||
|
|
||||||
joinProject: (project_id, user_id, callback = (error, project, privilegeLevel) ->) ->
|
joinProject: (project_id, user_id, callback = (error, project, privilegeLevel) ->) ->
|
||||||
callback(
|
callback(
|
||||||
null,
|
null,
|
||||||
MockWebClient.projects[project_id],
|
MockWebServer.projects[project_id],
|
||||||
MockWebClient.privileges[project_id][user_id]
|
MockWebServer.privileges[project_id][user_id]
|
||||||
)
|
)
|
||||||
|
|
||||||
joinProjectRequest: (req, res, next) ->
|
joinProjectRequest: (req, res, next) ->
|
||||||
{project_id} = req.params
|
{project_id} = req.params
|
||||||
{user_id} = req.query
|
{user_id} = req.query
|
||||||
MockWebClient.joinProject project_id, user_id, (error, project, privilegeLevel) ->
|
MockWebServer.joinProject project_id, user_id, (error, project, privilegeLevel) ->
|
||||||
return next(error) if error?
|
return next(error) if error?
|
||||||
res.json {
|
res.json {
|
||||||
project: project
|
project: project
|
||||||
|
@ -28,12 +28,12 @@ module.exports = MockWebClient =
|
||||||
|
|
||||||
running: false
|
running: false
|
||||||
run: (callback = (error) ->) ->
|
run: (callback = (error) ->) ->
|
||||||
if MockWebClient.running
|
if MockWebServer.running
|
||||||
return callback()
|
return callback()
|
||||||
app = express()
|
app = express()
|
||||||
app.post "/project/:project_id/join", MockWebClient.joinProjectRequest
|
app.post "/project/:project_id/join", MockWebServer.joinProjectRequest
|
||||||
app.listen 3000, (error) ->
|
app.listen 3000, (error) ->
|
||||||
MockWebClient.running = true
|
MockWebServer.running = true
|
||||||
callback(error)
|
callback(error)
|
||||||
|
|
||||||
sinon.spy MockWebClient, "joinProject"
|
sinon.spy MockWebServer, "joinProject"
|
|
@ -0,0 +1,39 @@
|
||||||
|
chai = require "chai"
|
||||||
|
chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
sinon = require("sinon")
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
path = require "path"
|
||||||
|
modulePath = '../../../app/js/AuthorizationManager'
|
||||||
|
|
||||||
|
describe 'AuthorizationManager', ->
|
||||||
|
beforeEach ->
|
||||||
|
@client =
|
||||||
|
params: {}
|
||||||
|
get: (param, cb) -> cb null, @params[param]
|
||||||
|
@AuthorizationManager = SandboxedModule.require modulePath, requires: {}
|
||||||
|
|
||||||
|
describe "assertClientCanViewProject", ->
|
||||||
|
it "should allow the readOnly privilegeLevel", (done) ->
|
||||||
|
@client.params.privilege_level = "readOnly"
|
||||||
|
@AuthorizationManager.assertClientCanViewProject @client, (error) ->
|
||||||
|
expect(error).to.be.null
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "should allow the readAndWrite privilegeLevel", (done) ->
|
||||||
|
@client.params.privilege_level = "readAndWrite"
|
||||||
|
@AuthorizationManager.assertClientCanViewProject @client, (error) ->
|
||||||
|
expect(error).to.be.null
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "should allow the owner privilegeLevel", (done) ->
|
||||||
|
@client.params.privilege_level = "owner"
|
||||||
|
@AuthorizationManager.assertClientCanViewProject @client, (error) ->
|
||||||
|
expect(error).to.be.null
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "should return an error with any other privilegeLevel", (done) ->
|
||||||
|
@client.params.privilege_level = "unknown"
|
||||||
|
@AuthorizationManager.assertClientCanViewProject @client, (error) ->
|
||||||
|
error.message.should.equal "not authorized"
|
||||||
|
done()
|
|
@ -0,0 +1,60 @@
|
||||||
|
require('chai').should()
|
||||||
|
sinon = require("sinon")
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
path = require "path"
|
||||||
|
modulePath = '../../../app/js/DocumentUpdaterManager'
|
||||||
|
|
||||||
|
describe 'DocumentUpdaterManager', ->
|
||||||
|
beforeEach ->
|
||||||
|
@project_id = "project-id-923"
|
||||||
|
@doc_id = "doc-id-394"
|
||||||
|
@lines = ["one", "two", "three"]
|
||||||
|
@version = 42
|
||||||
|
@settings =
|
||||||
|
apis: documentupdater: url: "http://doc-updater.example.com"
|
||||||
|
|
||||||
|
@DocumentUpdaterManager = SandboxedModule.require modulePath, requires:
|
||||||
|
'settings-sharelatex':@settings
|
||||||
|
'logger-sharelatex': @logger = {log: sinon.stub(), error: sinon.stub()}
|
||||||
|
'request': @request = {}
|
||||||
|
|
||||||
|
describe "getDocument", ->
|
||||||
|
beforeEach ->
|
||||||
|
@callback = sinon.stub()
|
||||||
|
|
||||||
|
describe "successfully", ->
|
||||||
|
beforeEach ->
|
||||||
|
@body = JSON.stringify
|
||||||
|
lines: @lines
|
||||||
|
version: @version
|
||||||
|
ops: @ops = ["mock-op-1", "mock-op-2"]
|
||||||
|
@fromVersion = 2
|
||||||
|
@request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body)
|
||||||
|
@DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback
|
||||||
|
|
||||||
|
it 'should get the document from the document updater', ->
|
||||||
|
url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}?fromVersion=#{@fromVersion}"
|
||||||
|
@request.get.calledWith(url).should.equal true
|
||||||
|
|
||||||
|
it "should call the callback with the lines and version", ->
|
||||||
|
@callback.calledWith(null, @lines, @version, @ops).should.equal true
|
||||||
|
|
||||||
|
describe "when the document updater API returns an error", ->
|
||||||
|
beforeEach ->
|
||||||
|
@request.get = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null)
|
||||||
|
@DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback
|
||||||
|
|
||||||
|
it "should return an error to the callback", ->
|
||||||
|
@callback.calledWith(@error).should.equal true
|
||||||
|
|
||||||
|
describe "when the document updater returns a failure error code", ->
|
||||||
|
beforeEach ->
|
||||||
|
@request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "")
|
||||||
|
@DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback
|
||||||
|
|
||||||
|
it "should return the callback with an error", ->
|
||||||
|
err = new Error("doc updater returned failure status code: 500")
|
||||||
|
err.statusCode = 500
|
||||||
|
@callback
|
||||||
|
.calledWith(err)
|
||||||
|
.should.equal true
|
|
@ -19,10 +19,14 @@ describe 'WebsocketController', ->
|
||||||
}
|
}
|
||||||
@callback = sinon.stub()
|
@callback = sinon.stub()
|
||||||
@client =
|
@client =
|
||||||
|
params: {}
|
||||||
set: sinon.stub()
|
set: sinon.stub()
|
||||||
|
get: (param, cb) -> cb null, @params[param]
|
||||||
join: sinon.stub()
|
join: sinon.stub()
|
||||||
@WebsocketController = SandboxedModule.require modulePath, requires:
|
@WebsocketController = SandboxedModule.require modulePath, requires:
|
||||||
"./WebApiManager": @WebApiManager = {}
|
"./WebApiManager": @WebApiManager = {}
|
||||||
|
"./AuthorizationManager": @AuthorizationManager = {}
|
||||||
|
"./DocumentUpdaterManager": @DocumentUpdaterManager = {}
|
||||||
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
|
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
|
||||||
|
|
||||||
afterEach ->
|
afterEach ->
|
||||||
|
@ -46,6 +50,9 @@ describe 'WebsocketController', ->
|
||||||
.calledWith(@project_id, @user._id)
|
.calledWith(@project_id, @user._id)
|
||||||
.should.equal true
|
.should.equal true
|
||||||
|
|
||||||
|
it "should set the privilege level on the client", ->
|
||||||
|
@client.set.calledWith("privilege_level", @privilegeLevel).should.equal true
|
||||||
|
|
||||||
it "should set the user's id on the client", ->
|
it "should set the user's id on the client", ->
|
||||||
@client.set.calledWith("user_id", @user._id).should.equal true
|
@client.set.calledWith("user_id", @user._id).should.equal true
|
||||||
|
|
||||||
|
@ -85,5 +92,65 @@ describe 'WebsocketController', ->
|
||||||
|
|
||||||
it "should return an error", ->
|
it "should return an error", ->
|
||||||
@callback
|
@callback
|
||||||
.calledWith({message: "not authorized"})
|
.calledWith(new Error("not authorized"))
|
||||||
.should.equal true
|
.should.equal true
|
||||||
|
|
||||||
|
describe "joinDoc", ->
|
||||||
|
beforeEach ->
|
||||||
|
@doc_id = "doc-id-123"
|
||||||
|
@doc_lines = ["doc", "lines"]
|
||||||
|
@version = 42
|
||||||
|
@ops = ["mock", "ops"]
|
||||||
|
|
||||||
|
@client.params.project_id = @project_id
|
||||||
|
|
||||||
|
@AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null)
|
||||||
|
@DocumentUpdaterManager.getDocument = sinon.stub().callsArgWith(3, null, @doc_lines, @version, @ops)
|
||||||
|
|
||||||
|
describe "with a fromVersion", ->
|
||||||
|
beforeEach ->
|
||||||
|
@fromVersion = 40
|
||||||
|
@WebsocketController.joinDoc @client, @doc_id, @fromVersion, @callback
|
||||||
|
|
||||||
|
it "should check that the client is authorized to view the project", ->
|
||||||
|
@AuthorizationManager.assertClientCanViewProject
|
||||||
|
.calledWith(@client)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should get the document from the DocumentUpdaterManager", ->
|
||||||
|
@DocumentUpdaterManager.getDocument
|
||||||
|
.calledWith(@project_id, @doc_id, @fromVersion)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should join the client to room for the doc_id", ->
|
||||||
|
@client.join
|
||||||
|
.calledWith(@doc_id)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should call the callback with the lines, version and ops", ->
|
||||||
|
@callback
|
||||||
|
.calledWith(null, @doc_lines, @version, @ops)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
describe "with doclines that need escaping", ->
|
||||||
|
beforeEach ->
|
||||||
|
@doc_lines.push ["räksmörgås"]
|
||||||
|
@WebsocketController.joinDoc @client, @doc_id, -1, @callback
|
||||||
|
|
||||||
|
it "should call the callback with the escaped lines", ->
|
||||||
|
escaped_lines = @callback.args[0][1]
|
||||||
|
escaped_word = escaped_lines.pop()
|
||||||
|
escaped_word.should.equal 'räksmörgås'
|
||||||
|
# Check that unescaping works
|
||||||
|
decodeURIComponent(escape(escaped_word)).should.equal "räksmörgås"
|
||||||
|
|
||||||
|
describe "when not authorized", ->
|
||||||
|
beforeEach ->
|
||||||
|
@AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, @err = new Error("not authorized"))
|
||||||
|
@WebsocketController.joinDoc @client, @doc_id, -1, @callback
|
||||||
|
|
||||||
|
it "should call the callback with an error", ->
|
||||||
|
@callback.calledWith(@err).should.equal true
|
||||||
|
|
||||||
|
it "should not call the DocumentUpdaterManager", ->
|
||||||
|
@DocumentUpdaterManager.getDocument.called.should.equal false
|
Loading…
Reference in a new issue