From 0b18edeff3874f8f6972dfef53f37efd575db57e Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 13 Nov 2014 11:48:49 +0000 Subject: [PATCH] Add in /clients and /client/:client_id status end points --- .../app/coffee/HttpController.coffee | 37 +++++++++++++++++++ services/real-time/app/coffee/Router.coffee | 7 +++- services/real-time/app/coffee/Utils.coffee | 14 +++++++ .../app/coffee/WebsocketController.coffee | 5 ++- services/real-time/package.json | 1 + .../acceptance/coffee/JoinProjectTests.coffee | 10 +++++ .../acceptance/coffee/SessionTests.coffee | 36 ++++++++++++++++-- .../coffee/helpers/RealTimeClient.coffee | 15 ++++++++ .../coffee/WebsocketControllerTests.coffee | 3 ++ 9 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 services/real-time/app/coffee/HttpController.coffee create mode 100644 services/real-time/app/coffee/Utils.coffee diff --git a/services/real-time/app/coffee/HttpController.coffee b/services/real-time/app/coffee/HttpController.coffee new file mode 100644 index 0000000000..91f8df0df8 --- /dev/null +++ b/services/real-time/app/coffee/HttpController.coffee @@ -0,0 +1,37 @@ +Utils = require "./Utils" +async = require "async" + +module.exports = HttpController = + # The code in this controller is hard to unit test because of a lot of + # dependencies on internal socket.io methods. It is not critical to the running + # of ShareLaTeX, and is only used for getting stats about connected clients, + # and for checking internal state in acceptance tests. The acceptances tests + # should provide appropriate coverage. + _getConnectedClientView: (ioClient, callback = (error, client) ->) -> + client_id = ioClient.id + Utils.getClientAttributes ioClient, [ + "project_id", "user_id", "first_name", "last_name", "email", "connected_time" + ], (error, {project_id, user_id, first_name, last_name, email, connected_time}) -> + return callback(error) if error? + client = {client_id, project_id, user_id, first_name, last_name, email, connected_time} + client.rooms = [] + for name, joined of ioClient.manager.roomClients[client_id] + if joined and name != "" + client.rooms.push name.replace(/^\//, "") # Remove leading / + callback(null, client) + + getConnectedClients: (req, res, next) -> + io = req.app.get("io") + ioClients = io.sockets.clients() + async.map ioClients, HttpController._getConnectedClientView, (error, clients) -> + return next(error) if error? + res.json clients + + getConnectedClient: (req, res, next) -> + {client_id} = req.params + io = req.app.get("io") + ioClient = io.sockets.socket(client_id) + HttpController._getConnectedClientView ioClient, (error, client) -> + return next(error) if error? + res.json client + \ 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 ac0d06f8c3..b3626f4617 100644 --- a/services/real-time/app/coffee/Router.coffee +++ b/services/real-time/app/coffee/Router.coffee @@ -1,6 +1,7 @@ Metrics = require "metrics-sharelatex" logger = require "logger-sharelatex" WebsocketController = require "./WebsocketController" +HttpController = require "./HttpController" module.exports = Router = # We don't want to send raw errors back to the client, in case they @@ -71,4 +72,8 @@ module.exports = Router = # 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 + callback(null, args...) + + app.set("io", io) + app.get "/clients", HttpController.getConnectedClients + app.get "/clients/:client_id", HttpController.getConnectedClient \ No newline at end of file diff --git a/services/real-time/app/coffee/Utils.coffee b/services/real-time/app/coffee/Utils.coffee new file mode 100644 index 0000000000..72dada30a3 --- /dev/null +++ b/services/real-time/app/coffee/Utils.coffee @@ -0,0 +1,14 @@ +async = require "async" + +module.exports = Utils = + getClientAttributes: (client, keys, callback = (error, attributes) ->) -> + attributes = {} + jobs = keys.map (key) -> + (callback) -> + client.get key, (error, value) -> + return callback(error) if error? + attributes[key] = value + callback() + async.series jobs, (error) -> + return callback(error) if error? + callback null, attributes \ 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 2f57aa4005..cd0e2a14dd 100644 --- a/services/real-time/app/coffee/WebsocketController.coffee +++ b/services/real-time/app/coffee/WebsocketController.coffee @@ -19,6 +19,8 @@ module.exports = WebsocketController = err = new Error("not authorized") logger.error {err, project_id, user_id, client_id: client.id}, "user is not authorized to join project" return callback(err) + + client.join project_id client.set("privilege_level", privilegeLevel) client.set("user_id", user_id) @@ -67,5 +69,4 @@ module.exports = WebsocketController = _getClientData: (client, callback = (error, data) ->) -> client.get "user_id", (error, user_id) -> client.get "project_id", (error, project_id) -> - callback null, {client_id: client.id, project_id, user_id} - \ No newline at end of file + callback null, {client_id: client.id, project_id, user_id} \ No newline at end of file diff --git a/services/real-time/package.json b/services/real-time/package.json index 5bc6c6b90e..5f9c323bb3 100644 --- a/services/real-time/package.json +++ b/services/real-time/package.json @@ -8,6 +8,7 @@ "url": "https://github.com/sharelatex/real-time-sharelatex.git" }, "dependencies": { + "async": "^0.9.0", "connect-redis": "^2.1.0", "express": "^4.10.1", "express-session": "^1.9.1", diff --git a/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee b/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee index 02be863b9c..36cae2b68a 100644 --- a/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee +++ b/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee @@ -41,6 +41,11 @@ describe "joinProject", -> it "should return the protocolVersion", -> @protocolVersion.should.equal 2 + it "should have joined the project room", (done) -> + RealTimeClient.getConnectedClient @client.socket.sessionid, (error, client) => + expect(@project_id in client.rooms).to.equal true + done() + describe "when not authorized", -> before (done) -> FixturesManager.setUpProject { @@ -60,3 +65,8 @@ describe "joinProject", -> it "should return an error", -> # We don't return specific errors @error.message.should.equal "Something went wrong" + + it "should not have joined the project room", (done) -> + RealTimeClient.getConnectedClient @client.socket.sessionid, (error, client) => + expect(@project_id in client.rooms).to.equal false + done() diff --git a/services/real-time/test/acceptance/coffee/SessionTests.coffee b/services/real-time/test/acceptance/coffee/SessionTests.coffee index 2eb0d8206a..618635db58 100644 --- a/services/real-time/test/acceptance/coffee/SessionTests.coffee +++ b/services/real-time/test/acceptance/coffee/SessionTests.coffee @@ -5,7 +5,7 @@ RealTimeClient = require "./helpers/RealTimeClient" describe "Session", -> describe "with an established session", -> - beforeEach (done) -> + before (done) -> @user_id = "mock-user-id" RealTimeClient.setSession { user: { _id: @user_id } @@ -22,9 +22,19 @@ describe "Session", -> expect(disconnected).to.equal false done() , 500 + + it "should appear in the list of connected clients", (done) -> + RealTimeClient.getConnectedClients (error, clients) => + included = false + for client in clients + if client.client_id == @client.socket.sessionid + included = true + break + expect(included).to.equal true + done() describe "without an established session", -> - beforeEach (done) -> + before (done) -> RealTimeClient.unsetSession (error) => throw error if error? @client = RealTimeClient.connect() @@ -34,8 +44,18 @@ describe "Session", -> @client.on "disconnect", () -> done() + it "not should appear in the list of connected clients", (done) -> + RealTimeClient.getConnectedClients (error, clients) => + included = false + for client in clients + if client.client_id == @client.socket.sessionid + included = true + break + expect(included).to.equal false + done() + describe "without a valid user set on the session", -> - beforeEach (done) -> + before (done) -> RealTimeClient.setSession { foo: "bar" }, (error) => @@ -45,4 +65,14 @@ describe "Session", -> it "should get disconnected", (done) -> @client.on "disconnect", () -> + done() + + it "not should appear in the list of connected clients", (done) -> + RealTimeClient.getConnectedClients (error, clients) => + included = false + for client in clients + if client.client_id == @client.socket.sessionid + included = true + break + expect(included).to.equal false done() \ No newline at end of file diff --git a/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.coffee b/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.coffee index 415f14fe5e..52b869f862 100644 --- a/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.coffee +++ b/services/real-time/test/acceptance/coffee/helpers/RealTimeClient.coffee @@ -1,6 +1,7 @@ XMLHttpRequest = require("../../libs/XMLHttpRequest").XMLHttpRequest io = require("socket.io-client") +request = require "request" Settings = require "settings-sharelatex" redis = require "redis-sharelatex" rclient = redis.createClient(Settings.redis.web) @@ -37,4 +38,18 @@ module.exports = Client = connect: (cookie) -> client = io.connect("http://localhost:3026", 'force new connection': true) return client + + getConnectedClients: (callback = (error, clients) ->) -> + request.get { + url: "http://localhost:3026/clients" + json: true + }, (error, response, data) -> + callback error, data + + getConnectedClient: (client_id, callback = (error, clients) ->) -> + request.get { + url: "http://localhost:3026/clients/#{client_id}" + json: true + }, (error, response, data) -> + callback error, data diff --git a/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee b/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee index 3be89d26e9..0a0fc36dcb 100644 --- a/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee +++ b/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee @@ -51,6 +51,9 @@ describe 'WebsocketController', -> .calledWith(@project_id, @user._id) .should.equal true + it "should join the project room", -> + @client.join.calledWith(@project_id).should.equal true + it "should set the privilege level on the client", -> @client.set.calledWith("privilege_level", @privilegeLevel).should.equal true