|
@@ -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')
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+})
|