MatrixTests.js 14 KB

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