From d68ed0efdfa69ed81c779c07b4ade03b22484211 Mon Sep 17 00:00:00 2001 From: June Kelly Date: Tue, 30 May 2023 13:21:55 +0100 Subject: [PATCH] Merge pull request #13140 from overleaf/jk-real-time-disconnect-link-sharing [real-time] Disconnect relevant users when link-sharing is turned off GitOrigin-RevId: cf44a30a235717b658a759e8a74ae4d0e5abae47 --- services/real-time/app/js/WebApiManager.js | 12 +- .../real-time/app/js/WebsocketController.js | 8 +- .../real-time/app/js/WebsocketLoadBalancer.js | 22 +- .../acceptance/js/ReceiveEditorEventTests.js | 191 ++++++++++++++++-- .../acceptance/js/helpers/FixturesManager.js | 11 +- .../acceptance/js/helpers/MockWebServer.js | 19 +- .../test/unit/js/WebApiManagerTests.js | 8 +- .../test/unit/js/WebsocketControllerTests.js | 47 +++-- .../unit/js/WebsocketLoadBalancerTests.js | 119 +++++++++-- .../Features/Editor/EditorHttpController.js | 13 +- services/web/frontend/js/ide.js | 7 + .../test/acceptance/src/TokenAccessTests.js | 15 ++ .../src/Editor/EditorHttpControllerTests.js | 33 +++ 13 files changed, 424 insertions(+), 81 deletions(-) diff --git a/services/real-time/app/js/WebApiManager.js b/services/real-time/app/js/WebApiManager.js index 953f1ec401..e33f05d418 100644 --- a/services/real-time/app/js/WebApiManager.js +++ b/services/real-time/app/js/WebApiManager.js @@ -40,12 +40,12 @@ module.exports = { if (!(data && data.project)) { return callback(new CorruptedJoinProjectResponseError()) } - callback( - null, - data.project, - data.privilegeLevel, - data.isRestrictedUser - ) + const userMetadata = { + isRestrictedUser: data.isRestrictedUser, + isTokenMember: data.isTokenMember, + isInvitedMember: data.isInvitedMember, + } + callback(null, data.project, data.privilegeLevel, userMetadata) } else if (response.statusCode === 429) { callback( new CodedError( diff --git a/services/real-time/app/js/WebsocketController.js b/services/real-time/app/js/WebsocketController.js index a10745becc..eeaf4fe9ba 100644 --- a/services/real-time/app/js/WebsocketController.js +++ b/services/real-time/app/js/WebsocketController.js @@ -43,7 +43,7 @@ module.exports = WebsocketController = { WebApiManager.joinProject( projectId, user, - function (error, project, privilegeLevel, isRestrictedUser) { + function (error, project, privilegeLevel, userMetadata) { if (error) { return callback(error) } @@ -73,7 +73,9 @@ module.exports = WebsocketController = { client.ol_context.connected_time = new Date() client.ol_context.signup_date = user.signUpDate client.ol_context.login_count = user.loginCount - client.ol_context.is_restricted_user = !!isRestrictedUser + client.ol_context.is_restricted_user = !!userMetadata.isRestrictedUser + client.ol_context.is_token_member = !!userMetadata.isTokenMember + client.ol_context.is_invited_member = !!userMetadata.isInvitedMember RoomManager.joinProject(client, projectId, function (err) { if (err) { @@ -85,7 +87,7 @@ module.exports = WebsocketController = { projectId, clientId: client.id, privilegeLevel, - isRestrictedUser, + userMetadata, }, 'user joined project' ) diff --git a/services/real-time/app/js/WebsocketLoadBalancer.js b/services/real-time/app/js/WebsocketLoadBalancer.js index d2cccab46d..294fe294a0 100644 --- a/services/real-time/app/js/WebsocketLoadBalancer.js +++ b/services/real-time/app/js/WebsocketLoadBalancer.js @@ -31,6 +31,14 @@ module.exports = WebsocketLoadBalancer = { if (message?.payload?.includes(userId)) { return true } + } else if (message?.message === 'project:publicAccessLevel:changed') { + const [info] = message.payload + if ( + info.newAccessLevel === 'private' && + !client.ol_context.is_invited_member + ) { + return true + } } return false }, @@ -139,12 +147,7 @@ module.exports = WebsocketLoadBalancer = { !RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST.includes(message.message) // send messages only to unique clients (due to duplicate entries in io.sockets.clients) - const clientList = io.sockets - .clients(message.room_id) - .filter( - client => - !(isRestrictedMessage && client.ol_context.is_restricted_user) - ) + const clientList = io.sockets.clients(message.room_id) // avoid unnecessary work if no clients are connected if (clientList.length === 0) { @@ -173,9 +176,14 @@ module.exports = WebsocketLoadBalancer = { }, 'disconnecting client' ) + client.emit('project:access:revoked') client.disconnect() } else { - client.emit(message.message, ...message.payload) + if ( + !(isRestrictedMessage && client.ol_context.is_restricted_user) + ) { + client.emit(message.message, ...message.payload) + } } } } diff --git a/services/real-time/test/acceptance/js/ReceiveEditorEventTests.js b/services/real-time/test/acceptance/js/ReceiveEditorEventTests.js index d12f45bcaf..f44f29719f 100644 --- a/services/real-time/test/acceptance/js/ReceiveEditorEventTests.js +++ b/services/real-time/test/acceptance/js/ReceiveEditorEventTests.js @@ -41,6 +41,9 @@ describe('receiveEditorEvent', function () { this.user_b_id = null this.user_b_client = null + this.user_c_id = null + this.user_c_client = null + async.series( [ /** @@ -51,6 +54,7 @@ describe('receiveEditorEvent', function () { { privilegeLevel: 'owner', project: { name: 'Test Project' }, + userMetadata: { isInvitedMember: true }, }, (error, { user_id: userId, project_id: projectId }) => { if (error) return done(error) @@ -95,13 +99,14 @@ describe('receiveEditorEvent', function () { }, /** - * add user_a to project + * add user_a to project, as an invited member */ cb => { return FixturesManager.setUpProject( { privilegeLevel: 'readAndWrite', project_id: this.project_id, + userMetadata: { isTokenMember: false, isInvitedMember: true }, }, (error, { user_id: userIdSecond }) => { if (error) return done(error) @@ -133,13 +138,14 @@ describe('receiveEditorEvent', function () { }, /** - * Set up user_b + * Set up user_b, as a token-access/link-sharing user */ cb => { return FixturesManager.setUpProject( { privilegeLevel: 'readAndWrite', project_id: this.project_id, + userMetadata: { isTokenMember: true, isInvitedMember: false }, }, (error, { user_id: userIdThird }) => { if (error) return done(error) @@ -170,23 +176,81 @@ describe('receiveEditorEvent', function () { return this.user_b_client.emit('joinDoc', this.doc_id, cb) }, + /** + * Set up user_c, as a 'restricted' user (anonymous read-only link-sharing) + */ + cb => { + return FixturesManager.setUpProject( + { + privilegeLevel: 'readAndWrite', + project_id: this.project_id, + userMetadata: { + isTokenMember: false, + isInvitedMember: false, + isRestrictedUser: true, + }, + }, + (error, { user_id: userIdFourth }) => { + if (error) return done(error) + this.user_c_id = userIdFourth + return cb() + } + ) + }, + + /** + * Connect user_c to project/doc + */ + cb => { + this.user_c_client = RealTimeClient.connect() + return this.user_c_client.on('connectionAccepted', cb) + }, + + cb => { + return this.user_c_client.emit( + 'joinProject', + { + project_id: this.project_id, + }, + cb + ) + }, + cb => { + return this.user_c_client.emit('joinDoc', this.doc_id, cb) + }, + + // -------------- + /** * Listen for updates */ cb => { - const eventName = 'userRemovedFromProject' this.owner_updates = [] - this.owner_client.on(eventName, update => - this.owner_updates.push({ [eventName]: update }) - ) this.user_a_updates = [] - this.user_a_client.on(eventName, update => - this.user_a_updates.push({ [eventName]: update }) - ) this.user_b_updates = [] - this.user_b_client.on(eventName, update => - this.user_b_updates.push({ [eventName]: update }) - ) + this.user_c_updates = [] + + const eventNames = [ + 'userRemovedFromProject', + 'project:publicAccessLevel:changed', + 'project:access:revoked', + ] + + for (const eventName of eventNames) { + this.owner_client.on(eventName, update => + this.owner_updates.push({ [eventName]: update }) + ) + this.user_a_client.on(eventName, update => + this.user_a_updates.push({ [eventName]: update }) + ) + this.user_b_client.on(eventName, update => + this.user_b_updates.push({ [eventName]: update }) + ) + this.user_c_client.on(eventName, update => + this.user_c_updates.push({ [eventName]: update }) + ) + } + return cb() }, ], @@ -204,6 +268,101 @@ describe('receiveEditorEvent', function () { if (this.user_b_client) { this.user_b_client.disconnect() } + if (this.user_c_client) { + this.user_c_client.disconnect() + } + }) + + describe('event: project:publicAccessLevel:changed, set to private', function () { + beforeEach(function (done) { + /** + * We turn off link sharing + */ + rclient.publish( + 'editor-events', + JSON.stringify({ + room_id: this.project_id, + message: 'project:publicAccessLevel:changed', + payload: [{ newAccessLevel: 'private' }], + }) + ) + setTimeout(done, 200) + }) + + it('should disconnect the token-access user, and restricted users', function () { + expect(this.user_b_client.socket.connected).to.equal(false) + expect(this.user_c_client.socket.connected).to.equal(false) + }) + + it('should not disconnect the other users', function () { + expect(this.owner_client.socket.connected).to.equal(true) + expect(this.user_a_client.socket.connected).to.equal(true) + }) + + it('should send the event to the remaining connected clients', function () { + expect(this.owner_updates).to.deep.equal([ + { 'project:publicAccessLevel:changed': { newAccessLevel: 'private' } }, + ]) + + expect(this.user_a_updates).to.deep.equal([ + { 'project:publicAccessLevel:changed': { newAccessLevel: 'private' } }, + ]) + }) + + it('should send a project:access:revoked message to the disconnected clients', function () { + expect(this.user_b_updates).to.deep.equal([ + { 'project:access:revoked': undefined }, + ]) + expect(this.user_c_updates).to.deep.equal([ + { 'project:access:revoked': undefined }, + ]) + }) + }) + + describe('event: project:publicAccessLevel:changed, set to tokenBased', function () { + beforeEach(function (done) { + /** + * We turn on link sharing + */ + rclient.publish( + 'editor-events', + JSON.stringify({ + room_id: this.project_id, + message: 'project:publicAccessLevel:changed', + payload: [{ newAccessLevel: 'tokenBased' }], + }) + ) + setTimeout(done, 200) + }) + + it('should not disconnect anyone', function () { + expect(this.owner_client.socket.connected).to.equal(true) + expect(this.user_a_client.socket.connected).to.equal(true) + expect(this.user_b_client.socket.connected).to.equal(true) + expect(this.user_c_client.socket.connected).to.equal(true) + }) + + it('should send the event to all non-restricted clients', function () { + expect(this.owner_updates).to.deep.equal([ + { + 'project:publicAccessLevel:changed': { newAccessLevel: 'tokenBased' }, + }, + ]) + + expect(this.user_a_updates).to.deep.equal([ + { + 'project:publicAccessLevel:changed': { newAccessLevel: 'tokenBased' }, + }, + ]) + + expect(this.user_b_updates).to.deep.equal([ + { + 'project:publicAccessLevel:changed': { newAccessLevel: 'tokenBased' }, + }, + ]) + // restricted users don't receive this type of message + expect(this.user_c_updates.length).to.equal(0) + }) }) describe('event: userRemovedFromProject', function () { @@ -238,11 +397,15 @@ describe('receiveEditorEvent', function () { { userRemovedFromProject: removedUserId }, ]) - expect(this.user_a_updates.length).to.equal(0) - expect(this.user_b_updates).to.deep.equal([ { userRemovedFromProject: removedUserId }, ]) }) + + it('should send a project:access:revoked message to the disconnected clients', function () { + expect(this.user_a_updates).to.deep.equal([ + { 'project:access:revoked': undefined }, + ]) + }) }) }) diff --git a/services/real-time/test/acceptance/js/helpers/FixturesManager.js b/services/real-time/test/acceptance/js/helpers/FixturesManager.js index 313aaff5a7..9bc8dd67c0 100644 --- a/services/real-time/test/acceptance/js/helpers/FixturesManager.js +++ b/services/real-time/test/acceptance/js/helpers/FixturesManager.js @@ -34,6 +34,7 @@ module.exports = FixturesManager = { privilegeLevel, project, publicAccess, + userMetadata, } = options const privileges = {} @@ -42,7 +43,15 @@ module.exports = FixturesManager = { privileges['anonymous-user'] = publicAccess } - MockWebServer.createMockProject(projectId, privileges, project) + const metadataByUser = {} + metadataByUser[userId] = userMetadata + + MockWebServer.createMockProject( + projectId, + privileges, + project, + metadataByUser + ) return MockWebServer.run(error => { if (error != null) { throw error diff --git a/services/real-time/test/acceptance/js/helpers/MockWebServer.js b/services/real-time/test/acceptance/js/helpers/MockWebServer.js index 8dfee0f962..0cc4420a4c 100644 --- a/services/real-time/test/acceptance/js/helpers/MockWebServer.js +++ b/services/real-time/test/acceptance/js/helpers/MockWebServer.js @@ -16,9 +16,11 @@ const express = require('express') module.exports = MockWebServer = { projects: {}, privileges: {}, + userMetadata: {}, - createMockProject(projectId, privileges, project) { + createMockProject(projectId, privileges, project, metadataByUser) { MockWebServer.privileges[projectId] = privileges + MockWebServer.userMetadata[projectId] = metadataByUser return (MockWebServer.projects[projectId] = project) }, @@ -26,12 +28,12 @@ module.exports = MockWebServer = { if (callback == null) { callback = function () {} } - return callback( - null, - MockWebServer.projects[projectId], + const project = MockWebServer.projects[projectId] + const privilegeLevel = MockWebServer.privileges[projectId][userId] || - MockWebServer.privileges[projectId]['anonymous-user'] - ) + MockWebServer.privileges[projectId]['anonymous-user'] + const userMetadata = MockWebServer.userMetadata[projectId]?.[userId] + return callback(null, project, privilegeLevel, userMetadata) }, joinProjectRequest(req, res, next) { @@ -52,13 +54,16 @@ module.exports = MockWebServer = { return MockWebServer.joinProject( projectId, userId, - (error, project, privilegeLevel) => { + (error, project, privilegeLevel, userMetadata) => { if (error != null) { return next(error) } return res.json({ project, privilegeLevel, + isRestrictedUser: !!userMetadata?.isRestrictedUser, + isTokenMember: !!userMetadata?.isTokenMember, + isInvitedMember: !!userMetadata?.isInvitedMember, }) } ) diff --git a/services/real-time/test/unit/js/WebApiManagerTests.js b/services/real-time/test/unit/js/WebApiManagerTests.js index 922e15a8f8..451c1ccedf 100644 --- a/services/real-time/test/unit/js/WebApiManagerTests.js +++ b/services/real-time/test/unit/js/WebApiManagerTests.js @@ -43,6 +43,8 @@ describe('WebApiManager', function () { project: { name: 'Test project' }, privilegeLevel: 'owner', isRestrictedUser: true, + isTokenMember: true, + isInvitedMember: true, } this.request.post = sinon .stub() @@ -79,7 +81,11 @@ describe('WebApiManager', function () { null, this.response.project, this.response.privilegeLevel, - this.response.isRestrictedUser + { + isRestrictedUser: this.response.isRestrictedUser, + isTokenMember: this.response.isTokenMember, + isInvitedMember: this.response.isInvitedMember, + } ) .should.equal(true) }) diff --git a/services/real-time/test/unit/js/WebsocketControllerTests.js b/services/real-time/test/unit/js/WebsocketControllerTests.js index b8e798486e..532a93d5be 100644 --- a/services/real-time/test/unit/js/WebsocketControllerTests.js +++ b/services/real-time/test/unit/js/WebsocketControllerTests.js @@ -75,15 +75,15 @@ describe('WebsocketController', function () { .stub() .callsArgAsync(4) this.isRestrictedUser = true + this.isTokenMember = true + this.isInvitedMember = true this.WebApiManager.joinProject = sinon .stub() - .callsArgWith( - 2, - null, - this.project, - this.privilegeLevel, - this.isRestrictedUser - ) + .callsArgWith(2, null, this.project, this.privilegeLevel, { + isRestrictedUser: this.isRestrictedUser, + isTokenMember: this.isTokenMember, + isInvitedMember: this.isInvitedMember, + }) this.RoomManager.joinProject = sinon.stub().callsArg(2) return this.WebsocketController.joinProject( this.client, @@ -150,6 +150,14 @@ describe('WebsocketController', function () { this.isRestrictedUser ) }) + it('should set the is_token_member flag on the client', function () { + this.client.ol_context.is_token_member.should.equal(this.isTokenMember) + }) + it('should set the is_invited_member flag on the client', function () { + this.client.ol_context.is_invited_member.should.equal( + this.isInvitedMember + ) + }) it('should call the callback with the project, privilegeLevel and protocolVersion', function () { return this.callback .calledWith( @@ -212,15 +220,15 @@ describe('WebsocketController', function () { .stub() .callsArgAsync(4) this.isRestrictedUser = true + this.isTokenMember = true + this.isInvitedMember = true this.WebApiManager.joinProject = sinon .stub() - .callsArgWith( - 2, - null, - this.project, - this.privilegeLevel, - this.isRestrictedUser - ) + .callsArgWith(2, null, this.project, this.privilegeLevel, { + isRestrictedUser: this.isRestrictedUser, + isTokenMember: this.isTokenMember, + isInvitedMember: this.isInvitedMember, + }) this.RoomManager.joinProject = sinon .stub() .callsArgWith(2, new Error('subscribe failed')) @@ -273,12 +281,11 @@ describe('WebsocketController', function () { beforeEach(function () { this.WebApiManager.joinProject = (project, user, cb) => { this.client.disconnected = true - return cb( - null, - this.project, - this.privilegeLevel, - this.isRestrictedUser - ) + return cb(null, this.project, this.privilegeLevel, { + isRestrictedUser: this.isRestrictedUser, + isTokenMember: this.isTokenMember, + isInvitedMember: this.isInvitedMember, + }) } return this.WebsocketController.joinProject( diff --git a/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js b/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js index 9299b0eb2d..3aadbfcc12 100644 --- a/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js +++ b/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js @@ -23,6 +23,7 @@ describe('WebsocketLoadBalancer', function () { this.RoomEvents = { on: sinon.stub() } this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, { requires: { + '@overleaf/settings': (this.Settings = { redis: {} }), './RedisClientManager': { createClientList: () => [], }, @@ -55,11 +56,10 @@ describe('WebsocketLoadBalancer', function () { }) describe('shouldDisconnectClient', function () { - const client = { - ol_context: { user_id: 'abcd' }, - } - it('should return false for general messages', function () { + const client = { + ol_context: { user_id: 'abcd' }, + } const message = { message: 'someNiceMessage', payload: [{ data: 'whatever' }], @@ -69,24 +69,103 @@ describe('WebsocketLoadBalancer', function () { ).to.equal(false) }) - it('should return false for userRemovedFromProject, when the user_id does not match', function () { - const message = { - message: 'userRemovedFromProject', - payload: ['xyz'], + describe('user removed from project', function () { + const messageName = 'userRemovedFromProject' + const client = { + ol_context: { user_id: 'abcd' }, } - expect( - this.WebsocketLoadBalancer.shouldDisconnectClient(client, message) - ).to.equal(false) + it('should return false, when the user_id does not match', function () { + const message = { + message: messageName, + payload: ['xyz'], + } + expect( + this.WebsocketLoadBalancer.shouldDisconnectClient(client, message) + ).to.equal(false) + }) + + it('should return true, if the user_id matches', function () { + const message = { + message: messageName, + payload: [`${client.ol_context.user_id}`], + } + expect( + this.WebsocketLoadBalancer.shouldDisconnectClient(client, message) + ).to.equal(true) + }) }) - it('should return true for userRemovedFromProject, if the user_id matches', function () { - const message = { - message: 'userRemovedFromProject', - payload: [`${client.ol_context.user_id}`], - } - expect( - this.WebsocketLoadBalancer.shouldDisconnectClient(client, message) - ).to.equal(true) + describe('link-sharing turned off', function () { + const messageName = 'project:publicAccessLevel:changed' + + describe('when the new access level is set to "private"', function () { + const message = { + message: messageName, + payload: [{ newAccessLevel: 'private' }], + } + describe('when the user is an invited member', function () { + const client = { + ol_context: { + is_invited_member: true, + }, + } + + it('should return false', function () { + expect( + this.WebsocketLoadBalancer.shouldDisconnectClient(client, message) + ).to.equal(false) + }) + }) + + describe('when the user not an invited member', function () { + const client = { + ol_context: { + is_invited_member: false, + }, + } + + it('should return true', function () { + expect( + this.WebsocketLoadBalancer.shouldDisconnectClient(client, message) + ).to.equal(true) + }) + }) + }) + + describe('when the new access level is "tokenBased"', function () { + const message = { + message: messageName, + payload: [{ newAccessLevel: 'tokenBased' }], + } + + describe('when the user is an invited member', function () { + const client = { + ol_context: { + is_invited_member: true, + }, + } + + it('should return false', function () { + expect( + this.WebsocketLoadBalancer.shouldDisconnectClient(client, message) + ).to.equal(false) + }) + }) + + describe('when the user not an invited member', function () { + const client = { + ol_context: { + is_invited_member: false, + }, + } + + it('should return false', function () { + expect( + this.WebsocketLoadBalancer.shouldDisconnectClient(client, message) + ).to.equal(false) + }) + }) + }) }) }) @@ -392,7 +471,7 @@ describe('WebsocketLoadBalancer', function () { client1.emit .calledWith(message, ...Array.from(payload)) .should.equal(true) - client2.emit.called.should.equal(false) // disconnected client should not be called + client2.emit.calledWith('project:access:revoked').should.equal(true) // disconnected client should get informative message client3.emit .calledWith(message, ...Array.from(payload)) .should.equal(true) diff --git a/services/web/app/src/Features/Editor/EditorHttpController.js b/services/web/app/src/Features/Editor/EditorHttpController.js index 33a38016ab..17e445ad38 100644 --- a/services/web/app/src/Features/Editor/EditorHttpController.js +++ b/services/web/app/src/Features/Editor/EditorHttpController.js @@ -57,8 +57,13 @@ async function joinProject(req, res, next) { userId = null } Metrics.inc('editor.join-project') - const { project, privilegeLevel, isRestrictedUser } = - await _buildJoinProjectView(req, projectId, userId) + const { + project, + privilegeLevel, + isRestrictedUser, + isTokenMember, + isInvitedMember, + } = await _buildJoinProjectView(req, projectId, userId) if (!project) { return res.sendStatus(403) } @@ -85,6 +90,8 @@ async function joinProject(req, res, next) { project, privilegeLevel, isRestrictedUser, + isTokenMember, + isInvitedMember, }) } @@ -150,6 +157,8 @@ async function _buildJoinProjectView(req, projectId, userId) { deletedDocsFromDocstore ), privilegeLevel, + isTokenMember, + isInvitedMember, isRestrictedUser, } } diff --git a/services/web/frontend/js/ide.js b/services/web/frontend/js/ide.js index a25ac1d171..8ef2a1dbde 100644 --- a/services/web/frontend/js/ide.js +++ b/services/web/frontend/js/ide.js @@ -476,6 +476,13 @@ If the project has been renamed please look in your project list for a new proje $scope.hasLintingError = event.detail.hasLintingError }) + ide.socket.on('project:access:revoked', () => { + ide.showGenericMessageModal( + 'Removed From Project', + 'You have been removed from this project, and will no longer have access to it. You will be redirected to your project dashboard momentarily.' + ) + }) + return ide.socket.on('project:publicAccessLevel:changed', data => { if (data.newAccessLevel != null) { ide.$scope.project.publicAccesLevel = data.newAccessLevel diff --git a/services/web/test/acceptance/src/TokenAccessTests.js b/services/web/test/acceptance/src/TokenAccessTests.js index a33a506cb2..cbd961375c 100644 --- a/services/web/test/acceptance/src/TokenAccessTests.js +++ b/services/web/test/acceptance/src/TokenAccessTests.js @@ -380,6 +380,8 @@ describe('TokenAccess', function () { (response, body) => { expect(body.privilegeLevel).to.equal('readOnly') expect(body.isRestrictedUser).to.equal(true) + expect(body.isTokenMember).to.equal(true) + expect(body.isInvitedMember).to.equal(false) expect(body.project.owner).to.have.keys('_id') expect(body.project.owner).to.not.have.any.keys( 'email', @@ -431,6 +433,8 @@ describe('TokenAccess', function () { (response, body) => { expect(body.privilegeLevel).to.equal('owner') expect(body.isRestrictedUser).to.equal(false) + expect(body.isTokenMember).to.equal(false) + expect(body.isInvitedMember).to.equal(false) }, cb ) @@ -560,6 +564,8 @@ describe('TokenAccess', function () { (response, body) => { expect(body.privilegeLevel).to.equal('readOnly') expect(body.isRestrictedUser).to.equal(true) + expect(body.isTokenMember).to.equal(false) + expect(body.isInvitedMember).to.equal(false) expect(body.project.owner).to.have.keys('_id') expect(body.project.owner).to.not.have.any.keys( 'email', @@ -718,6 +724,8 @@ describe('TokenAccess', function () { (response, body) => { expect(body.privilegeLevel).to.equal('readAndWrite') expect(body.isRestrictedUser).to.equal(false) + expect(body.isTokenMember).to.equal(true) + expect(body.isInvitedMember).to.equal(false) expect(body.project.owner).to.have.all.keys( '_id', 'email', @@ -804,6 +812,8 @@ describe('TokenAccess', function () { (response, body) => { expect(body.privilegeLevel).to.equal('readOnly') expect(body.isRestrictedUser).to.equal(true) + expect(body.isTokenMember).to.equal(true) + expect(body.isInvitedMember).to.equal(false) expect(body.project.owner).to.have.keys('_id') expect(body.project.owner).to.not.have.any.keys( 'email', @@ -847,6 +857,8 @@ describe('TokenAccess', function () { (response, body) => { expect(body.privilegeLevel).to.equal('readAndWrite') expect(body.isRestrictedUser).to.equal(false) + expect(body.isTokenMember).to.equal(true) + expect(body.isInvitedMember).to.equal(false) expect(body.project.owner).to.have.all.keys( '_id', 'email', @@ -1352,6 +1364,9 @@ describe('TokenAccess', function () { this.projectId, (response, body) => { expect(body.privilegeLevel).to.equal('readAndWrite') + expect(body.isRestrictedUser).to.equal(false) + expect(body.isTokenMember).to.equal(false) + expect(body.isInvitedMember).to.equal(true) }, cb ), diff --git a/services/web/test/unit/src/Editor/EditorHttpControllerTests.js b/services/web/test/unit/src/Editor/EditorHttpControllerTests.js index 58f990f36d..74095b3b5e 100644 --- a/services/web/test/unit/src/Editor/EditorHttpControllerTests.js +++ b/services/web/test/unit/src/Editor/EditorHttpControllerTests.js @@ -161,6 +161,9 @@ describe('EditorHttpController', function () { describe('successfully', function () { beforeEach(function (done) { + this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( + true + ) this.res.json.callsFake(() => done()) this.EditorHttpController.joinProject(this.req, this.res) }) @@ -170,6 +173,8 @@ describe('EditorHttpController', function () { project: this.projectView, privilegeLevel: 'owner', isRestrictedUser: false, + isTokenMember: false, + isInvitedMember: true, }) }) @@ -212,6 +217,8 @@ describe('EditorHttpController', function () { project: this.reducedProjectView, privilegeLevel: 'readOnly', isRestrictedUser: true, + isTokenMember: false, + isInvitedMember: false, }) }) }) @@ -248,6 +255,32 @@ describe('EditorHttpController', function () { project: this.reducedProjectView, privilegeLevel: 'readOnly', isRestrictedUser: true, + isTokenMember: false, + isInvitedMember: false, + }) + }) + }) + + describe('with a token access user', function () { + beforeEach(function (done) { + this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( + false + ) + this.CollaboratorsHandler.promises.userIsTokenMember.resolves(true) + this.AuthorizationManager.promises.getPrivilegeLevelForProject.resolves( + 'readAndWrite' + ) + this.res.json.callsFake(() => done()) + this.EditorHttpController.joinProject(this.req, this.res) + }) + + it('should mark the user as being a token-access member', function () { + expect(this.res.json).to.have.been.calledWith({ + project: this.projectView, + privilegeLevel: 'readAndWrite', + isRestrictedUser: false, + isTokenMember: true, + isInvitedMember: false, }) }) })