mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 06:03:42 -05:00
[MatrixTests] add a large testing matrix
Layers/Dimensions: - users: anonymous, registered, registeredWithOwnedProject - session setup: noop, joinReadWriteProject, joinReadWriteProjectAndDoc, joinOwnProject, joinOwnProjectAndDoc - invalid requests: noop, joinProjectWithDocId, joinDocWithDocId, joinProjectWithProjectId, joinDocWithProjectId, joinProjectWithProjectIdThenJoinDocWithDocId
This commit is contained in:
parent
4ec73acb88
commit
e846192db0
1 changed files with 478 additions and 0 deletions
478
services/real-time/test/acceptance/js/MatrixTests.js
Normal file
478
services/real-time/test/acceptance/js/MatrixTests.js
Normal file
|
@ -0,0 +1,478 @@
|
|||
/*
|
||||
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 }
|
||||
*/
|
||||
/* eslint-disable
|
||||
camelcase,
|
||||
*/
|
||||
const chai = require('chai')
|
||||
const { expect } = chai
|
||||
const async = require('async')
|
||||
|
||||
const RealTimeClient = require('./helpers/RealTimeClient')
|
||||
const FixturesManager = require('./helpers/FixturesManager')
|
||||
|
||||
const settings = require('settings-sharelatex')
|
||||
const Keys = settings.redis.documentupdater.key_schema
|
||||
const redis = require('redis-sharelatex')
|
||||
const rclient = redis.createClient(settings.redis.pubsub)
|
||||
|
||||
function getPendingUpdates(doc_id, cb) {
|
||||
rclient.lrange(Keys.pendingUpdates({ doc_id }), 0, 10, cb)
|
||||
}
|
||||
function cleanupPreviousUpdates(doc_id, cb) {
|
||||
rclient.del(Keys.pendingUpdates({ doc_id }), cb)
|
||||
}
|
||||
|
||||
describe('MatrixTests', function () {
|
||||
let privateProjectId, privateDocId, readWriteProjectId, readWriteDocId
|
||||
|
||||
let privateClient
|
||||
before(function setupPrivateProject(done) {
|
||||
FixturesManager.setUpEditorSession(
|
||||
{ privilegeLevel: 'owner' },
|
||||
(err, { project_id, doc_id }) => {
|
||||
if (err) return done(err)
|
||||
privateProjectId = project_id
|
||||
privateDocId = doc_id
|
||||
privateClient = RealTimeClient.connect()
|
||||
privateClient.on('connectionAccepted', () => {
|
||||
privateClient.emit(
|
||||
'joinProject',
|
||||
{ project_id: privateProjectId },
|
||||
(err) => {
|
||||
if (err) return done(err)
|
||||
privateClient.emit('joinDoc', privateDocId, done)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
before(function setupReadWriteProject(done) {
|
||||
FixturesManager.setUpEditorSession(
|
||||
{
|
||||
publicAccess: 'readAndWrite'
|
||||
},
|
||||
(err, { project_id, doc_id }) => {
|
||||
readWriteProjectId = project_id
|
||||
readWriteDocId = doc_id
|
||||
done(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const USER_SETUP = {
|
||||
anonymous: {
|
||||
setup(cb) {
|
||||
RealTimeClient.setSession({}, (err) => {
|
||||
if (err) return cb(err)
|
||||
cb(null, {
|
||||
client: RealTimeClient.connect()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
registered: {
|
||||
setup(cb) {
|
||||
const user_id = FixturesManager.getRandomId()
|
||||
RealTimeClient.setSession(
|
||||
{
|
||||
user: {
|
||||
_id: user_id,
|
||||
first_name: 'Joe',
|
||||
last_name: 'Bloggs'
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
if (err) return cb(err)
|
||||
cb(null, {
|
||||
user_id,
|
||||
client: RealTimeClient.connect()
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
registeredWithOwnedProject: {
|
||||
setup(cb) {
|
||||
FixturesManager.setUpEditorSession(
|
||||
{ privilegeLevel: 'owner' },
|
||||
(err, { project_id, user_id, doc_id }) => {
|
||||
if (err) return cb(err)
|
||||
cb(null, {
|
||||
user_id,
|
||||
project_id,
|
||||
doc_id,
|
||||
client: RealTimeClient.connect()
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
hasOwnProject: true
|
||||
}
|
||||
}
|
||||
|
||||
Object.entries(USER_SETUP).forEach((level0) => {
|
||||
const [userDescription, userItem] = level0
|
||||
let options, client
|
||||
|
||||
const SESSION_SETUP = {
|
||||
noop: {
|
||||
getActions(cb) {
|
||||
cb(null, [])
|
||||
},
|
||||
needsOwnProject: false
|
||||
},
|
||||
|
||||
joinReadWriteProject: {
|
||||
getActions(cb) {
|
||||
cb(null, [
|
||||
{ rpc: 'joinProject', args: [{ project_id: readWriteProjectId }] }
|
||||
])
|
||||
},
|
||||
needsOwnProject: false
|
||||
},
|
||||
|
||||
joinReadWriteProjectAndDoc: {
|
||||
getActions(cb) {
|
||||
cb(null, [
|
||||
{ rpc: 'joinProject', args: [{ project_id: readWriteProjectId }] },
|
||||
{ rpc: 'joinDoc', args: [readWriteDocId] }
|
||||
])
|
||||
},
|
||||
needsOwnProject: false
|
||||
},
|
||||
|
||||
joinOwnProject: {
|
||||
getActions(cb) {
|
||||
cb(null, [
|
||||
{ rpc: 'joinProject', args: [{ project_id: options.project_id }] }
|
||||
])
|
||||
},
|
||||
needsOwnProject: true
|
||||
},
|
||||
|
||||
joinOwnProjectAndDoc: {
|
||||
getActions(cb) {
|
||||
cb(null, [
|
||||
{ rpc: 'joinProject', args: [{ project_id: 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, cb) => {
|
||||
if (action.rpc) {
|
||||
client.emit(action.rpc, ...action.args, (...returnedArgs) => {
|
||||
const error = returnedArgs.shift()
|
||||
if (action.fails) {
|
||||
expect(error).to.exist
|
||||
expect(returnedArgs).to.have.length(0)
|
||||
return cb()
|
||||
}
|
||||
cb(error)
|
||||
})
|
||||
} else {
|
||||
cb(new Error('unexpected action'))
|
||||
}
|
||||
},
|
||||
done
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
describe(userDescription, function () {
|
||||
beforeEach(function userSetup(done) {
|
||||
userItem.setup((err, _options) => {
|
||||
if (err) return done(err)
|
||||
|
||||
options = _options
|
||||
client = options.client
|
||||
client.on('connectionAccepted', done)
|
||||
})
|
||||
})
|
||||
|
||||
Object.entries(SESSION_SETUP).forEach((level1) => {
|
||||
const [sessionSetupDescription, sessionSetupItem] = level1
|
||||
const INVALID_REQUESTS = {
|
||||
noop: {
|
||||
getActions(cb) {
|
||||
cb(null, [])
|
||||
}
|
||||
},
|
||||
|
||||
joinProjectWithDocId: {
|
||||
getActions(cb) {
|
||||
cb(null, [
|
||||
{
|
||||
rpc: 'joinProject',
|
||||
args: [{ project_id: privateDocId }],
|
||||
fails: 1
|
||||
}
|
||||
])
|
||||
}
|
||||
},
|
||||
|
||||
joinDocWithDocId: {
|
||||
getActions(cb) {
|
||||
cb(null, [{ rpc: 'joinDoc', args: [privateDocId], fails: 1 }])
|
||||
}
|
||||
},
|
||||
|
||||
joinProjectWithProjectId: {
|
||||
getActions(cb) {
|
||||
cb(null, [
|
||||
{
|
||||
rpc: 'joinProject',
|
||||
args: [{ project_id: privateProjectId }],
|
||||
fails: 1
|
||||
}
|
||||
])
|
||||
}
|
||||
},
|
||||
|
||||
joinDocWithProjectId: {
|
||||
getActions(cb) {
|
||||
cb(null, [{ rpc: 'joinDoc', args: [privateProjectId], fails: 1 }])
|
||||
}
|
||||
},
|
||||
|
||||
joinProjectWithProjectIdThenJoinDocWithDocId: {
|
||||
getActions(cb) {
|
||||
cb(null, [
|
||||
{
|
||||
rpc: 'joinProject',
|
||||
args: [{ project_id: 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) 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) 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) {
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue