diff --git a/services/real-time/app/coffee/AuthorizationManager.coffee b/services/real-time/app/coffee/AuthorizationManager.coffee new file mode 100644 index 0000000000..345bfa4804 --- /dev/null +++ b/services/real-time/app/coffee/AuthorizationManager.coffee @@ -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") \ No newline at end of file diff --git a/services/real-time/app/coffee/DocumentUpdaterManager.coffee b/services/real-time/app/coffee/DocumentUpdaterManager.coffee new file mode 100644 index 0000000000..54ba6f9072 --- /dev/null +++ b/services/real-time/app/coffee/DocumentUpdaterManager.coffee @@ -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 \ No newline at end of file diff --git a/services/real-time/app/coffee/Router.coffee b/services/real-time/app/coffee/Router.coffee index 23108921b7..06cd7bb7a0 100644 --- a/services/real-time/app/coffee/Router.coffee +++ b/services/real-time/app/coffee/Router.coffee @@ -3,6 +3,23 @@ logger = require "logger-sharelatex" WebsocketController = require "./WebsocketController" 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) -> session.on 'connection', (error, client, session) -> if error? @@ -12,7 +29,7 @@ module.exports = Router = Metrics.inc('socket-io.connection') - logger.log session: session, "got session" + logger.log session: session, client_id: client.id, "client connected" user = session.user if !user? or !user._id? @@ -21,5 +38,28 @@ module.exports = Router = return 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...) \ No newline at end of file diff --git a/services/real-time/app/coffee/WebsocketController.coffee b/services/real-time/app/coffee/WebsocketController.coffee index 41ce2d7464..f409f39375 100644 --- a/services/real-time/app/coffee/WebsocketController.coffee +++ b/services/real-time/app/coffee/WebsocketController.coffee @@ -1,5 +1,7 @@ logger = require "logger-sharelatex" WebApiManager = require "./WebApiManager" +AuthorizationManager = require "./AuthorizationManager" +DocumentUpdaterManager = require "./DocumentUpdaterManager" module.exports = WebsocketController = # 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) ->) -> 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) -> return callback(error) if error? if !privilegeLevel or privilegeLevel == "" err = new Error("not authorized") - logger.error {err, project_id, user_id}, "user is not authorized to join project" - # Don't send an error object since socket.io can apparently - # only serialize JSON. - return callback({message: err.message}) + logger.error {err, project_id, user_id, client_id: client.id}, "user is not authorized to join project" + return callback(err) + client.set("privilege_level", privilegeLevel) client.set("user_id", user_id) client.set("project_id", project_id) client.set("owner_id", project?.owner?._id) @@ -30,4 +31,30 @@ module.exports = WebsocketController = client.set("signup_date", user?.signUpDate) client.set("login_count", user?.loginCount) - callback null, project, privilegeLevel, WebsocketController.PROTOCOL_VERSION \ No newline at end of file + 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 + \ No newline at end of file diff --git a/services/real-time/config/settings.defaults.coffee b/services/real-time/config/settings.defaults.coffee index f1b46c4638..7d9ddf184c 100644 --- a/services/real-time/config/settings.defaults.coffee +++ b/services/real-time/config/settings.defaults.coffee @@ -13,6 +13,8 @@ module.exports = apis: web: url: "http://localhost:3000" + documentupdater: + url: "http://localhost:3003" security: sessionSecret: "secret-please-change" diff --git a/services/real-time/test/acceptance/coffee/JoinDocTests.coffee b/services/real-time/test/acceptance/coffee/JoinDocTests.coffee new file mode 100644 index 0000000000..a339cf7ef8 --- /dev/null +++ b/services/real-time/test/acceptance/coffee/JoinDocTests.coffee @@ -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] \ No newline at end of file diff --git a/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee b/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee index 84f61c6476..02be863b9c 100644 --- a/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee +++ b/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee @@ -3,33 +3,30 @@ expect = chai.expect chai.should() RealTimeClient = require "./helpers/RealTimeClient" -MockWebClient = require "./helpers/MockWebClient" +MockWebServer = require "./helpers/MockWebServer" +FixturesManager = require "./helpers/FixturesManager" + describe "joinProject", -> describe "when authorized", -> before (done) -> - @user_id = "mock-user-id" - @project_id = "mock-project-id" - privileges = {} - privileges[@user_id] = "owner" - MockWebClient.createMockProject(@project_id, privileges, { - name: "Test Project" - }) - MockWebClient.run (error) => + FixturesManager.setUpProject { + privilegeLevel: "owner" + project: { + name: "Test Project" + } + }, (error, data) => throw error if error? - RealTimeClient.setSession { - user: { _id: @user_id } - }, (error) => + {@user_id, @project_id} = data + @client = RealTimeClient.connect() + @client.emit "joinProject", { + project_id: @project_id + }, (error, @project, @privilegeLevel, @protocolVersion) => throw error if error? - @client = RealTimeClient.connect() - @client.emit "joinProject", { - project_id: @project_id - }, (error, @project, @privilegeLevel, @protocolVersion) => - throw error if error? - done() + done() it "should get the project from web", -> - MockWebClient.joinProject + MockWebServer.joinProject .calledWith(@project_id, @user_id) .should.equal true @@ -46,23 +43,20 @@ describe "joinProject", -> describe "when not authorized", -> before (done) -> - @user_id = "mock-user-id-2" - @project_id = "mock-project-id-2" - privileges = {} - MockWebClient.createMockProject(@project_id, privileges, { - name: "Test Project" - }) - MockWebClient.run (error) => + FixturesManager.setUpProject { + privilegeLevel: null + project: { + name: "Test Project" + } + }, (error, data) => throw error if error? - RealTimeClient.setSession { - user: { _id: @user_id } - }, (error) => - throw error if error? - @client = RealTimeClient.connect() - @client.emit "joinProject", { - project_id: @project_id - }, (@error, @project, @privilegeLevel, @protocolVersion) => - done() + {@user_id, @project_id} = data + @client = RealTimeClient.connect() + @client.emit "joinProject", { + project_id: @project_id + }, (@error, @project, @privilegeLevel, @protocolVersion) => + done() it "should return an error", -> - @error.message.should.equal "not authorized" + # We don't return specific errors + @error.message.should.equal "Something went wrong" diff --git a/services/real-time/test/acceptance/coffee/helpers/FixturesManager.coffee b/services/real-time/test/acceptance/coffee/helpers/FixturesManager.coffee new file mode 100644 index 0000000000..5c2eb5fd3a --- /dev/null +++ b/services/real-time/test/acceptance/coffee/helpers/FixturesManager.coffee @@ -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) + \ No newline at end of file diff --git a/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.coffee b/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.coffee new file mode 100644 index 0000000000..094d10aceb --- /dev/null +++ b/services/real-time/test/acceptance/coffee/helpers/MockDocUpdaterServer.coffee @@ -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" \ No newline at end of file diff --git a/services/real-time/test/acceptance/coffee/helpers/MockWebClient.coffee b/services/real-time/test/acceptance/coffee/helpers/MockWebServer.coffee similarity index 58% rename from services/real-time/test/acceptance/coffee/helpers/MockWebClient.coffee rename to services/real-time/test/acceptance/coffee/helpers/MockWebServer.coffee index b7c4ead1fc..2fff23e252 100644 --- a/services/real-time/test/acceptance/coffee/helpers/MockWebClient.coffee +++ b/services/real-time/test/acceptance/coffee/helpers/MockWebServer.coffee @@ -1,25 +1,25 @@ sinon = require "sinon" express = require "express" -module.exports = MockWebClient = +module.exports = MockWebServer = projects: {} privileges: {} createMockProject: (project_id, privileges, project) -> - MockWebClient.privileges[project_id] = privileges - MockWebClient.projects[project_id] = project + MockWebServer.privileges[project_id] = privileges + MockWebServer.projects[project_id] = project joinProject: (project_id, user_id, callback = (error, project, privilegeLevel) ->) -> callback( null, - MockWebClient.projects[project_id], - MockWebClient.privileges[project_id][user_id] + MockWebServer.projects[project_id], + MockWebServer.privileges[project_id][user_id] ) joinProjectRequest: (req, res, next) -> {project_id} = req.params {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? res.json { project: project @@ -28,12 +28,12 @@ module.exports = MockWebClient = running: false run: (callback = (error) ->) -> - if MockWebClient.running + if MockWebServer.running return callback() app = express() - app.post "/project/:project_id/join", MockWebClient.joinProjectRequest + app.post "/project/:project_id/join", MockWebServer.joinProjectRequest app.listen 3000, (error) -> - MockWebClient.running = true + MockWebServer.running = true callback(error) -sinon.spy MockWebClient, "joinProject" \ No newline at end of file +sinon.spy MockWebServer, "joinProject" \ No newline at end of file diff --git a/services/real-time/test/unit/coffee/AuthorizationManagerTests.coffee b/services/real-time/test/unit/coffee/AuthorizationManagerTests.coffee new file mode 100644 index 0000000000..cd3822538b --- /dev/null +++ b/services/real-time/test/unit/coffee/AuthorizationManagerTests.coffee @@ -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() \ No newline at end of file diff --git a/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.coffee b/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.coffee new file mode 100644 index 0000000000..9f80981374 --- /dev/null +++ b/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.coffee @@ -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 diff --git a/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee b/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee index cec6c0fc6b..eea0e7e1f0 100644 --- a/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee +++ b/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee @@ -19,10 +19,14 @@ describe 'WebsocketController', -> } @callback = sinon.stub() @client = + params: {} set: sinon.stub() + get: (param, cb) -> cb null, @params[param] join: sinon.stub() @WebsocketController = SandboxedModule.require modulePath, requires: "./WebApiManager": @WebApiManager = {} + "./AuthorizationManager": @AuthorizationManager = {} + "./DocumentUpdaterManager": @DocumentUpdaterManager = {} "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } afterEach -> @@ -46,6 +50,9 @@ describe 'WebsocketController', -> .calledWith(@project_id, @user._id) .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", -> @client.set.calledWith("user_id", @user._id).should.equal true @@ -85,5 +92,65 @@ describe 'WebsocketController', -> it "should return an error", -> @callback - .calledWith({message: "not authorized"}) - .should.equal true \ No newline at end of file + .calledWith(new Error("not authorized")) + .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 \ No newline at end of file