ApplyUpdateTests.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763
  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. import async from 'async'
  14. import { expect } from 'chai'
  15. import RealTimeClient from './helpers/RealTimeClient.js'
  16. import FixturesManager from './helpers/FixturesManager.js'
  17. import settings from '@overleaf/settings'
  18. import redis from '@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(this.project_id, cb)
  82. },
  83. cb => {
  84. return this.client.emit('joinDoc', this.doc_id, cb)
  85. },
  86. cb => {
  87. return this.client.emit(
  88. 'applyOtUpdate',
  89. this.doc_id,
  90. this.update,
  91. cb
  92. )
  93. },
  94. ],
  95. done
  96. )
  97. })
  98. it('should push the doc into the pending updates list', function (done) {
  99. getPendingUpdatesList((error, ...rest) => {
  100. if (error) return done(error)
  101. const [docId] = Array.from(rest[0])
  102. docId.should.equal(`${this.project_id}:${this.doc_id}`)
  103. return done()
  104. })
  105. return null
  106. })
  107. it('should push the update into redis', function (done) {
  108. rclient.lrange(
  109. redisSettings.documentupdater.key_schema.pendingUpdates({
  110. doc_id: this.doc_id,
  111. }),
  112. 0,
  113. -1,
  114. (error, ...rest) => {
  115. if (error) return done(error)
  116. let [update] = Array.from(rest[0])
  117. update = JSON.parse(update)
  118. update.op.should.deep.equal(this.update.op)
  119. update.meta.should.include({
  120. source: this.client.publicId,
  121. user_id: this.user_id,
  122. })
  123. return done()
  124. }
  125. )
  126. return null
  127. })
  128. return after(function (done) {
  129. return async.series(
  130. [
  131. cb => clearPendingUpdatesList(cb),
  132. cb =>
  133. rclient.del(
  134. 'DocsWithPendingUpdates',
  135. `${this.project_id}:${this.doc_id}`,
  136. cb
  137. ),
  138. cb =>
  139. rclient.del(
  140. redisSettings.documentupdater.key_schema.pendingUpdates(
  141. this.doc_id
  142. ),
  143. cb
  144. ),
  145. ],
  146. done
  147. )
  148. })
  149. })
  150. describe('when authorized with a huge edit update', function () {
  151. before(function (done) {
  152. this.update = {
  153. op: {
  154. p: 12,
  155. t: 'update is too large'.repeat(1024 * 400), // >7MB
  156. },
  157. }
  158. return async.series(
  159. [
  160. cb => {
  161. return FixturesManager.setUpProject(
  162. {
  163. privilegeLevel: 'readAndWrite',
  164. },
  165. (e, { project_id: projectId, user_id: userId }) => {
  166. this.project_id = projectId
  167. this.user_id = userId
  168. return cb(e)
  169. }
  170. )
  171. },
  172. cb => {
  173. return FixturesManager.setUpDoc(
  174. this.project_id,
  175. { lines: this.lines, version: this.version, ops: this.ops },
  176. (e, { doc_id: docId }) => {
  177. this.doc_id = docId
  178. return cb(e)
  179. }
  180. )
  181. },
  182. cb => {
  183. this.client = RealTimeClient.connect(this.project_id, cb)
  184. return this.client.on('otUpdateError', otUpdateError => {
  185. this.otUpdateError = otUpdateError
  186. })
  187. },
  188. cb => {
  189. return this.client.emit('joinDoc', this.doc_id, cb)
  190. },
  191. cb => {
  192. return this.client.emit(
  193. 'applyOtUpdate',
  194. this.doc_id,
  195. this.update,
  196. error => {
  197. this.error = error
  198. return cb()
  199. }
  200. )
  201. },
  202. ],
  203. done
  204. )
  205. })
  206. it('should not return an error', function () {
  207. return expect(this.error).to.not.exist
  208. })
  209. it('should send an otUpdateError to the client', function (done) {
  210. return setTimeout(() => {
  211. expect(this.otUpdateError).to.exist
  212. return done()
  213. }, 300)
  214. })
  215. it('should disconnect the client', function (done) {
  216. return setTimeout(() => {
  217. this.client.socket.connected.should.equal(false)
  218. return done()
  219. }, 300)
  220. })
  221. return it('should not put the update in redis', function (done) {
  222. rclient.llen(
  223. redisSettings.documentupdater.key_schema.pendingUpdates({
  224. doc_id: this.doc_id,
  225. }),
  226. (error, len) => {
  227. if (error) return done(error)
  228. len.should.equal(0)
  229. return done()
  230. }
  231. )
  232. return null
  233. })
  234. })
  235. describe('when authorized to read-only with an edit update', function () {
  236. before(function (done) {
  237. return async.series(
  238. [
  239. cb => {
  240. return FixturesManager.setUpProject(
  241. {
  242. privilegeLevel: 'readOnly',
  243. },
  244. (e, { project_id: projectId, user_id: userId }) => {
  245. this.project_id = projectId
  246. this.user_id = userId
  247. return cb(e)
  248. }
  249. )
  250. },
  251. cb => {
  252. return FixturesManager.setUpDoc(
  253. this.project_id,
  254. { lines: this.lines, version: this.version, ops: this.ops },
  255. (e, { doc_id: docId }) => {
  256. this.doc_id = docId
  257. return cb(e)
  258. }
  259. )
  260. },
  261. cb => {
  262. this.client = RealTimeClient.connect(this.project_id, cb)
  263. },
  264. cb => {
  265. return this.client.emit('joinDoc', this.doc_id, cb)
  266. },
  267. cb => {
  268. return this.client.emit(
  269. 'applyOtUpdate',
  270. this.doc_id,
  271. this.update,
  272. error => {
  273. this.error = error
  274. return cb()
  275. }
  276. )
  277. },
  278. ],
  279. done
  280. )
  281. })
  282. it('should return an error', function () {
  283. return expect(this.error).to.exist
  284. })
  285. it('should disconnect the client', function (done) {
  286. return setTimeout(() => {
  287. this.client.socket.connected.should.equal(false)
  288. return done()
  289. }, 300)
  290. })
  291. return it('should not put the update in redis', function (done) {
  292. rclient.llen(
  293. redisSettings.documentupdater.key_schema.pendingUpdates({
  294. doc_id: this.doc_id,
  295. }),
  296. (error, len) => {
  297. if (error) return done(error)
  298. len.should.equal(0)
  299. return done()
  300. }
  301. )
  302. return null
  303. })
  304. })
  305. describe('when authorized to read-only with a comment update', function () {
  306. before(function (done) {
  307. this.comment_update = {
  308. op: [{ c: 'foo', p: 42 }],
  309. }
  310. return async.series(
  311. [
  312. cb => {
  313. return FixturesManager.setUpProject(
  314. {
  315. privilegeLevel: 'readOnly',
  316. },
  317. (e, { project_id: projectId, user_id: userId }) => {
  318. this.project_id = projectId
  319. this.user_id = userId
  320. return cb(e)
  321. }
  322. )
  323. },
  324. cb => {
  325. return FixturesManager.setUpDoc(
  326. this.project_id,
  327. { lines: this.lines, version: this.version, ops: this.ops },
  328. (e, { doc_id: docId }) => {
  329. this.doc_id = docId
  330. return cb(e)
  331. }
  332. )
  333. },
  334. cb => {
  335. this.client = RealTimeClient.connect(this.project_id, cb)
  336. },
  337. cb => {
  338. return this.client.emit('joinDoc', this.doc_id, cb)
  339. },
  340. cb => {
  341. return this.client.emit(
  342. 'applyOtUpdate',
  343. this.doc_id,
  344. this.comment_update,
  345. cb
  346. )
  347. },
  348. ],
  349. done
  350. )
  351. })
  352. it('should push the doc into the pending updates list', function (done) {
  353. getPendingUpdatesList((error, ...rest) => {
  354. if (error) return done(error)
  355. const [docId] = Array.from(rest[0])
  356. docId.should.equal(`${this.project_id}:${this.doc_id}`)
  357. return done()
  358. })
  359. return null
  360. })
  361. it('should push the update into redis', function (done) {
  362. rclient.lrange(
  363. redisSettings.documentupdater.key_schema.pendingUpdates({
  364. doc_id: this.doc_id,
  365. }),
  366. 0,
  367. -1,
  368. (error, ...rest) => {
  369. if (error) return done(error)
  370. let [update] = Array.from(rest[0])
  371. update = JSON.parse(update)
  372. update.op.should.deep.equal(this.comment_update.op)
  373. update.meta.should.include({
  374. source: this.client.publicId,
  375. user_id: this.user_id,
  376. })
  377. return done()
  378. }
  379. )
  380. return null
  381. })
  382. return after(function (done) {
  383. return async.series(
  384. [
  385. cb => clearPendingUpdatesList(cb),
  386. cb =>
  387. rclient.del(
  388. 'DocsWithPendingUpdates',
  389. `${this.project_id}:${this.doc_id}`,
  390. cb
  391. ),
  392. cb =>
  393. rclient.del(
  394. redisSettings.documentupdater.key_schema.pendingUpdates({
  395. doc_id: this.doc_id,
  396. }),
  397. cb
  398. ),
  399. ],
  400. done
  401. )
  402. })
  403. })
  404. describe('when authorized with an edit update to a missing doc field', function () {
  405. before(function (done) {
  406. return async.series(
  407. [
  408. cb => {
  409. return FixturesManager.setUpProject(
  410. {
  411. privilegeLevel: 'readAndWrite',
  412. },
  413. (e, { project_id: projectId, user_id: userId }) => {
  414. this.project_id = projectId
  415. this.user_id = userId
  416. return cb(e)
  417. }
  418. )
  419. },
  420. cb => {
  421. return FixturesManager.setUpDoc(
  422. this.project_id,
  423. { lines: this.lines, version: this.version, ops: this.ops },
  424. (e, { doc_id: docId }) => {
  425. this.doc_id = docId
  426. return cb(e)
  427. }
  428. )
  429. },
  430. cb => {
  431. this.client = RealTimeClient.connect(this.project_id, cb)
  432. },
  433. cb => {
  434. return this.client.emit('joinDoc', this.doc_id, cb)
  435. },
  436. cb => {
  437. return this.client.emit(
  438. 'applyOtUpdate',
  439. this.doc_id,
  440. this.update,
  441. error => {
  442. this.error = error
  443. return cb()
  444. }
  445. )
  446. },
  447. ],
  448. done
  449. )
  450. })
  451. after(function (done) {
  452. async.series(
  453. [
  454. cb => clearPendingUpdatesList(cb),
  455. cb =>
  456. rclient.del(
  457. redisSettings.documentupdater.key_schema.pendingUpdates({
  458. doc_id: this.doc_id,
  459. }),
  460. cb
  461. ),
  462. ],
  463. done
  464. )
  465. })
  466. it('should not return an error', function () {
  467. expect(this.error).to.not.exist
  468. })
  469. it('should put the update in redis', function (done) {
  470. rclient.lrange(
  471. redisSettings.documentupdater.key_schema.pendingUpdates({
  472. doc_id: this.doc_id,
  473. }),
  474. 0,
  475. -1,
  476. (error, entries) => {
  477. if (error) return done(error)
  478. entries.length.should.equal(1)
  479. const update = JSON.parse(entries[0])
  480. update.should.include({ doc: this.doc_id })
  481. return done()
  482. }
  483. )
  484. return null
  485. })
  486. })
  487. describe('when authorized with an edit update to an invalid doc', function () {
  488. before(function (done) {
  489. return async.series(
  490. [
  491. cb => {
  492. return FixturesManager.setUpProject(
  493. {
  494. privilegeLevel: 'readOnly',
  495. },
  496. (e, { project_id: projectId, user_id: userId }) => {
  497. this.project_id = projectId
  498. this.user_id = userId
  499. return cb(e)
  500. }
  501. )
  502. },
  503. cb => {
  504. return FixturesManager.setUpDoc(
  505. this.project_id,
  506. { lines: this.lines, version: this.version, ops: this.ops },
  507. (e, { doc_id: docId }) => {
  508. this.doc_id = docId
  509. return cb(e)
  510. }
  511. )
  512. },
  513. cb => {
  514. this.client = RealTimeClient.connect(this.project_id, cb)
  515. },
  516. cb => {
  517. return this.client.emit('joinDoc', this.doc_id, cb)
  518. },
  519. cb => {
  520. return this.client.emit(
  521. 'applyOtUpdate',
  522. 'invalid-doc-id',
  523. this.update,
  524. error => {
  525. this.error = error
  526. return cb()
  527. }
  528. )
  529. },
  530. ],
  531. done
  532. )
  533. })
  534. it('should return an error', function () {
  535. return expect(this.error).to.exist
  536. })
  537. it('should disconnect the client', function (done) {
  538. return setTimeout(() => {
  539. this.client.socket.connected.should.equal(false)
  540. return done()
  541. }, 300)
  542. })
  543. return it('should not put the update in redis', function (done) {
  544. rclient.llen(
  545. redisSettings.documentupdater.key_schema.pendingUpdates({
  546. doc_id: this.doc_id,
  547. }),
  548. (error, len) => {
  549. if (error) return done(error)
  550. len.should.equal(0)
  551. return done()
  552. }
  553. )
  554. return null
  555. })
  556. })
  557. describe('when authorized with an edit update to a different doc', function () {
  558. before(function (done) {
  559. return async.series(
  560. [
  561. cb => {
  562. return FixturesManager.setUpProject(
  563. {
  564. privilegeLevel: 'readAndWrite',
  565. },
  566. (e, { project_id: projectId, user_id: userId }) => {
  567. this.project_id = projectId
  568. this.user_id = userId
  569. return cb(e)
  570. }
  571. )
  572. },
  573. cb => {
  574. return FixturesManager.setUpDoc(
  575. this.project_id,
  576. { lines: this.lines, version: this.version, ops: this.ops },
  577. (e, { doc_id: docId }) => {
  578. this.doc_id = docId
  579. return cb(e)
  580. }
  581. )
  582. },
  583. cb => {
  584. this.client = RealTimeClient.connect(this.project_id, cb)
  585. },
  586. cb => {
  587. return this.client.emit('joinDoc', this.doc_id, cb)
  588. },
  589. cb => {
  590. return this.client.emit(
  591. 'applyOtUpdate',
  592. this.doc_id,
  593. { doc: 'other-doc' },
  594. error => {
  595. this.error = error
  596. return cb()
  597. }
  598. )
  599. },
  600. ],
  601. done
  602. )
  603. })
  604. it('should return an error', function () {
  605. expect(this.error).to.deep.equal({
  606. message:
  607. 'update.doc must be identical to docId parameter in applyOtUpdate(docId, update)',
  608. })
  609. })
  610. it('should not put the update in redis', function (done) {
  611. rclient.llen(
  612. redisSettings.documentupdater.key_schema.pendingUpdates({
  613. doc_id: this.doc_id,
  614. }),
  615. (error, len) => {
  616. if (error) return done(error)
  617. len.should.equal(0)
  618. return done()
  619. }
  620. )
  621. return null
  622. })
  623. })
  624. describe('when authorized with an invalid edit update', function () {
  625. before(function (done) {
  626. return async.series(
  627. [
  628. cb => {
  629. return FixturesManager.setUpProject(
  630. {
  631. privilegeLevel: 'readAndWrite',
  632. },
  633. (e, { project_id: projectId, user_id: userId }) => {
  634. this.project_id = projectId
  635. this.user_id = userId
  636. return cb(e)
  637. }
  638. )
  639. },
  640. cb => {
  641. return FixturesManager.setUpDoc(
  642. this.project_id,
  643. { lines: this.lines, version: this.version, ops: this.ops },
  644. (e, { doc_id: docId }) => {
  645. this.doc_id = docId
  646. return cb(e)
  647. }
  648. )
  649. },
  650. cb => {
  651. this.client = RealTimeClient.connect(this.project_id, cb)
  652. },
  653. cb => {
  654. return this.client.emit('joinDoc', this.doc_id, cb)
  655. },
  656. cb => {
  657. return this.client.emit(
  658. 'applyOtUpdate',
  659. this.doc_id,
  660. 'invalid-update',
  661. error => {
  662. this.error = error
  663. return cb()
  664. }
  665. )
  666. },
  667. ],
  668. done
  669. )
  670. })
  671. it('should return an error', function () {
  672. return expect(this.error).to.exist
  673. })
  674. it('should disconnect the client', function (done) {
  675. return setTimeout(() => {
  676. this.client.socket.connected.should.equal(false)
  677. return done()
  678. }, 300)
  679. })
  680. return it('should not put the update in redis', function (done) {
  681. rclient.llen(
  682. redisSettings.documentupdater.key_schema.pendingUpdates({
  683. doc_id: this.doc_id,
  684. }),
  685. (error, len) => {
  686. if (error) return done(error)
  687. len.should.equal(0)
  688. return done()
  689. }
  690. )
  691. return null
  692. })
  693. })
  694. })