Răsfoiți Sursa

Add appltOtUpdate end point (sans acceptance tests for now)

James Allen 12 ani în urmă
părinte
comite
f0e69bfe2d

+ 4 - 1
services/real-time/app/coffee/AuthorizationManager.coffee

@@ -1,7 +1,10 @@
 module.exports = AuthorizationManager =
 module.exports = AuthorizationManager =
 	assertClientCanViewProject: (client, callback = (error) ->) ->
 	assertClientCanViewProject: (client, callback = (error) ->) ->
 		AuthorizationManager._assertClientHasPrivilegeLevel client, ["readOnly", "readAndWrite", "owner"], callback
 		AuthorizationManager._assertClientHasPrivilegeLevel client, ["readOnly", "readAndWrite", "owner"], callback
-		
+
+	assertClientCanEditProject: (client, callback = (error) ->) ->
+		AuthorizationManager._assertClientHasPrivilegeLevel client, ["readAndWrite", "owner"], callback
+				
 	_assertClientHasPrivilegeLevel: (client, allowedLevels, callback = (error) ->) ->
 	_assertClientHasPrivilegeLevel: (client, allowedLevels, callback = (error) ->) ->
 		client.get "privilege_level", (error, privilegeLevel) ->
 		client.get "privilege_level", (error, privilegeLevel) ->
 			return callback(error) if error?
 			return callback(error) if error?

+ 15 - 1
services/real-time/app/coffee/DocumentUpdaterManager.coffee

@@ -2,6 +2,9 @@ request = require "request"
 logger = require "logger-sharelatex"
 logger = require "logger-sharelatex"
 settings = require "settings-sharelatex"
 settings = require "settings-sharelatex"
 
 
+redis = require("redis-sharelatex")
+rclient = redis.createClient(settings.redis.web)
+
 module.exports = DocumentUpdaterManager =
 module.exports = DocumentUpdaterManager =
 	getDocument: (project_id, doc_id, fromVersion, callback = (error, exists, doclines, version) ->) ->
 	getDocument: (project_id, doc_id, fromVersion, callback = (error, exists, doclines, version) ->) ->
 		#timer = new metrics.Timer("get-document")
 		#timer = new metrics.Timer("get-document")
@@ -23,4 +26,15 @@ module.exports = DocumentUpdaterManager =
 				err = new Error("doc updater returned a non-success status code: #{res.statusCode}")
 				err = new Error("doc updater returned a non-success status code: #{res.statusCode}")
 				err.statusCode = res.statusCode
 				err.statusCode = res.statusCode
 				logger.error {err, project_id, doc_id, url}, "doc updater returned a non-success status code: #{res.statusCode}"
 				logger.error {err, project_id, doc_id, url}, "doc updater returned a non-success status code: #{res.statusCode}"
-				callback err
+				callback err
+				
+	queueChange: (project_id, doc_id, change, callback = ()->)->
+		jsonChange = JSON.stringify change
+		doc_key = "#{project_id}:#{doc_id}"
+		multi = rclient.multi()
+		multi.rpush "PendingUpdates:#{doc_id}", jsonChange
+		multi.sadd  "DocsWithPendingUpdates", doc_key
+		multi.rpush "pending-updates-list", doc_key
+		multi.exec (error) ->
+			return callback(error) if error?
+			callback()

+ 24 - 0
services/real-time/app/coffee/WebsocketController.coffee

@@ -109,3 +109,27 @@ module.exports = WebsocketController =
 				ConnectedUsersManager.getConnectedUsers project_id, (error, users) ->
 				ConnectedUsersManager.getConnectedUsers project_id, (error, users) ->
 					return callback(error) if error?
 					return callback(error) if error?
 					callback null, users
 					callback null, users
+
+	applyOtUpdate: (client, project_id, doc_id, update, callback = (error) ->) ->
+		AuthorizationManager.assertClientCanEditProject client, (error) ->
+			if error?
+				logger.error {err: error, project_id, doc_id, client_id: client.id, version: update.v}, "client is not authorized to make update"
+				client.disconnect()
+				return callback(error)
+			
+			Utils.getClientAttributes client, ["user_id"], (error, {user_id}) ->
+				return callback(error) if error?
+				update.meta ||= {}
+				update.meta.source = client.id
+				update.meta.user_id = user_id
+				# metrics.inc "editor.doc-update", 0.3
+				# metrics.set "editor.active-projects", project_id, 0.3
+				# metrics.set "editor.active-users", user_id, 0.3
+
+				logger.log {user_id, doc_id, project_id, client_id: client.id, version: update.v}, "sending update to doc updater"
+
+				DocumentUpdaterManager.queueChange project_id, doc_id, update, (error) ->
+					if error?
+						logger.error {err: error, project_id, doc_id, client_id: client.id, version: update.v}, "document was not available for update"
+						client.disconnect()
+					callback(error)

+ 12 - 0
services/real-time/test/acceptance/coffee/ApplyUpdateTests.coffee

@@ -0,0 +1,12 @@
+describe "applyOtUpdate", ->
+	describe "when authorized", ->
+		it "should publish a message on redis"
+		
+		it "should add the doc to the pending updates set in redis"
+		
+		it "should push the update into redis"
+		
+	describe "when not authorized", ->
+		it "should return an error"
+		
+		it "should disconnect the client"

+ 25 - 0
services/real-time/test/unit/coffee/AuthorizationManagerTests.coffee

@@ -35,5 +35,30 @@ describe 'AuthorizationManager', ->
 		it "should return an error with any other privilegeLevel", (done) ->
 		it "should return an error with any other privilegeLevel", (done) ->
 			@client.params.privilege_level = "unknown"
 			@client.params.privilege_level = "unknown"
 			@AuthorizationManager.assertClientCanViewProject @client, (error) ->
 			@AuthorizationManager.assertClientCanViewProject @client, (error) ->
+				error.message.should.equal "not authorized"
+				done()
+
+	describe "assertClientCanEditProject", ->
+		it "should not allow the readOnly privilegeLevel", (done) ->
+			@client.params.privilege_level = "readOnly"
+			@AuthorizationManager.assertClientCanEditProject @client, (error) ->
+				error.message.should.equal "not authorized"
+				done()
+	
+		it "should allow the readAndWrite privilegeLevel", (done) ->
+			@client.params.privilege_level = "readAndWrite"
+			@AuthorizationManager.assertClientCanEditProject @client, (error) ->
+				expect(error).to.be.null
+				done()
+				
+		it "should allow the owner privilegeLevel", (done) ->
+			@client.params.privilege_level = "owner"
+			@AuthorizationManager.assertClientCanEditProject @client, (error) ->
+				expect(error).to.be.null
+				done()
+				
+		it "should return an error with any other privilegeLevel", (done) ->
+			@client.params.privilege_level = "unknown"
+			@AuthorizationManager.assertClientCanEditProject @client, (error) ->
 				error.message.should.equal "not authorized"
 				error.message.should.equal "not authorized"
 				done()
 				done()

+ 43 - 0
services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.coffee

@@ -12,11 +12,14 @@ describe 'DocumentUpdaterManager', ->
 		@version = 42
 		@version = 42
 		@settings = 
 		@settings = 
 			apis: documentupdater: url: "http://doc-updater.example.com"
 			apis: documentupdater: url: "http://doc-updater.example.com"
+			redis: web: {}
+		@rclient = {auth:->}
 			
 			
 		@DocumentUpdaterManager = SandboxedModule.require modulePath, requires:
 		@DocumentUpdaterManager = SandboxedModule.require modulePath, requires:
 			'settings-sharelatex':@settings
 			'settings-sharelatex':@settings
 			'logger-sharelatex': @logger = {log: sinon.stub(), error: sinon.stub()}
 			'logger-sharelatex': @logger = {log: sinon.stub(), error: sinon.stub()}
 			'request': @request = {}
 			'request': @request = {}
+			'redis-sharelatex' : createClient: () => @rclient
 
 
 	describe "getDocument", ->
 	describe "getDocument", ->
 		beforeEach ->
 		beforeEach ->
@@ -58,3 +61,43 @@ describe 'DocumentUpdaterManager', ->
 				@callback
 				@callback
 					.calledWith(err)
 					.calledWith(err)
 					.should.equal true
 					.should.equal true
+
+	describe 'queueChange', ->
+		beforeEach ->
+			@change = {
+				"action":"removeText",
+				"range":{"start":{"row":2,"column":2},"end":{"row":2,"column":3}},
+				"text":"e"
+			}
+			@rclient.multi = sinon.stub().returns @rclient
+			@rclient.exec = sinon.stub().callsArg(0)
+			@rclient.rpush = sinon.stub()
+			@rclient.sadd = sinon.stub()
+			@callback = sinon.stub()
+
+		describe "successfully", ->
+			beforeEach ->
+				@DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback)
+
+			it "should push the change", ->
+				@rclient.rpush
+					.calledWith("PendingUpdates:#{@doc_id}", JSON.stringify(@change))
+					.should.equal true
+
+			it "should notify the doc updater of the change via the pending-updates-list queue", ->
+				@rclient.rpush
+					.calledWith("pending-updates-list", "#{@project_id}:#{@doc_id}")
+					.should.equal true
+
+			it "should push the doc id into the pending updates set", ->
+				@rclient.sadd
+					.calledWith("DocsWithPendingUpdates", "#{@project_id}:#{@doc_id}")
+					.should.equal true
+
+		describe "with error connecting to redis during exec", ->
+			beforeEach ->
+				@rclient.exec = sinon.stub().callsArgWith(0, new Error("something went wrong"))
+				@DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback)
+
+			it "should return an error", ->
+				@callback.calledWithExactly(sinon.match(Error)).should.equal true

+ 68 - 2
services/real-time/test/unit/coffee/WebsocketControllerTests.coffee

@@ -10,7 +10,7 @@ describe 'WebsocketController', ->
 		tk.freeze(new Date())
 		tk.freeze(new Date())
 		@project_id = "project-id-123"
 		@project_id = "project-id-123"
 		@user = {
 		@user = {
-			_id: "user-id-123"
+			_id: @user_id = "user-id-123"
 			first_name: "James"
 			first_name: "James"
 			last_name: "Allen"
 			last_name: "Allen"
 			email: "james@example.com"
 			email: "james@example.com"
@@ -19,6 +19,7 @@ describe 'WebsocketController', ->
 		}
 		}
 		@callback = sinon.stub()
 		@callback = sinon.stub()
 		@client =
 		@client =
+			id: @client_id = "mock-client-id-123"
 			params: {}
 			params: {}
 			set: sinon.stub()
 			set: sinon.stub()
 			get: (param, cb) -> cb null, @params[param]
 			get: (param, cb) -> cb null, @params[param]
@@ -286,4 +287,69 @@ describe 'WebsocketController', ->
 				
 				
 			it "should not send cursor data to the connected user manager", (done)->
 			it "should not send cursor data to the connected user manager", (done)->
 				@ConnectedUsersManager.updateUserPosition.called.should.equal false
 				@ConnectedUsersManager.updateUserPosition.called.should.equal false
-				done()
+				done()
+
+	describe "applyOtUpdate", ->
+		beforeEach ->
+			@update = {op: {p: 12, t: "foo"}}
+			@client.params.user_id = @user_id
+			@AuthorizationManager.assertClientCanEditProject = sinon.stub().callsArg(1)
+			@DocumentUpdaterManager.queueChange = sinon.stub().callsArg(3)
+
+		describe "succesfully", ->
+			beforeEach ->
+				@WebsocketController.applyOtUpdate @client, @project_id, @doc_id, @update, @callback
+
+			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 queue the update", ->
+				@DocumentUpdaterManager.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()
+				@DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, @error = new Error("Something went wrong"))
+				@WebsocketController.applyOtUpdate @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
+
+			it "should call the callback with the error", ->
+				@callback.calledWith(@error).should.equal true
+				
+		describe "when not authorized", ->
+			beforeEach ->
+				@client.disconnect = sinon.stub()
+				@AuthorizationManager.assertClientCanEditProject = sinon.stub().callsArgWith(1, @error = new Error("not authorized"))
+				@WebsocketController.applyOtUpdate @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
+
+			it "should call the callback with the error", ->
+				@callback.calledWith(@error).should.equal true
+