ApplyUpdateTests.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  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 an invalid doc', function () {
  405. before(function (done) {
  406. return async.series(
  407. [
  408. cb => {
  409. return FixturesManager.setUpProject(
  410. {
  411. privilegeLevel: 'readOnly',
  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. 'invalid-doc-id',
  440. this.update,
  441. error => {
  442. this.error = error
  443. return cb()
  444. }
  445. )
  446. },
  447. ],
  448. done
  449. )
  450. })
  451. it('should return an error', function () {
  452. return expect(this.error).to.exist
  453. })
  454. it('should disconnect the client', function (done) {
  455. return setTimeout(() => {
  456. this.client.socket.connected.should.equal(false)
  457. return done()
  458. }, 300)
  459. })
  460. return it('should not put the update in redis', function (done) {
  461. rclient.llen(
  462. redisSettings.documentupdater.key_schema.pendingUpdates({
  463. doc_id: this.doc_id,
  464. }),
  465. (error, len) => {
  466. if (error) return done(error)
  467. len.should.equal(0)
  468. return done()
  469. }
  470. )
  471. return null
  472. })
  473. })
  474. describe('when authorized with an invalid edit update', function () {
  475. before(function (done) {
  476. return async.series(
  477. [
  478. cb => {
  479. return FixturesManager.setUpProject(
  480. {
  481. privilegeLevel: 'readAndWrite',
  482. },
  483. (e, { project_id: projectId, user_id: userId }) => {
  484. this.project_id = projectId
  485. this.user_id = userId
  486. return cb(e)
  487. }
  488. )
  489. },
  490. cb => {
  491. return FixturesManager.setUpDoc(
  492. this.project_id,
  493. { lines: this.lines, version: this.version, ops: this.ops },
  494. (e, { doc_id: docId }) => {
  495. this.doc_id = docId
  496. return cb(e)
  497. }
  498. )
  499. },
  500. cb => {
  501. this.client = RealTimeClient.connect(this.project_id, cb)
  502. },
  503. cb => {
  504. return this.client.emit('joinDoc', this.doc_id, cb)
  505. },
  506. cb => {
  507. return this.client.emit(
  508. 'applyOtUpdate',
  509. this.doc_id,
  510. 'invalid-update',
  511. error => {
  512. this.error = error
  513. return cb()
  514. }
  515. )
  516. },
  517. ],
  518. done
  519. )
  520. })
  521. it('should return an error', function () {
  522. return expect(this.error).to.exist
  523. })
  524. it('should disconnect the client', function (done) {
  525. return setTimeout(() => {
  526. this.client.socket.connected.should.equal(false)
  527. return done()
  528. }, 300)
  529. })
  530. return it('should not put the update in redis', function (done) {
  531. rclient.llen(
  532. redisSettings.documentupdater.key_schema.pendingUpdates({
  533. doc_id: this.doc_id,
  534. }),
  535. (error, len) => {
  536. if (error) return done(error)
  537. len.should.equal(0)
  538. return done()
  539. }
  540. )
  541. return null
  542. })
  543. })
  544. })