ApplyUpdateTests.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. /* eslint-disable
  2. no-return-assign,
  3. */
  4. // TODO: This file was created by bulk-decaffeinate.
  5. // Fix any style issues and re-enable lint.
  6. /*
  7. * decaffeinate suggestions:
  8. * DS101: Remove unnecessary use of Array.from
  9. * DS102: Remove unnecessary code created because of implicit returns
  10. * DS201: Simplify complex destructure assignments
  11. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  12. */
  13. const async = require('async')
  14. const { expect } = require('chai')
  15. const RealTimeClient = require('./helpers/RealTimeClient')
  16. const FixturesManager = require('./helpers/FixturesManager')
  17. const settings = require('@overleaf/settings')
  18. const redis = require('@overleaf/redis-wrapper')
  19. const rclient = redis.createClient(settings.redis.documentupdater)
  20. const redisSettings = settings.redis
  21. const PENDING_UPDATES_LIST_KEYS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(n => {
  22. let key = 'pending-updates-list'
  23. if (n !== 0) {
  24. key += `-${n}`
  25. }
  26. return key
  27. })
  28. function getPendingUpdatesList(cb) {
  29. Promise.all(PENDING_UPDATES_LIST_KEYS.map(key => rclient.lrange(key, 0, -1)))
  30. .then(results => {
  31. cb(
  32. null,
  33. results.reduce((acc, more) => {
  34. if (more.length) {
  35. acc.push(...more)
  36. }
  37. return acc
  38. }, [])
  39. )
  40. })
  41. .catch(cb)
  42. }
  43. function clearPendingUpdatesList(cb) {
  44. Promise.all(PENDING_UPDATES_LIST_KEYS.map(key => rclient.del(key)))
  45. .then(() => cb(null))
  46. .catch(cb)
  47. }
  48. describe('applyOtUpdate', function () {
  49. before(function () {
  50. return (this.update = {
  51. op: [{ i: 'foo', p: 42 }],
  52. })
  53. })
  54. describe('when authorized', function () {
  55. before(function (done) {
  56. return async.series(
  57. [
  58. cb => {
  59. return FixturesManager.setUpProject(
  60. {
  61. privilegeLevel: 'readAndWrite',
  62. },
  63. (e, { project_id: projectId, user_id: userId }) => {
  64. this.project_id = projectId
  65. this.user_id = userId
  66. return cb(e)
  67. }
  68. )
  69. },
  70. cb => {
  71. return FixturesManager.setUpDoc(
  72. this.project_id,
  73. { lines: this.lines, version: this.version, ops: this.ops },
  74. (e, { doc_id: docId }) => {
  75. this.doc_id = docId
  76. return cb(e)
  77. }
  78. )
  79. },
  80. cb => {
  81. this.client = RealTimeClient.connect()
  82. return this.client.on('connectionAccepted', cb)
  83. },
  84. cb => {
  85. return this.client.emit(
  86. 'joinProject',
  87. { project_id: this.project_id },
  88. cb
  89. )
  90. },
  91. cb => {
  92. return this.client.emit('joinDoc', this.doc_id, cb)
  93. },
  94. cb => {
  95. return this.client.emit(
  96. 'applyOtUpdate',
  97. this.doc_id,
  98. this.update,
  99. cb
  100. )
  101. },
  102. ],
  103. done
  104. )
  105. })
  106. it('should push the doc into the pending updates list', function (done) {
  107. getPendingUpdatesList((error, ...rest) => {
  108. if (error) return done(error)
  109. const [docId] = Array.from(rest[0])
  110. docId.should.equal(`${this.project_id}:${this.doc_id}`)
  111. return done()
  112. })
  113. return null
  114. })
  115. it('should push the update into redis', function (done) {
  116. rclient.lrange(
  117. redisSettings.documentupdater.key_schema.pendingUpdates({
  118. doc_id: this.doc_id,
  119. }),
  120. 0,
  121. -1,
  122. (error, ...rest) => {
  123. if (error) return done(error)
  124. let [update] = Array.from(rest[0])
  125. update = JSON.parse(update)
  126. update.op.should.deep.equal(this.update.op)
  127. update.meta.should.deep.equal({
  128. source: this.client.publicId,
  129. user_id: this.user_id,
  130. })
  131. return done()
  132. }
  133. )
  134. return null
  135. })
  136. return after(function (done) {
  137. return async.series(
  138. [
  139. cb => clearPendingUpdatesList(cb),
  140. cb =>
  141. rclient.del(
  142. 'DocsWithPendingUpdates',
  143. `${this.project_id}:${this.doc_id}`,
  144. cb
  145. ),
  146. cb =>
  147. rclient.del(
  148. redisSettings.documentupdater.key_schema.pendingUpdates(
  149. this.doc_id
  150. ),
  151. cb
  152. ),
  153. ],
  154. done
  155. )
  156. })
  157. })
  158. describe('when authorized with a huge edit update', function () {
  159. before(function (done) {
  160. this.update = {
  161. op: {
  162. p: 12,
  163. t: 'update is too large'.repeat(1024 * 400), // >7MB
  164. },
  165. }
  166. return async.series(
  167. [
  168. cb => {
  169. return FixturesManager.setUpProject(
  170. {
  171. privilegeLevel: 'readAndWrite',
  172. },
  173. (e, { project_id: projectId, user_id: userId }) => {
  174. this.project_id = projectId
  175. this.user_id = userId
  176. return cb(e)
  177. }
  178. )
  179. },
  180. cb => {
  181. return FixturesManager.setUpDoc(
  182. this.project_id,
  183. { lines: this.lines, version: this.version, ops: this.ops },
  184. (e, { doc_id: docId }) => {
  185. this.doc_id = docId
  186. return cb(e)
  187. }
  188. )
  189. },
  190. cb => {
  191. this.client = RealTimeClient.connect()
  192. this.client.on('connectionAccepted', cb)
  193. return this.client.on('otUpdateError', otUpdateError => {
  194. this.otUpdateError = otUpdateError
  195. })
  196. },
  197. cb => {
  198. return this.client.emit(
  199. 'joinProject',
  200. { project_id: this.project_id },
  201. cb
  202. )
  203. },
  204. cb => {
  205. return this.client.emit('joinDoc', this.doc_id, cb)
  206. },
  207. cb => {
  208. return this.client.emit(
  209. 'applyOtUpdate',
  210. this.doc_id,
  211. this.update,
  212. error => {
  213. this.error = error
  214. return cb()
  215. }
  216. )
  217. },
  218. ],
  219. done
  220. )
  221. })
  222. it('should not return an error', function () {
  223. return expect(this.error).to.not.exist
  224. })
  225. it('should send an otUpdateError to the client', function (done) {
  226. return setTimeout(() => {
  227. expect(this.otUpdateError).to.exist
  228. return done()
  229. }, 300)
  230. })
  231. it('should disconnect the client', function (done) {
  232. return setTimeout(() => {
  233. this.client.socket.connected.should.equal(false)
  234. return done()
  235. }, 300)
  236. })
  237. return it('should not put the update in redis', function (done) {
  238. rclient.llen(
  239. redisSettings.documentupdater.key_schema.pendingUpdates({
  240. doc_id: this.doc_id,
  241. }),
  242. (error, len) => {
  243. if (error) return done(error)
  244. len.should.equal(0)
  245. return done()
  246. }
  247. )
  248. return null
  249. })
  250. })
  251. describe('when authorized to read-only with an edit update', function () {
  252. before(function (done) {
  253. return async.series(
  254. [
  255. cb => {
  256. return FixturesManager.setUpProject(
  257. {
  258. privilegeLevel: 'readOnly',
  259. },
  260. (e, { project_id: projectId, user_id: userId }) => {
  261. this.project_id = projectId
  262. this.user_id = userId
  263. return cb(e)
  264. }
  265. )
  266. },
  267. cb => {
  268. return FixturesManager.setUpDoc(
  269. this.project_id,
  270. { lines: this.lines, version: this.version, ops: this.ops },
  271. (e, { doc_id: docId }) => {
  272. this.doc_id = docId
  273. return cb(e)
  274. }
  275. )
  276. },
  277. cb => {
  278. this.client = RealTimeClient.connect()
  279. return this.client.on('connectionAccepted', cb)
  280. },
  281. cb => {
  282. return this.client.emit(
  283. 'joinProject',
  284. { project_id: this.project_id },
  285. cb
  286. )
  287. },
  288. cb => {
  289. return this.client.emit('joinDoc', this.doc_id, cb)
  290. },
  291. cb => {
  292. return this.client.emit(
  293. 'applyOtUpdate',
  294. this.doc_id,
  295. this.update,
  296. error => {
  297. this.error = error
  298. return cb()
  299. }
  300. )
  301. },
  302. ],
  303. done
  304. )
  305. })
  306. it('should return an error', function () {
  307. return expect(this.error).to.exist
  308. })
  309. it('should disconnect the client', function (done) {
  310. return setTimeout(() => {
  311. this.client.socket.connected.should.equal(false)
  312. return done()
  313. }, 300)
  314. })
  315. return it('should not put the update in redis', function (done) {
  316. rclient.llen(
  317. redisSettings.documentupdater.key_schema.pendingUpdates({
  318. doc_id: this.doc_id,
  319. }),
  320. (error, len) => {
  321. if (error) return done(error)
  322. len.should.equal(0)
  323. return done()
  324. }
  325. )
  326. return null
  327. })
  328. })
  329. describe('when authorized to read-only with a comment update', function () {
  330. before(function (done) {
  331. this.comment_update = {
  332. op: [{ c: 'foo', p: 42 }],
  333. }
  334. return async.series(
  335. [
  336. cb => {
  337. return FixturesManager.setUpProject(
  338. {
  339. privilegeLevel: 'readOnly',
  340. },
  341. (e, { project_id: projectId, user_id: userId }) => {
  342. this.project_id = projectId
  343. this.user_id = userId
  344. return cb(e)
  345. }
  346. )
  347. },
  348. cb => {
  349. return FixturesManager.setUpDoc(
  350. this.project_id,
  351. { lines: this.lines, version: this.version, ops: this.ops },
  352. (e, { doc_id: docId }) => {
  353. this.doc_id = docId
  354. return cb(e)
  355. }
  356. )
  357. },
  358. cb => {
  359. this.client = RealTimeClient.connect()
  360. return this.client.on('connectionAccepted', cb)
  361. },
  362. cb => {
  363. return this.client.emit(
  364. 'joinProject',
  365. { project_id: this.project_id },
  366. cb
  367. )
  368. },
  369. cb => {
  370. return this.client.emit('joinDoc', this.doc_id, cb)
  371. },
  372. cb => {
  373. return this.client.emit(
  374. 'applyOtUpdate',
  375. this.doc_id,
  376. this.comment_update,
  377. cb
  378. )
  379. },
  380. ],
  381. done
  382. )
  383. })
  384. it('should push the doc into the pending updates list', function (done) {
  385. getPendingUpdatesList((error, ...rest) => {
  386. if (error) return done(error)
  387. const [docId] = Array.from(rest[0])
  388. docId.should.equal(`${this.project_id}:${this.doc_id}`)
  389. return done()
  390. })
  391. return null
  392. })
  393. it('should push the update into redis', function (done) {
  394. rclient.lrange(
  395. redisSettings.documentupdater.key_schema.pendingUpdates({
  396. doc_id: this.doc_id,
  397. }),
  398. 0,
  399. -1,
  400. (error, ...rest) => {
  401. if (error) return done(error)
  402. let [update] = Array.from(rest[0])
  403. update = JSON.parse(update)
  404. update.op.should.deep.equal(this.comment_update.op)
  405. update.meta.should.deep.equal({
  406. source: this.client.publicId,
  407. user_id: this.user_id,
  408. })
  409. return done()
  410. }
  411. )
  412. return null
  413. })
  414. return after(function (done) {
  415. return async.series(
  416. [
  417. cb => clearPendingUpdatesList(cb),
  418. cb =>
  419. rclient.del(
  420. 'DocsWithPendingUpdates',
  421. `${this.project_id}:${this.doc_id}`,
  422. cb
  423. ),
  424. cb =>
  425. rclient.del(
  426. redisSettings.documentupdater.key_schema.pendingUpdates({
  427. doc_id: this.doc_id,
  428. }),
  429. cb
  430. ),
  431. ],
  432. done
  433. )
  434. })
  435. })
  436. describe('when authorized with an edit update to an invalid doc', function () {
  437. before(function (done) {
  438. return async.series(
  439. [
  440. cb => {
  441. return FixturesManager.setUpProject(
  442. {
  443. privilegeLevel: 'readOnly',
  444. },
  445. (e, { project_id: projectId, user_id: userId }) => {
  446. this.project_id = projectId
  447. this.user_id = userId
  448. return cb(e)
  449. }
  450. )
  451. },
  452. cb => {
  453. return FixturesManager.setUpDoc(
  454. this.project_id,
  455. { lines: this.lines, version: this.version, ops: this.ops },
  456. (e, { doc_id: docId }) => {
  457. this.doc_id = docId
  458. return cb(e)
  459. }
  460. )
  461. },
  462. cb => {
  463. this.client = RealTimeClient.connect()
  464. return this.client.on('connectionAccepted', cb)
  465. },
  466. cb => {
  467. return this.client.emit(
  468. 'joinProject',
  469. { project_id: this.project_id },
  470. cb
  471. )
  472. },
  473. cb => {
  474. return this.client.emit('joinDoc', this.doc_id, cb)
  475. },
  476. cb => {
  477. return this.client.emit(
  478. 'applyOtUpdate',
  479. 'invalid-doc-id',
  480. this.update,
  481. error => {
  482. this.error = error
  483. return cb()
  484. }
  485. )
  486. },
  487. ],
  488. done
  489. )
  490. })
  491. it('should return an error', function () {
  492. return expect(this.error).to.exist
  493. })
  494. it('should disconnect the client', function (done) {
  495. return setTimeout(() => {
  496. this.client.socket.connected.should.equal(false)
  497. return done()
  498. }, 300)
  499. })
  500. return it('should not put the update in redis', function (done) {
  501. rclient.llen(
  502. redisSettings.documentupdater.key_schema.pendingUpdates({
  503. doc_id: this.doc_id,
  504. }),
  505. (error, len) => {
  506. if (error) return done(error)
  507. len.should.equal(0)
  508. return done()
  509. }
  510. )
  511. return null
  512. })
  513. })
  514. describe('when authorized with an invalid edit update', function () {
  515. before(function (done) {
  516. return async.series(
  517. [
  518. cb => {
  519. return FixturesManager.setUpProject(
  520. {
  521. privilegeLevel: 'readAndWrite',
  522. },
  523. (e, { project_id: projectId, user_id: userId }) => {
  524. this.project_id = projectId
  525. this.user_id = userId
  526. return cb(e)
  527. }
  528. )
  529. },
  530. cb => {
  531. return FixturesManager.setUpDoc(
  532. this.project_id,
  533. { lines: this.lines, version: this.version, ops: this.ops },
  534. (e, { doc_id: docId }) => {
  535. this.doc_id = docId
  536. return cb(e)
  537. }
  538. )
  539. },
  540. cb => {
  541. this.client = RealTimeClient.connect()
  542. return this.client.on('connectionAccepted', cb)
  543. },
  544. cb => {
  545. return this.client.emit(
  546. 'joinProject',
  547. { project_id: this.project_id },
  548. cb
  549. )
  550. },
  551. cb => {
  552. return this.client.emit('joinDoc', this.doc_id, cb)
  553. },
  554. cb => {
  555. return this.client.emit(
  556. 'applyOtUpdate',
  557. this.doc_id,
  558. 'invalid-update',
  559. error => {
  560. this.error = error
  561. return cb()
  562. }
  563. )
  564. },
  565. ],
  566. done
  567. )
  568. })
  569. it('should return an error', function () {
  570. return expect(this.error).to.exist
  571. })
  572. it('should disconnect the client', function (done) {
  573. return setTimeout(() => {
  574. this.client.socket.connected.should.equal(false)
  575. return done()
  576. }, 300)
  577. })
  578. return it('should not put the update in redis', function (done) {
  579. rclient.llen(
  580. redisSettings.documentupdater.key_schema.pendingUpdates({
  581. doc_id: this.doc_id,
  582. }),
  583. (error, len) => {
  584. if (error) return done(error)
  585. len.should.equal(0)
  586. return done()
  587. }
  588. )
  589. return null
  590. })
  591. })
  592. })