MatrixTests.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. /*
  2. This test suite is a multi level matrix which allows us to test many cases
  3. with all kinds of setups.
  4. Users/Actors are defined in USERS and are a low level entity that does connect
  5. to a real-time pod. A typical UserItem is:
  6. someDescriptiveNameForTheTestSuite: {
  7. setup(cb) {
  8. // <setup session here>
  9. const options = { client: RealTimeClient.connect(), foo: 'bar' }
  10. cb(null, options)
  11. }
  12. }
  13. Sessions are a set of actions that a User performs in the life-cycle of a
  14. real-time session, before they try something weird. A typical SessionItem is:
  15. someOtherDescriptiveNameForTheTestSuite: {
  16. getActions(cb) {
  17. cb(null, [
  18. { rpc: 'RPC_ENDPOINT', args: [...] }
  19. ])
  20. }
  21. }
  22. Finally there are InvalidRequests which are the weird actions I hinted on in
  23. the Sessions section. The defined actions may be marked as 'failed' to denote
  24. that real-time rejects them with an (for this test) expected error.
  25. A typical InvalidRequestItem is:
  26. joinOwnProject: {
  27. getActions(cb) {
  28. cb(null, [
  29. { rpc: 'RPC_ENDPOINT', args: [...], failed: true }
  30. ])
  31. }
  32. }
  33. There is additional meta-data that UserItems and SessionItems may use to skip
  34. certain areas of the matrix. Theses are:
  35. - Has the User an own project that they join as part of the Session?
  36. UserItem: { hasOwnProject: true, setup(cb) { cb(null, { project_id, ... }) }}
  37. SessionItem: { needsOwnProject: true }
  38. */
  39. const { expect } = require('chai')
  40. const async = require('async')
  41. const RealTimeClient = require('./helpers/RealTimeClient')
  42. const FixturesManager = require('./helpers/FixturesManager')
  43. const settings = require('@overleaf/settings')
  44. const Keys = settings.redis.documentupdater.key_schema
  45. const redis = require('@overleaf/redis-wrapper')
  46. const rclient = redis.createClient(settings.redis.pubsub)
  47. function getPendingUpdates(docId, cb) {
  48. rclient.lrange(Keys.pendingUpdates({ doc_id: docId }), 0, 10, cb)
  49. }
  50. function cleanupPreviousUpdates(docId, cb) {
  51. rclient.del(Keys.pendingUpdates({ doc_id: docId }), cb)
  52. }
  53. describe('MatrixTests', function () {
  54. let privateProjectId, privateDocId, readWriteProjectId, readWriteDocId
  55. let privateClient
  56. before(function setupPrivateProject(done) {
  57. FixturesManager.setUpEditorSession(
  58. { privilegeLevel: 'owner' },
  59. (err, { project_id: projectId, doc_id: docId }) => {
  60. if (err) return done(err)
  61. privateProjectId = projectId
  62. privateDocId = docId
  63. privateClient = RealTimeClient.connect()
  64. privateClient.on('connectionAccepted', () => {
  65. privateClient.emit(
  66. 'joinProject',
  67. { project_id: privateProjectId },
  68. err => {
  69. if (err) return done(err)
  70. privateClient.emit('joinDoc', privateDocId, done)
  71. }
  72. )
  73. })
  74. }
  75. )
  76. })
  77. before(function setupReadWriteProject(done) {
  78. FixturesManager.setUpEditorSession(
  79. {
  80. publicAccess: 'readAndWrite',
  81. },
  82. (err, { project_id: projectId, doc_id: docId }) => {
  83. readWriteProjectId = projectId
  84. readWriteDocId = docId
  85. done(err)
  86. }
  87. )
  88. })
  89. const USER_SETUP = {
  90. anonymous: {
  91. setup(cb) {
  92. RealTimeClient.setSession({}, err => {
  93. if (err) return cb(err)
  94. cb(null, {
  95. client: RealTimeClient.connect(),
  96. })
  97. })
  98. },
  99. },
  100. registered: {
  101. setup(cb) {
  102. const userId = FixturesManager.getRandomId()
  103. RealTimeClient.setSession(
  104. {
  105. user: {
  106. _id: userId,
  107. first_name: 'Joe',
  108. last_name: 'Bloggs',
  109. },
  110. },
  111. err => {
  112. if (err) return cb(err)
  113. cb(null, {
  114. user_id: userId,
  115. client: RealTimeClient.connect(),
  116. })
  117. }
  118. )
  119. },
  120. },
  121. registeredWithOwnedProject: {
  122. setup(cb) {
  123. FixturesManager.setUpEditorSession(
  124. { privilegeLevel: 'owner' },
  125. (err, { project_id: projectId, user_id: userId, doc_id: docId }) => {
  126. if (err) return cb(err)
  127. cb(null, {
  128. user_id: userId,
  129. project_id: projectId,
  130. doc_id: docId,
  131. client: RealTimeClient.connect(),
  132. })
  133. }
  134. )
  135. },
  136. hasOwnProject: true,
  137. },
  138. }
  139. Object.entries(USER_SETUP).forEach(level0 => {
  140. const [userDescription, userItem] = level0
  141. let options, client
  142. const SESSION_SETUP = {
  143. noop: {
  144. getActions(cb) {
  145. cb(null, [])
  146. },
  147. needsOwnProject: false,
  148. },
  149. joinReadWriteProject: {
  150. getActions(cb) {
  151. cb(null, [
  152. { rpc: 'joinProject', args: [{ project_id: readWriteProjectId }] },
  153. ])
  154. },
  155. needsOwnProject: false,
  156. },
  157. joinReadWriteProjectAndDoc: {
  158. getActions(cb) {
  159. cb(null, [
  160. { rpc: 'joinProject', args: [{ project_id: readWriteProjectId }] },
  161. { rpc: 'joinDoc', args: [readWriteDocId] },
  162. ])
  163. },
  164. needsOwnProject: false,
  165. },
  166. joinOwnProject: {
  167. getActions(cb) {
  168. cb(null, [
  169. { rpc: 'joinProject', args: [{ project_id: options.project_id }] },
  170. ])
  171. },
  172. needsOwnProject: true,
  173. },
  174. joinOwnProjectAndDoc: {
  175. getActions(cb) {
  176. cb(null, [
  177. { rpc: 'joinProject', args: [{ project_id: options.project_id }] },
  178. { rpc: 'joinDoc', args: [options.doc_id] },
  179. ])
  180. },
  181. needsOwnProject: true,
  182. },
  183. }
  184. function performActions(getActions, done) {
  185. getActions((err, actions) => {
  186. if (err) return done(err)
  187. async.eachSeries(
  188. actions,
  189. (action, cb) => {
  190. if (action.rpc) {
  191. client.emit(action.rpc, ...action.args, (...returnedArgs) => {
  192. const error = returnedArgs.shift()
  193. if (action.fails) {
  194. expect(error).to.exist
  195. expect(returnedArgs).to.have.length(0)
  196. return cb()
  197. }
  198. cb(error)
  199. })
  200. } else {
  201. cb(new Error('unexpected action'))
  202. }
  203. },
  204. done
  205. )
  206. })
  207. }
  208. describe(userDescription, function () {
  209. beforeEach(function userSetup(done) {
  210. userItem.setup((err, _options) => {
  211. if (err) return done(err)
  212. options = _options
  213. client = options.client
  214. client.on('connectionAccepted', done)
  215. })
  216. })
  217. Object.entries(SESSION_SETUP).forEach(level1 => {
  218. const [sessionSetupDescription, sessionSetupItem] = level1
  219. const INVALID_REQUESTS = {
  220. noop: {
  221. getActions(cb) {
  222. cb(null, [])
  223. },
  224. },
  225. joinProjectWithDocId: {
  226. getActions(cb) {
  227. cb(null, [
  228. {
  229. rpc: 'joinProject',
  230. args: [{ project_id: privateDocId }],
  231. fails: 1,
  232. },
  233. ])
  234. },
  235. },
  236. joinDocWithDocId: {
  237. getActions(cb) {
  238. cb(null, [{ rpc: 'joinDoc', args: [privateDocId], fails: 1 }])
  239. },
  240. },
  241. joinProjectWithProjectId: {
  242. getActions(cb) {
  243. cb(null, [
  244. {
  245. rpc: 'joinProject',
  246. args: [{ project_id: privateProjectId }],
  247. fails: 1,
  248. },
  249. ])
  250. },
  251. },
  252. joinDocWithProjectId: {
  253. getActions(cb) {
  254. cb(null, [{ rpc: 'joinDoc', args: [privateProjectId], fails: 1 }])
  255. },
  256. },
  257. joinProjectWithProjectIdThenJoinDocWithDocId: {
  258. getActions(cb) {
  259. cb(null, [
  260. {
  261. rpc: 'joinProject',
  262. args: [{ project_id: privateProjectId }],
  263. fails: 1,
  264. },
  265. { rpc: 'joinDoc', args: [privateDocId], fails: 1 },
  266. ])
  267. },
  268. },
  269. }
  270. // skip some areas of the matrix
  271. // - some Users do not have an own project
  272. const skip = sessionSetupItem.needsOwnProject && !userItem.hasOwnProject
  273. describe(sessionSetupDescription, function () {
  274. beforeEach(function performSessionActions(done) {
  275. if (skip) return this.skip()
  276. performActions(sessionSetupItem.getActions, done)
  277. })
  278. Object.entries(INVALID_REQUESTS).forEach(level2 => {
  279. const [InvalidRequestDescription, InvalidRequestItem] = level2
  280. describe(InvalidRequestDescription, function () {
  281. beforeEach(function performInvalidRequests(done) {
  282. performActions(InvalidRequestItem.getActions, done)
  283. })
  284. describe('rooms', function () {
  285. it('should not add the user into the privateProject room', function (done) {
  286. RealTimeClient.getConnectedClient(
  287. client.socket.sessionid,
  288. (error, client) => {
  289. if (error) return done(error)
  290. expect(client.rooms).to.not.include(privateProjectId)
  291. done()
  292. }
  293. )
  294. })
  295. it('should not add the user into the privateDoc room', function (done) {
  296. RealTimeClient.getConnectedClient(
  297. client.socket.sessionid,
  298. (error, client) => {
  299. if (error) return done(error)
  300. expect(client.rooms).to.not.include(privateDocId)
  301. done()
  302. }
  303. )
  304. })
  305. })
  306. describe('receive updates', function () {
  307. const receivedMessages = []
  308. beforeEach(function publishAnUpdateInRedis(done) {
  309. const update = {
  310. doc_id: privateDocId,
  311. op: {
  312. meta: { source: privateClient.publicId },
  313. v: 42,
  314. doc: privateDocId,
  315. op: [{ i: 'foo', p: 50 }],
  316. },
  317. }
  318. client.on('otUpdateApplied', update => {
  319. receivedMessages.push(update)
  320. })
  321. privateClient.once('otUpdateApplied', () => {
  322. setTimeout(done, 10)
  323. })
  324. rclient.publish('applied-ops', JSON.stringify(update))
  325. })
  326. it('should send nothing to client', function () {
  327. expect(receivedMessages).to.have.length(0)
  328. })
  329. })
  330. describe('receive messages from web', function () {
  331. const receivedMessages = []
  332. beforeEach(function publishAMessageInRedis(done) {
  333. const event = {
  334. room_id: privateProjectId,
  335. message: 'removeEntity',
  336. payload: ['foo', 'convertDocToFile'],
  337. _id: 'web:123',
  338. }
  339. client.on('removeEntity', (...args) => {
  340. receivedMessages.push(args)
  341. })
  342. privateClient.once('removeEntity', () => {
  343. setTimeout(done, 10)
  344. })
  345. rclient.publish('editor-events', JSON.stringify(event))
  346. })
  347. it('should send nothing to client', function () {
  348. expect(receivedMessages).to.have.length(0)
  349. })
  350. })
  351. describe('send updates', function () {
  352. let receivedArgs, submittedUpdates, update
  353. beforeEach(function cleanup(done) {
  354. cleanupPreviousUpdates(privateDocId, done)
  355. })
  356. beforeEach(function setupUpdateFields() {
  357. update = {
  358. doc_id: privateDocId,
  359. op: {
  360. v: 43,
  361. lastV: 42,
  362. doc: privateDocId,
  363. op: [{ i: 'foo', p: 50 }],
  364. },
  365. }
  366. })
  367. beforeEach(function sendAsUser(done) {
  368. const userUpdate = Object.assign({}, update, {
  369. hash: 'user',
  370. })
  371. client.emit(
  372. 'applyOtUpdate',
  373. privateDocId,
  374. userUpdate,
  375. (...args) => {
  376. receivedArgs = args
  377. done()
  378. }
  379. )
  380. })
  381. beforeEach(function sendAsPrivateUserForReferenceOp(done) {
  382. const privateUpdate = Object.assign({}, update, {
  383. hash: 'private',
  384. })
  385. privateClient.emit(
  386. 'applyOtUpdate',
  387. privateDocId,
  388. privateUpdate,
  389. done
  390. )
  391. })
  392. beforeEach(function fetchPendingOps(done) {
  393. getPendingUpdates(privateDocId, (err, updates) => {
  394. submittedUpdates = updates
  395. done(err)
  396. })
  397. })
  398. it('should error out trying to send', function () {
  399. expect(receivedArgs).to.have.length(1)
  400. expect(receivedArgs[0]).to.have.property('message')
  401. // we are using an old version of chai: 1.9.2
  402. // TypeError: expect(...).to.be.oneOf is not a function
  403. expect(
  404. [
  405. 'no project_id found on client',
  406. 'not authorized',
  407. ].includes(receivedArgs[0].message)
  408. ).to.equal(true)
  409. })
  410. it('should submit the private users message only', function () {
  411. expect(submittedUpdates).to.have.length(1)
  412. const update = JSON.parse(submittedUpdates[0])
  413. expect(update.hash).to.equal('private')
  414. })
  415. })
  416. })
  417. })
  418. })
  419. })
  420. })
  421. })
  422. })