mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 14:33:43 -05:00
5520f9eed9
[real-time] migrate acceptance tests to auto-join GitOrigin-RevId: fc14a4e803bfab4d07f2773c6ecbdb5459d12684
494 lines
15 KiB
JavaScript
494 lines
15 KiB
JavaScript
/*
|
|
This test suite is a multi level matrix which allows us to test many cases
|
|
with all kinds of setups.
|
|
|
|
Users/Actors are defined in USERS and are a low level entity that does connect
|
|
to a real-time pod. A typical UserItem is:
|
|
|
|
someDescriptiveNameForTheTestSuite: {
|
|
setup(cb) {
|
|
// <setup session here>
|
|
const options = { client: RealTimeClient.connect(), foo: 'bar' }
|
|
cb(null, options)
|
|
}
|
|
}
|
|
|
|
Sessions are a set of actions that a User performs in the life-cycle of a
|
|
real-time session, before they try something weird. A typical SessionItem is:
|
|
|
|
someOtherDescriptiveNameForTheTestSuite: {
|
|
getActions(cb) {
|
|
cb(null, [
|
|
{ rpc: 'RPC_ENDPOINT', args: [...] }
|
|
])
|
|
}
|
|
}
|
|
|
|
Finally there are InvalidRequests which are the weird actions I hinted on in
|
|
the Sessions section. The defined actions may be marked as 'failed' to denote
|
|
that real-time rejects them with an (for this test) expected error.
|
|
A typical InvalidRequestItem is:
|
|
|
|
joinOwnProject: {
|
|
getActions(cb) {
|
|
cb(null, [
|
|
{ rpc: 'RPC_ENDPOINT', args: [...], failed: true }
|
|
])
|
|
}
|
|
}
|
|
|
|
There is additional meta-data that UserItems and SessionItems may use to skip
|
|
certain areas of the matrix. Theses are:
|
|
|
|
- Has the User an own project that they join as part of the Session?
|
|
UserItem: { hasOwnProject: true, setup(cb) { cb(null, { project_id, ... }) }}
|
|
SessionItem: { needsOwnProject: true }
|
|
*/
|
|
|
|
const { expect } = require('chai')
|
|
const async = require('async')
|
|
|
|
const RealTimeClient = require('./helpers/RealTimeClient')
|
|
const FixturesManager = require('./helpers/FixturesManager')
|
|
const MockWebServer = require('./helpers/MockWebServer')
|
|
|
|
const settings = require('@overleaf/settings')
|
|
const Keys = settings.redis.documentupdater.key_schema
|
|
const redis = require('@overleaf/redis-wrapper')
|
|
const rclient = redis.createClient(settings.redis.pubsub)
|
|
|
|
function getPendingUpdates(docId, cb) {
|
|
rclient.lrange(Keys.pendingUpdates({ doc_id: docId }), 0, 10, cb)
|
|
}
|
|
function cleanupPreviousUpdates(docId, cb) {
|
|
rclient.del(Keys.pendingUpdates({ doc_id: docId }), cb)
|
|
}
|
|
|
|
describe('MatrixTests', function () {
|
|
let privateProjectId,
|
|
privateDocId,
|
|
readWriteProjectId,
|
|
readWriteDocId,
|
|
readWriteAnonymousAccessToken
|
|
|
|
let privateClient
|
|
before(function setupPrivateProject(done) {
|
|
FixturesManager.setUpEditorSession(
|
|
{ privilegeLevel: 'owner', publicAccessLevel: 'readAndWrite' },
|
|
(err, { project_id: projectId, doc_id: docId }) => {
|
|
if (err) return done(err)
|
|
privateProjectId = projectId
|
|
privateDocId = docId
|
|
privateClient = RealTimeClient.connect(projectId, err => {
|
|
if (err) return done(err)
|
|
privateClient.emit('joinDoc', privateDocId, done)
|
|
})
|
|
}
|
|
)
|
|
})
|
|
|
|
before(function setupReadWriteProject(done) {
|
|
FixturesManager.setUpEditorSession(
|
|
{
|
|
publicAccess: 'readAndWrite',
|
|
},
|
|
(err, { project_id: projectId, doc_id: docId, anonymousAccessToken }) => {
|
|
readWriteProjectId = projectId
|
|
readWriteDocId = docId
|
|
readWriteAnonymousAccessToken = anonymousAccessToken
|
|
done(err)
|
|
}
|
|
)
|
|
})
|
|
|
|
const USER_SETUP = {
|
|
anonymous: {
|
|
setup(cb) {
|
|
RealTimeClient.setAnonSession(
|
|
readWriteProjectId,
|
|
readWriteAnonymousAccessToken,
|
|
err => {
|
|
if (err) return cb(err)
|
|
cb(null, {})
|
|
}
|
|
)
|
|
},
|
|
},
|
|
|
|
registered: {
|
|
setup(cb) {
|
|
const userId = FixturesManager.getRandomId()
|
|
const user = { _id: userId, first_name: 'Joe', last_name: 'Bloggs' }
|
|
RealTimeClient.setSession({ user }, err => {
|
|
if (err) return cb(err)
|
|
|
|
MockWebServer.inviteUserToProject(
|
|
readWriteProjectId,
|
|
user,
|
|
'readAndWrite'
|
|
)
|
|
cb(null, {
|
|
user_id: userId,
|
|
})
|
|
})
|
|
},
|
|
},
|
|
|
|
registeredWithOwnedProject: {
|
|
setup(cb) {
|
|
FixturesManager.setUpEditorSession(
|
|
{ privilegeLevel: 'owner' },
|
|
(err, { project_id: projectId, user_id: userId, doc_id: docId }) => {
|
|
if (err) return cb(err)
|
|
|
|
MockWebServer.inviteUserToProject(
|
|
readWriteProjectId,
|
|
{ _id: userId },
|
|
'readAndWrite'
|
|
)
|
|
cb(null, {
|
|
user_id: userId,
|
|
project_id: projectId,
|
|
doc_id: docId,
|
|
})
|
|
}
|
|
)
|
|
},
|
|
hasOwnProject: true,
|
|
},
|
|
}
|
|
|
|
Object.entries(USER_SETUP).forEach(level0 => {
|
|
const [userDescription, userItem] = level0
|
|
let options, client
|
|
|
|
const SESSION_SETUP = {
|
|
joinReadWriteProject: {
|
|
getActions(cb) {
|
|
cb(null, [{ connect: readWriteProjectId }])
|
|
},
|
|
needsOwnProject: false,
|
|
},
|
|
|
|
joinReadWriteProjectAndDoc: {
|
|
getActions(cb) {
|
|
cb(null, [
|
|
{ connect: readWriteProjectId },
|
|
{ rpc: 'joinDoc', args: [readWriteDocId] },
|
|
])
|
|
},
|
|
needsOwnProject: false,
|
|
},
|
|
|
|
joinOwnProject: {
|
|
getActions(cb) {
|
|
cb(null, [{ connect: options.project_id }])
|
|
},
|
|
needsOwnProject: true,
|
|
},
|
|
|
|
joinOwnProjectAndDoc: {
|
|
getActions(cb) {
|
|
cb(null, [
|
|
{ connect: options.project_id },
|
|
{ rpc: 'joinDoc', args: [options.doc_id] },
|
|
])
|
|
},
|
|
needsOwnProject: true,
|
|
},
|
|
}
|
|
|
|
function performActions(getActions, done) {
|
|
getActions((err, actions) => {
|
|
if (err) return done(err)
|
|
|
|
async.eachSeries(
|
|
actions,
|
|
(action, next) => {
|
|
const cb = (...returnedArgs) => {
|
|
const error = returnedArgs.shift()
|
|
if (action.fails) {
|
|
expect(error).to.exist
|
|
expect(returnedArgs).to.have.length(0)
|
|
return next()
|
|
}
|
|
next(error)
|
|
}
|
|
|
|
if (action.connect) {
|
|
client = RealTimeClient.connect(action.connect, cb)
|
|
} else if (action.rpc) {
|
|
if (client?.socket?.connected) {
|
|
client.emit(action.rpc, ...action.args, cb)
|
|
} else {
|
|
cb(new Error('not connected!'))
|
|
}
|
|
} else {
|
|
next(new Error('unexpected action'))
|
|
}
|
|
},
|
|
done
|
|
)
|
|
})
|
|
}
|
|
|
|
describe(userDescription, function () {
|
|
beforeEach(function userSetup(done) {
|
|
userItem.setup((err, _options) => {
|
|
if (err) return done(err)
|
|
options = _options
|
|
done()
|
|
})
|
|
})
|
|
|
|
Object.entries(SESSION_SETUP).forEach(level1 => {
|
|
const [sessionSetupDescription, sessionSetupItem] = level1
|
|
const INVALID_REQUESTS = {
|
|
noop: {
|
|
getActions(cb) {
|
|
cb(null, [])
|
|
},
|
|
},
|
|
|
|
joinProjectWithBadAccessToken: {
|
|
getActions(cb) {
|
|
RealTimeClient.setAnonSession(
|
|
privateProjectId,
|
|
'invalid-access-token',
|
|
err => {
|
|
if (err) return cb(err)
|
|
cb(null, [
|
|
{
|
|
connect: privateProjectId,
|
|
fails: 1,
|
|
},
|
|
])
|
|
}
|
|
)
|
|
},
|
|
},
|
|
|
|
joinProjectWithDocId: {
|
|
getActions(cb) {
|
|
cb(null, [
|
|
{
|
|
connect: privateDocId,
|
|
fails: 1,
|
|
},
|
|
])
|
|
},
|
|
},
|
|
|
|
joinDocWithDocId: {
|
|
getActions(cb) {
|
|
cb(null, [{ rpc: 'joinDoc', args: [privateDocId], fails: 1 }])
|
|
},
|
|
},
|
|
|
|
joinProjectWithProjectId: {
|
|
getActions(cb) {
|
|
cb(null, [
|
|
{
|
|
connect: privateProjectId,
|
|
fails: 1,
|
|
},
|
|
])
|
|
},
|
|
},
|
|
|
|
joinDocWithProjectId: {
|
|
getActions(cb) {
|
|
cb(null, [{ rpc: 'joinDoc', args: [privateProjectId], fails: 1 }])
|
|
},
|
|
},
|
|
|
|
joinProjectWithProjectIdThenJoinDocWithDocId: {
|
|
getActions(cb) {
|
|
cb(null, [
|
|
{
|
|
connect: privateProjectId,
|
|
fails: 1,
|
|
},
|
|
{ rpc: 'joinDoc', args: [privateDocId], fails: 1 },
|
|
])
|
|
},
|
|
},
|
|
}
|
|
|
|
// skip some areas of the matrix
|
|
// - some Users do not have an own project
|
|
const skip = sessionSetupItem.needsOwnProject && !userItem.hasOwnProject
|
|
|
|
describe(sessionSetupDescription, function () {
|
|
beforeEach(function performSessionActions(done) {
|
|
if (skip) return this.skip()
|
|
performActions(sessionSetupItem.getActions, done)
|
|
})
|
|
|
|
Object.entries(INVALID_REQUESTS).forEach(level2 => {
|
|
const [InvalidRequestDescription, InvalidRequestItem] = level2
|
|
describe(InvalidRequestDescription, function () {
|
|
beforeEach(function performInvalidRequests(done) {
|
|
performActions(InvalidRequestItem.getActions, done)
|
|
})
|
|
|
|
describe('rooms', function () {
|
|
it('should not add the user into the privateProject room', function (done) {
|
|
RealTimeClient.getConnectedClient(
|
|
client.socket.sessionid,
|
|
(error, client) => {
|
|
if (error?.message === 'not found') return done() // disconnected
|
|
if (error) return done(error)
|
|
expect(client.rooms).to.not.include(privateProjectId)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
it('should not add the user into the privateDoc room', function (done) {
|
|
RealTimeClient.getConnectedClient(
|
|
client.socket.sessionid,
|
|
(error, client) => {
|
|
if (error?.message === 'not found') return done() // disconnected
|
|
if (error) return done(error)
|
|
expect(client.rooms).to.not.include(privateDocId)
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('receive updates', function () {
|
|
const receivedMessages = []
|
|
beforeEach(function publishAnUpdateInRedis(done) {
|
|
const update = {
|
|
doc_id: privateDocId,
|
|
op: {
|
|
meta: { source: privateClient.publicId },
|
|
v: 42,
|
|
doc: privateDocId,
|
|
op: [{ i: 'foo', p: 50 }],
|
|
},
|
|
}
|
|
client.on('otUpdateApplied', update => {
|
|
receivedMessages.push(update)
|
|
})
|
|
privateClient.once('otUpdateApplied', () => {
|
|
setTimeout(done, 10)
|
|
})
|
|
rclient.publish('applied-ops', JSON.stringify(update))
|
|
})
|
|
|
|
it('should send nothing to client', function () {
|
|
expect(receivedMessages).to.have.length(0)
|
|
})
|
|
})
|
|
|
|
describe('receive messages from web', function () {
|
|
const receivedMessages = []
|
|
beforeEach(function publishAMessageInRedis(done) {
|
|
const event = {
|
|
room_id: privateProjectId,
|
|
message: 'removeEntity',
|
|
payload: ['foo', 'convertDocToFile'],
|
|
_id: 'web:123',
|
|
}
|
|
client.on('removeEntity', (...args) => {
|
|
receivedMessages.push(args)
|
|
})
|
|
privateClient.once('removeEntity', () => {
|
|
setTimeout(done, 10)
|
|
})
|
|
rclient.publish('editor-events', JSON.stringify(event))
|
|
})
|
|
|
|
it('should send nothing to client', function () {
|
|
expect(receivedMessages).to.have.length(0)
|
|
})
|
|
})
|
|
|
|
describe('send updates', function () {
|
|
let receivedArgs, submittedUpdates, update
|
|
|
|
beforeEach(function cleanup(done) {
|
|
cleanupPreviousUpdates(privateDocId, done)
|
|
})
|
|
|
|
beforeEach(function setupUpdateFields() {
|
|
update = {
|
|
doc_id: privateDocId,
|
|
op: {
|
|
v: 43,
|
|
lastV: 42,
|
|
doc: privateDocId,
|
|
op: [{ i: 'foo', p: 50 }],
|
|
},
|
|
}
|
|
})
|
|
|
|
beforeEach(function sendAsUser(done) {
|
|
if (!client?.socket?.connected) {
|
|
// disconnected clients cannot emit messages
|
|
return this.skip()
|
|
}
|
|
const userUpdate = Object.assign({}, update, {
|
|
hash: 'user',
|
|
})
|
|
|
|
client.emit(
|
|
'applyOtUpdate',
|
|
privateDocId,
|
|
userUpdate,
|
|
(...args) => {
|
|
receivedArgs = args
|
|
done()
|
|
}
|
|
)
|
|
})
|
|
|
|
beforeEach(function sendAsPrivateUserForReferenceOp(done) {
|
|
const privateUpdate = Object.assign({}, update, {
|
|
hash: 'private',
|
|
})
|
|
|
|
privateClient.emit(
|
|
'applyOtUpdate',
|
|
privateDocId,
|
|
privateUpdate,
|
|
done
|
|
)
|
|
})
|
|
|
|
beforeEach(function fetchPendingOps(done) {
|
|
getPendingUpdates(privateDocId, (err, updates) => {
|
|
submittedUpdates = updates
|
|
done(err)
|
|
})
|
|
})
|
|
|
|
it('should error out trying to send', function () {
|
|
expect(receivedArgs).to.have.length(1)
|
|
expect(receivedArgs[0]).to.have.property('message')
|
|
// we are using an old version of chai: 1.9.2
|
|
// TypeError: expect(...).to.be.oneOf is not a function
|
|
expect(
|
|
[
|
|
'no project_id found on client',
|
|
'not authorized',
|
|
].includes(receivedArgs[0].message)
|
|
).to.equal(true)
|
|
})
|
|
|
|
it('should submit the private users message only', function () {
|
|
expect(submittedUpdates).to.have.length(1)
|
|
const update = JSON.parse(submittedUpdates[0])
|
|
expect(update.hash).to.equal('private')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|