Просмотр исходного кода

Merge pull request #194 from overleaf/jpa-matrix-tests

[MatrixTests] add a large testing matrix
Jakob Ackermann 6 лет назад
Родитель
Сommit
152f6c5596

+ 478 - 0
services/real-time/test/acceptance/js/MatrixTests.js

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

+ 16 - 0
services/real-time/test/acceptance/js/helpers/FixturesManager.js

@@ -109,6 +109,22 @@ module.exports = FixturesManager = {
     })
   },
 
+  setUpEditorSession(options, callback) {
+    FixturesManager.setUpProject(options, (err, detailsProject) => {
+      if (err) return callback(err)
+
+      FixturesManager.setUpDoc(
+        detailsProject.project_id,
+        options,
+        (err, detailsDoc) => {
+          if (err) return callback(err)
+
+          callback(null, Object.assign({}, detailsProject, detailsDoc))
+        }
+      )
+    })
+  },
+
   getRandomId() {
     return require('crypto')
       .createHash('sha1')

+ 3 - 0
services/real-time/test/acceptance/js/helpers/MockDocUpdaterServer.js

@@ -43,6 +43,9 @@ module.exports = MockDocUpdaterServer = {
         if (error != null) {
           return next(error)
         }
+        if (!data) {
+          return res.sendStatus(404)
+        }
         return res.json(data)
       }
     )

+ 2 - 1
services/real-time/test/acceptance/js/helpers/MockWebServer.js

@@ -31,7 +31,8 @@ module.exports = MockWebServer = {
     return callback(
       null,
       MockWebServer.projects[project_id],
-      MockWebServer.privileges[project_id][user_id]
+      MockWebServer.privileges[project_id][user_id] ||
+        MockWebServer.privileges[project_id]['anonymous-user']
     )
   },