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
This commit is contained in:
June Kelly 2023-05-30 13:21:55 +01:00 committed by Copybot
parent bb815268f2
commit d68ed0efdf
13 changed files with 424 additions and 81 deletions

View file

@ -40,12 +40,12 @@ module.exports = {
if (!(data && data.project)) { if (!(data && data.project)) {
return callback(new CorruptedJoinProjectResponseError()) return callback(new CorruptedJoinProjectResponseError())
} }
callback( const userMetadata = {
null, isRestrictedUser: data.isRestrictedUser,
data.project, isTokenMember: data.isTokenMember,
data.privilegeLevel, isInvitedMember: data.isInvitedMember,
data.isRestrictedUser }
) callback(null, data.project, data.privilegeLevel, userMetadata)
} else if (response.statusCode === 429) { } else if (response.statusCode === 429) {
callback( callback(
new CodedError( new CodedError(

View file

@ -43,7 +43,7 @@ module.exports = WebsocketController = {
WebApiManager.joinProject( WebApiManager.joinProject(
projectId, projectId,
user, user,
function (error, project, privilegeLevel, isRestrictedUser) { function (error, project, privilegeLevel, userMetadata) {
if (error) { if (error) {
return callback(error) return callback(error)
} }
@ -73,7 +73,9 @@ module.exports = WebsocketController = {
client.ol_context.connected_time = new Date() client.ol_context.connected_time = new Date()
client.ol_context.signup_date = user.signUpDate client.ol_context.signup_date = user.signUpDate
client.ol_context.login_count = user.loginCount 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) { RoomManager.joinProject(client, projectId, function (err) {
if (err) { if (err) {
@ -85,7 +87,7 @@ module.exports = WebsocketController = {
projectId, projectId,
clientId: client.id, clientId: client.id,
privilegeLevel, privilegeLevel,
isRestrictedUser, userMetadata,
}, },
'user joined project' 'user joined project'
) )

View file

@ -31,6 +31,14 @@ module.exports = WebsocketLoadBalancer = {
if (message?.payload?.includes(userId)) { if (message?.payload?.includes(userId)) {
return true 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 return false
}, },
@ -139,12 +147,7 @@ module.exports = WebsocketLoadBalancer = {
!RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST.includes(message.message) !RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST.includes(message.message)
// send messages only to unique clients (due to duplicate entries in io.sockets.clients) // send messages only to unique clients (due to duplicate entries in io.sockets.clients)
const clientList = io.sockets const clientList = io.sockets.clients(message.room_id)
.clients(message.room_id)
.filter(
client =>
!(isRestrictedMessage && client.ol_context.is_restricted_user)
)
// avoid unnecessary work if no clients are connected // avoid unnecessary work if no clients are connected
if (clientList.length === 0) { if (clientList.length === 0) {
@ -173,9 +176,14 @@ module.exports = WebsocketLoadBalancer = {
}, },
'disconnecting client' 'disconnecting client'
) )
client.emit('project:access:revoked')
client.disconnect() client.disconnect()
} else { } else {
client.emit(message.message, ...message.payload) if (
!(isRestrictedMessage && client.ol_context.is_restricted_user)
) {
client.emit(message.message, ...message.payload)
}
} }
} }
} }

View file

@ -41,6 +41,9 @@ describe('receiveEditorEvent', function () {
this.user_b_id = null this.user_b_id = null
this.user_b_client = null this.user_b_client = null
this.user_c_id = null
this.user_c_client = null
async.series( async.series(
[ [
/** /**
@ -51,6 +54,7 @@ describe('receiveEditorEvent', function () {
{ {
privilegeLevel: 'owner', privilegeLevel: 'owner',
project: { name: 'Test Project' }, project: { name: 'Test Project' },
userMetadata: { isInvitedMember: true },
}, },
(error, { user_id: userId, project_id: projectId }) => { (error, { user_id: userId, project_id: projectId }) => {
if (error) return done(error) 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 => { cb => {
return FixturesManager.setUpProject( return FixturesManager.setUpProject(
{ {
privilegeLevel: 'readAndWrite', privilegeLevel: 'readAndWrite',
project_id: this.project_id, project_id: this.project_id,
userMetadata: { isTokenMember: false, isInvitedMember: true },
}, },
(error, { user_id: userIdSecond }) => { (error, { user_id: userIdSecond }) => {
if (error) return done(error) 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 => { cb => {
return FixturesManager.setUpProject( return FixturesManager.setUpProject(
{ {
privilegeLevel: 'readAndWrite', privilegeLevel: 'readAndWrite',
project_id: this.project_id, project_id: this.project_id,
userMetadata: { isTokenMember: true, isInvitedMember: false },
}, },
(error, { user_id: userIdThird }) => { (error, { user_id: userIdThird }) => {
if (error) return done(error) if (error) return done(error)
@ -170,23 +176,81 @@ describe('receiveEditorEvent', function () {
return this.user_b_client.emit('joinDoc', this.doc_id, cb) 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 * Listen for updates
*/ */
cb => { cb => {
const eventName = 'userRemovedFromProject'
this.owner_updates = [] this.owner_updates = []
this.owner_client.on(eventName, update =>
this.owner_updates.push({ [eventName]: update })
)
this.user_a_updates = [] 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_updates = []
this.user_b_client.on(eventName, update => this.user_c_updates = []
this.user_b_updates.push({ [eventName]: update })
) 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() return cb()
}, },
], ],
@ -204,6 +268,101 @@ describe('receiveEditorEvent', function () {
if (this.user_b_client) { if (this.user_b_client) {
this.user_b_client.disconnect() 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 () { describe('event: userRemovedFromProject', function () {
@ -238,11 +397,15 @@ describe('receiveEditorEvent', function () {
{ userRemovedFromProject: removedUserId }, { userRemovedFromProject: removedUserId },
]) ])
expect(this.user_a_updates.length).to.equal(0)
expect(this.user_b_updates).to.deep.equal([ expect(this.user_b_updates).to.deep.equal([
{ userRemovedFromProject: removedUserId }, { 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 },
])
})
}) })
}) })

View file

@ -34,6 +34,7 @@ module.exports = FixturesManager = {
privilegeLevel, privilegeLevel,
project, project,
publicAccess, publicAccess,
userMetadata,
} = options } = options
const privileges = {} const privileges = {}
@ -42,7 +43,15 @@ module.exports = FixturesManager = {
privileges['anonymous-user'] = publicAccess privileges['anonymous-user'] = publicAccess
} }
MockWebServer.createMockProject(projectId, privileges, project) const metadataByUser = {}
metadataByUser[userId] = userMetadata
MockWebServer.createMockProject(
projectId,
privileges,
project,
metadataByUser
)
return MockWebServer.run(error => { return MockWebServer.run(error => {
if (error != null) { if (error != null) {
throw error throw error

View file

@ -16,9 +16,11 @@ const express = require('express')
module.exports = MockWebServer = { module.exports = MockWebServer = {
projects: {}, projects: {},
privileges: {}, privileges: {},
userMetadata: {},
createMockProject(projectId, privileges, project) { createMockProject(projectId, privileges, project, metadataByUser) {
MockWebServer.privileges[projectId] = privileges MockWebServer.privileges[projectId] = privileges
MockWebServer.userMetadata[projectId] = metadataByUser
return (MockWebServer.projects[projectId] = project) return (MockWebServer.projects[projectId] = project)
}, },
@ -26,12 +28,12 @@ module.exports = MockWebServer = {
if (callback == null) { if (callback == null) {
callback = function () {} callback = function () {}
} }
return callback( const project = MockWebServer.projects[projectId]
null, const privilegeLevel =
MockWebServer.projects[projectId],
MockWebServer.privileges[projectId][userId] || 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) { joinProjectRequest(req, res, next) {
@ -52,13 +54,16 @@ module.exports = MockWebServer = {
return MockWebServer.joinProject( return MockWebServer.joinProject(
projectId, projectId,
userId, userId,
(error, project, privilegeLevel) => { (error, project, privilegeLevel, userMetadata) => {
if (error != null) { if (error != null) {
return next(error) return next(error)
} }
return res.json({ return res.json({
project, project,
privilegeLevel, privilegeLevel,
isRestrictedUser: !!userMetadata?.isRestrictedUser,
isTokenMember: !!userMetadata?.isTokenMember,
isInvitedMember: !!userMetadata?.isInvitedMember,
}) })
} }
) )

View file

@ -43,6 +43,8 @@ describe('WebApiManager', function () {
project: { name: 'Test project' }, project: { name: 'Test project' },
privilegeLevel: 'owner', privilegeLevel: 'owner',
isRestrictedUser: true, isRestrictedUser: true,
isTokenMember: true,
isInvitedMember: true,
} }
this.request.post = sinon this.request.post = sinon
.stub() .stub()
@ -79,7 +81,11 @@ describe('WebApiManager', function () {
null, null,
this.response.project, this.response.project,
this.response.privilegeLevel, this.response.privilegeLevel,
this.response.isRestrictedUser {
isRestrictedUser: this.response.isRestrictedUser,
isTokenMember: this.response.isTokenMember,
isInvitedMember: this.response.isInvitedMember,
}
) )
.should.equal(true) .should.equal(true)
}) })

View file

@ -75,15 +75,15 @@ describe('WebsocketController', function () {
.stub() .stub()
.callsArgAsync(4) .callsArgAsync(4)
this.isRestrictedUser = true this.isRestrictedUser = true
this.isTokenMember = true
this.isInvitedMember = true
this.WebApiManager.joinProject = sinon this.WebApiManager.joinProject = sinon
.stub() .stub()
.callsArgWith( .callsArgWith(2, null, this.project, this.privilegeLevel, {
2, isRestrictedUser: this.isRestrictedUser,
null, isTokenMember: this.isTokenMember,
this.project, isInvitedMember: this.isInvitedMember,
this.privilegeLevel, })
this.isRestrictedUser
)
this.RoomManager.joinProject = sinon.stub().callsArg(2) this.RoomManager.joinProject = sinon.stub().callsArg(2)
return this.WebsocketController.joinProject( return this.WebsocketController.joinProject(
this.client, this.client,
@ -150,6 +150,14 @@ describe('WebsocketController', function () {
this.isRestrictedUser 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 () { it('should call the callback with the project, privilegeLevel and protocolVersion', function () {
return this.callback return this.callback
.calledWith( .calledWith(
@ -212,15 +220,15 @@ describe('WebsocketController', function () {
.stub() .stub()
.callsArgAsync(4) .callsArgAsync(4)
this.isRestrictedUser = true this.isRestrictedUser = true
this.isTokenMember = true
this.isInvitedMember = true
this.WebApiManager.joinProject = sinon this.WebApiManager.joinProject = sinon
.stub() .stub()
.callsArgWith( .callsArgWith(2, null, this.project, this.privilegeLevel, {
2, isRestrictedUser: this.isRestrictedUser,
null, isTokenMember: this.isTokenMember,
this.project, isInvitedMember: this.isInvitedMember,
this.privilegeLevel, })
this.isRestrictedUser
)
this.RoomManager.joinProject = sinon this.RoomManager.joinProject = sinon
.stub() .stub()
.callsArgWith(2, new Error('subscribe failed')) .callsArgWith(2, new Error('subscribe failed'))
@ -273,12 +281,11 @@ describe('WebsocketController', function () {
beforeEach(function () { beforeEach(function () {
this.WebApiManager.joinProject = (project, user, cb) => { this.WebApiManager.joinProject = (project, user, cb) => {
this.client.disconnected = true this.client.disconnected = true
return cb( return cb(null, this.project, this.privilegeLevel, {
null, isRestrictedUser: this.isRestrictedUser,
this.project, isTokenMember: this.isTokenMember,
this.privilegeLevel, isInvitedMember: this.isInvitedMember,
this.isRestrictedUser })
)
} }
return this.WebsocketController.joinProject( return this.WebsocketController.joinProject(

View file

@ -23,6 +23,7 @@ describe('WebsocketLoadBalancer', function () {
this.RoomEvents = { on: sinon.stub() } this.RoomEvents = { on: sinon.stub() }
this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, { this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, {
requires: { requires: {
'@overleaf/settings': (this.Settings = { redis: {} }),
'./RedisClientManager': { './RedisClientManager': {
createClientList: () => [], createClientList: () => [],
}, },
@ -55,11 +56,10 @@ describe('WebsocketLoadBalancer', function () {
}) })
describe('shouldDisconnectClient', function () { describe('shouldDisconnectClient', function () {
const client = {
ol_context: { user_id: 'abcd' },
}
it('should return false for general messages', function () { it('should return false for general messages', function () {
const client = {
ol_context: { user_id: 'abcd' },
}
const message = { const message = {
message: 'someNiceMessage', message: 'someNiceMessage',
payload: [{ data: 'whatever' }], payload: [{ data: 'whatever' }],
@ -69,24 +69,103 @@ describe('WebsocketLoadBalancer', function () {
).to.equal(false) ).to.equal(false)
}) })
it('should return false for userRemovedFromProject, when the user_id does not match', function () { describe('user removed from project', function () {
const message = { const messageName = 'userRemovedFromProject'
message: 'userRemovedFromProject', const client = {
payload: ['xyz'], ol_context: { user_id: 'abcd' },
} }
expect( it('should return false, when the user_id does not match', function () {
this.WebsocketLoadBalancer.shouldDisconnectClient(client, message) const message = {
).to.equal(false) 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 () { describe('link-sharing turned off', function () {
const message = { const messageName = 'project:publicAccessLevel:changed'
message: 'userRemovedFromProject',
payload: [`${client.ol_context.user_id}`], describe('when the new access level is set to "private"', function () {
} const message = {
expect( message: messageName,
this.WebsocketLoadBalancer.shouldDisconnectClient(client, message) payload: [{ newAccessLevel: 'private' }],
).to.equal(true) }
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 client1.emit
.calledWith(message, ...Array.from(payload)) .calledWith(message, ...Array.from(payload))
.should.equal(true) .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 client3.emit
.calledWith(message, ...Array.from(payload)) .calledWith(message, ...Array.from(payload))
.should.equal(true) .should.equal(true)

View file

@ -57,8 +57,13 @@ async function joinProject(req, res, next) {
userId = null userId = null
} }
Metrics.inc('editor.join-project') Metrics.inc('editor.join-project')
const { project, privilegeLevel, isRestrictedUser } = const {
await _buildJoinProjectView(req, projectId, userId) project,
privilegeLevel,
isRestrictedUser,
isTokenMember,
isInvitedMember,
} = await _buildJoinProjectView(req, projectId, userId)
if (!project) { if (!project) {
return res.sendStatus(403) return res.sendStatus(403)
} }
@ -85,6 +90,8 @@ async function joinProject(req, res, next) {
project, project,
privilegeLevel, privilegeLevel,
isRestrictedUser, isRestrictedUser,
isTokenMember,
isInvitedMember,
}) })
} }
@ -150,6 +157,8 @@ async function _buildJoinProjectView(req, projectId, userId) {
deletedDocsFromDocstore deletedDocsFromDocstore
), ),
privilegeLevel, privilegeLevel,
isTokenMember,
isInvitedMember,
isRestrictedUser, isRestrictedUser,
} }
} }

View file

@ -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 $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 => { return ide.socket.on('project:publicAccessLevel:changed', data => {
if (data.newAccessLevel != null) { if (data.newAccessLevel != null) {
ide.$scope.project.publicAccesLevel = data.newAccessLevel ide.$scope.project.publicAccesLevel = data.newAccessLevel

View file

@ -380,6 +380,8 @@ describe('TokenAccess', function () {
(response, body) => { (response, body) => {
expect(body.privilegeLevel).to.equal('readOnly') expect(body.privilegeLevel).to.equal('readOnly')
expect(body.isRestrictedUser).to.equal(true) 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.have.keys('_id')
expect(body.project.owner).to.not.have.any.keys( expect(body.project.owner).to.not.have.any.keys(
'email', 'email',
@ -431,6 +433,8 @@ describe('TokenAccess', function () {
(response, body) => { (response, body) => {
expect(body.privilegeLevel).to.equal('owner') expect(body.privilegeLevel).to.equal('owner')
expect(body.isRestrictedUser).to.equal(false) expect(body.isRestrictedUser).to.equal(false)
expect(body.isTokenMember).to.equal(false)
expect(body.isInvitedMember).to.equal(false)
}, },
cb cb
) )
@ -560,6 +564,8 @@ describe('TokenAccess', function () {
(response, body) => { (response, body) => {
expect(body.privilegeLevel).to.equal('readOnly') expect(body.privilegeLevel).to.equal('readOnly')
expect(body.isRestrictedUser).to.equal(true) 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.have.keys('_id')
expect(body.project.owner).to.not.have.any.keys( expect(body.project.owner).to.not.have.any.keys(
'email', 'email',
@ -718,6 +724,8 @@ describe('TokenAccess', function () {
(response, body) => { (response, body) => {
expect(body.privilegeLevel).to.equal('readAndWrite') expect(body.privilegeLevel).to.equal('readAndWrite')
expect(body.isRestrictedUser).to.equal(false) 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( expect(body.project.owner).to.have.all.keys(
'_id', '_id',
'email', 'email',
@ -804,6 +812,8 @@ describe('TokenAccess', function () {
(response, body) => { (response, body) => {
expect(body.privilegeLevel).to.equal('readOnly') expect(body.privilegeLevel).to.equal('readOnly')
expect(body.isRestrictedUser).to.equal(true) 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.have.keys('_id')
expect(body.project.owner).to.not.have.any.keys( expect(body.project.owner).to.not.have.any.keys(
'email', 'email',
@ -847,6 +857,8 @@ describe('TokenAccess', function () {
(response, body) => { (response, body) => {
expect(body.privilegeLevel).to.equal('readAndWrite') expect(body.privilegeLevel).to.equal('readAndWrite')
expect(body.isRestrictedUser).to.equal(false) 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( expect(body.project.owner).to.have.all.keys(
'_id', '_id',
'email', 'email',
@ -1352,6 +1364,9 @@ describe('TokenAccess', function () {
this.projectId, this.projectId,
(response, body) => { (response, body) => {
expect(body.privilegeLevel).to.equal('readAndWrite') 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 cb
), ),

View file

@ -161,6 +161,9 @@ describe('EditorHttpController', function () {
describe('successfully', function () { describe('successfully', function () {
beforeEach(function (done) { beforeEach(function (done) {
this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves(
true
)
this.res.json.callsFake(() => done()) this.res.json.callsFake(() => done())
this.EditorHttpController.joinProject(this.req, this.res) this.EditorHttpController.joinProject(this.req, this.res)
}) })
@ -170,6 +173,8 @@ describe('EditorHttpController', function () {
project: this.projectView, project: this.projectView,
privilegeLevel: 'owner', privilegeLevel: 'owner',
isRestrictedUser: false, isRestrictedUser: false,
isTokenMember: false,
isInvitedMember: true,
}) })
}) })
@ -212,6 +217,8 @@ describe('EditorHttpController', function () {
project: this.reducedProjectView, project: this.reducedProjectView,
privilegeLevel: 'readOnly', privilegeLevel: 'readOnly',
isRestrictedUser: true, isRestrictedUser: true,
isTokenMember: false,
isInvitedMember: false,
}) })
}) })
}) })
@ -248,6 +255,32 @@ describe('EditorHttpController', function () {
project: this.reducedProjectView, project: this.reducedProjectView,
privilegeLevel: 'readOnly', privilegeLevel: 'readOnly',
isRestrictedUser: true, 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,
}) })
}) })
}) })