UpdateManagerTests.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. /* eslint-disable
  2. no-return-assign,
  3. no-unused-vars,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS101: Remove unnecessary use of Array.from
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS206: Consider reworking classes to avoid initClass
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. const sinon = require('sinon')
  15. const modulePath = '../../../../app/js/UpdateManager.js'
  16. const SandboxedModule = require('sandboxed-module')
  17. describe('UpdateManager', function () {
  18. beforeEach(function () {
  19. let Profiler, Timer
  20. this.project_id = 'project-id-123'
  21. this.projectHistoryId = 'history-id-123'
  22. this.doc_id = 'document-id-123'
  23. this.callback = sinon.stub()
  24. return (this.UpdateManager = SandboxedModule.require(modulePath, {
  25. requires: {
  26. './LockManager': (this.LockManager = {}),
  27. './RedisManager': (this.RedisManager = {}),
  28. './RealTimeRedisManager': (this.RealTimeRedisManager = {}),
  29. './ShareJsUpdateManager': (this.ShareJsUpdateManager = {}),
  30. './HistoryManager': (this.HistoryManager = {}),
  31. './Metrics': (this.Metrics = {
  32. inc: sinon.stub(),
  33. Timer: (Timer = (function () {
  34. Timer = class Timer {
  35. static initClass() {
  36. this.prototype.done = sinon.stub()
  37. }
  38. }
  39. Timer.initClass()
  40. return Timer
  41. })()),
  42. }),
  43. '@overleaf/settings': (this.Settings = {}),
  44. './DocumentManager': (this.DocumentManager = {}),
  45. './RangesManager': (this.RangesManager = {}),
  46. './SnapshotManager': (this.SnapshotManager = {}),
  47. './Profiler': (Profiler = (function () {
  48. Profiler = class Profiler {
  49. static initClass() {
  50. this.prototype.log = sinon.stub().returns({ end: sinon.stub() })
  51. this.prototype.end = sinon.stub()
  52. }
  53. }
  54. Profiler.initClass()
  55. return Profiler
  56. })()),
  57. },
  58. }))
  59. })
  60. describe('processOutstandingUpdates', function () {
  61. beforeEach(function () {
  62. this.UpdateManager.fetchAndApplyUpdates = sinon.stub().callsArg(2)
  63. return this.UpdateManager.processOutstandingUpdates(
  64. this.project_id,
  65. this.doc_id,
  66. this.callback
  67. )
  68. })
  69. it('should apply the updates', function () {
  70. return this.UpdateManager.fetchAndApplyUpdates
  71. .calledWith(this.project_id, this.doc_id)
  72. .should.equal(true)
  73. })
  74. it('should call the callback', function () {
  75. return this.callback.called.should.equal(true)
  76. })
  77. return it('should time the execution', function () {
  78. return this.Metrics.Timer.prototype.done.called.should.equal(true)
  79. })
  80. })
  81. describe('processOutstandingUpdatesWithLock', function () {
  82. describe('when the lock is free', function () {
  83. beforeEach(function () {
  84. this.LockManager.tryLock = sinon
  85. .stub()
  86. .callsArgWith(1, null, true, (this.lockValue = 'mock-lock-value'))
  87. this.LockManager.releaseLock = sinon.stub().callsArg(2)
  88. this.UpdateManager.continueProcessingUpdatesWithLock = sinon
  89. .stub()
  90. .callsArg(2)
  91. return (this.UpdateManager.processOutstandingUpdates = sinon
  92. .stub()
  93. .callsArg(2))
  94. })
  95. describe('successfully', function () {
  96. beforeEach(function () {
  97. return this.UpdateManager.processOutstandingUpdatesWithLock(
  98. this.project_id,
  99. this.doc_id,
  100. this.callback
  101. )
  102. })
  103. it('should acquire the lock', function () {
  104. return this.LockManager.tryLock
  105. .calledWith(this.doc_id)
  106. .should.equal(true)
  107. })
  108. it('should free the lock', function () {
  109. return this.LockManager.releaseLock
  110. .calledWith(this.doc_id, this.lockValue)
  111. .should.equal(true)
  112. })
  113. it('should process the outstanding updates', function () {
  114. return this.UpdateManager.processOutstandingUpdates
  115. .calledWith(this.project_id, this.doc_id)
  116. .should.equal(true)
  117. })
  118. it('should do everything with the lock acquired', function () {
  119. this.UpdateManager.processOutstandingUpdates
  120. .calledAfter(this.LockManager.tryLock)
  121. .should.equal(true)
  122. return this.UpdateManager.processOutstandingUpdates
  123. .calledBefore(this.LockManager.releaseLock)
  124. .should.equal(true)
  125. })
  126. it('should continue processing new updates that may have come in', function () {
  127. return this.UpdateManager.continueProcessingUpdatesWithLock
  128. .calledWith(this.project_id, this.doc_id)
  129. .should.equal(true)
  130. })
  131. return it('should return the callback', function () {
  132. return this.callback.called.should.equal(true)
  133. })
  134. })
  135. return describe('when processOutstandingUpdates returns an error', function () {
  136. beforeEach(function () {
  137. this.UpdateManager.processOutstandingUpdates = sinon
  138. .stub()
  139. .callsArgWith(2, (this.error = new Error('Something went wrong')))
  140. return this.UpdateManager.processOutstandingUpdatesWithLock(
  141. this.project_id,
  142. this.doc_id,
  143. this.callback
  144. )
  145. })
  146. it('should free the lock', function () {
  147. return this.LockManager.releaseLock
  148. .calledWith(this.doc_id, this.lockValue)
  149. .should.equal(true)
  150. })
  151. return it('should return the error in the callback', function () {
  152. return this.callback.calledWith(this.error).should.equal(true)
  153. })
  154. })
  155. })
  156. return describe('when the lock is taken', function () {
  157. beforeEach(function () {
  158. this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, false)
  159. this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2)
  160. return this.UpdateManager.processOutstandingUpdatesWithLock(
  161. this.project_id,
  162. this.doc_id,
  163. this.callback
  164. )
  165. })
  166. it('should return the callback', function () {
  167. return this.callback.called.should.equal(true)
  168. })
  169. return it('should not process the updates', function () {
  170. return this.UpdateManager.processOutstandingUpdates.called.should.equal(
  171. false
  172. )
  173. })
  174. })
  175. })
  176. describe('continueProcessingUpdatesWithLock', function () {
  177. describe('when there are outstanding updates', function () {
  178. beforeEach(function () {
  179. this.RealTimeRedisManager.getUpdatesLength = sinon
  180. .stub()
  181. .callsArgWith(1, null, 3)
  182. this.UpdateManager.processOutstandingUpdatesWithLock = sinon
  183. .stub()
  184. .callsArg(2)
  185. return this.UpdateManager.continueProcessingUpdatesWithLock(
  186. this.project_id,
  187. this.doc_id,
  188. this.callback
  189. )
  190. })
  191. it('should process the outstanding updates', function () {
  192. return this.UpdateManager.processOutstandingUpdatesWithLock
  193. .calledWith(this.project_id, this.doc_id)
  194. .should.equal(true)
  195. })
  196. return it('should return the callback', function () {
  197. return this.callback.called.should.equal(true)
  198. })
  199. })
  200. return describe('when there are no outstanding updates', function () {
  201. beforeEach(function () {
  202. this.RealTimeRedisManager.getUpdatesLength = sinon
  203. .stub()
  204. .callsArgWith(1, null, 0)
  205. this.UpdateManager.processOutstandingUpdatesWithLock = sinon
  206. .stub()
  207. .callsArg(2)
  208. return this.UpdateManager.continueProcessingUpdatesWithLock(
  209. this.project_id,
  210. this.doc_id,
  211. this.callback
  212. )
  213. })
  214. it('should not try to process the outstanding updates', function () {
  215. return this.UpdateManager.processOutstandingUpdatesWithLock.called.should.equal(
  216. false
  217. )
  218. })
  219. return it('should return the callback', function () {
  220. return this.callback.called.should.equal(true)
  221. })
  222. })
  223. })
  224. describe('fetchAndApplyUpdates', function () {
  225. describe('with updates', function () {
  226. beforeEach(function () {
  227. this.updates = [{ p: 1, t: 'foo' }]
  228. this.updatedDocLines = ['updated', 'lines']
  229. this.version = 34
  230. this.RealTimeRedisManager.getPendingUpdatesForDoc = sinon
  231. .stub()
  232. .callsArgWith(1, null, this.updates)
  233. this.UpdateManager.applyUpdate = sinon
  234. .stub()
  235. .callsArgWith(3, null, this.updatedDocLines, this.version)
  236. return this.UpdateManager.fetchAndApplyUpdates(
  237. this.project_id,
  238. this.doc_id,
  239. this.callback
  240. )
  241. })
  242. it('should get the pending updates', function () {
  243. return this.RealTimeRedisManager.getPendingUpdatesForDoc
  244. .calledWith(this.doc_id)
  245. .should.equal(true)
  246. })
  247. it('should apply the updates', function () {
  248. return Array.from(this.updates).map(update =>
  249. this.UpdateManager.applyUpdate
  250. .calledWith(this.project_id, this.doc_id, update)
  251. .should.equal(true)
  252. )
  253. })
  254. return it('should call the callback', function () {
  255. return this.callback.called.should.equal(true)
  256. })
  257. })
  258. return describe('when there are no updates', function () {
  259. beforeEach(function () {
  260. this.updates = []
  261. this.RealTimeRedisManager.getPendingUpdatesForDoc = sinon
  262. .stub()
  263. .callsArgWith(1, null, this.updates)
  264. this.UpdateManager.applyUpdate = sinon.stub()
  265. this.RedisManager.setDocument = sinon.stub()
  266. return this.UpdateManager.fetchAndApplyUpdates(
  267. this.project_id,
  268. this.doc_id,
  269. this.callback
  270. )
  271. })
  272. it('should not call applyUpdate', function () {
  273. return this.UpdateManager.applyUpdate.called.should.equal(false)
  274. })
  275. return it('should call the callback', function () {
  276. return this.callback.called.should.equal(true)
  277. })
  278. })
  279. })
  280. describe('applyUpdate', function () {
  281. beforeEach(function () {
  282. this.updateMeta = { user_id: 'last-author-fake-id' }
  283. this.update = { op: [{ p: 42, i: 'foo' }], meta: this.updateMeta }
  284. this.updatedDocLines = ['updated', 'lines']
  285. this.version = 34
  286. this.lines = ['original', 'lines']
  287. this.ranges = { entries: 'mock', comments: 'mock' }
  288. this.updated_ranges = { entries: 'updated', comments: 'updated' }
  289. this.appliedOps = [
  290. { v: 42, op: 'mock-op-42' },
  291. { v: 45, op: 'mock-op-45' },
  292. ]
  293. this.doc_ops_length = sinon.stub()
  294. this.project_ops_length = sinon.stub()
  295. this.pathname = '/a/b/c.tex'
  296. this.DocumentManager.getDoc = sinon
  297. .stub()
  298. .yields(
  299. null,
  300. this.lines,
  301. this.version,
  302. this.ranges,
  303. this.pathname,
  304. this.projectHistoryId
  305. )
  306. this.RangesManager.applyUpdate = sinon
  307. .stub()
  308. .yields(null, this.updated_ranges, false)
  309. this.ShareJsUpdateManager.applyUpdate = sinon
  310. .stub()
  311. .yields(null, this.updatedDocLines, this.version, this.appliedOps)
  312. this.RedisManager.updateDocument = sinon
  313. .stub()
  314. .yields(null, this.doc_ops_length, this.project_ops_length)
  315. this.RealTimeRedisManager.sendData = sinon.stub()
  316. this.UpdateManager._addProjectHistoryMetadataToOps = sinon.stub()
  317. return (this.HistoryManager.recordAndFlushHistoryOps = sinon
  318. .stub()
  319. .callsArg(5))
  320. })
  321. describe('normally', function () {
  322. beforeEach(function () {
  323. return this.UpdateManager.applyUpdate(
  324. this.project_id,
  325. this.doc_id,
  326. this.update,
  327. this.callback
  328. )
  329. })
  330. it('should apply the updates via ShareJS', function () {
  331. return this.ShareJsUpdateManager.applyUpdate
  332. .calledWith(
  333. this.project_id,
  334. this.doc_id,
  335. this.update,
  336. this.lines,
  337. this.version
  338. )
  339. .should.equal(true)
  340. })
  341. it('should update the ranges', function () {
  342. return this.RangesManager.applyUpdate
  343. .calledWith(
  344. this.project_id,
  345. this.doc_id,
  346. this.ranges,
  347. this.appliedOps,
  348. this.updatedDocLines
  349. )
  350. .should.equal(true)
  351. })
  352. it('should save the document', function () {
  353. return this.RedisManager.updateDocument
  354. .calledWith(
  355. this.project_id,
  356. this.doc_id,
  357. this.updatedDocLines,
  358. this.version,
  359. this.appliedOps,
  360. this.updated_ranges,
  361. this.updateMeta
  362. )
  363. .should.equal(true)
  364. })
  365. it('should add metadata to the ops', function () {
  366. return this.UpdateManager._addProjectHistoryMetadataToOps
  367. .calledWith(
  368. this.appliedOps,
  369. this.pathname,
  370. this.projectHistoryId,
  371. this.lines
  372. )
  373. .should.equal(true)
  374. })
  375. it('should push the applied ops into the history queue', function () {
  376. return this.HistoryManager.recordAndFlushHistoryOps
  377. .calledWith(
  378. this.project_id,
  379. this.doc_id,
  380. this.appliedOps,
  381. this.doc_ops_length,
  382. this.project_ops_length
  383. )
  384. .should.equal(true)
  385. })
  386. return it('should call the callback', function () {
  387. return this.callback.called.should.equal(true)
  388. })
  389. })
  390. describe('with UTF-16 surrogate pairs in the update', function () {
  391. beforeEach(function () {
  392. this.update = { op: [{ p: 42, i: '\uD835\uDC00' }] }
  393. return this.UpdateManager.applyUpdate(
  394. this.project_id,
  395. this.doc_id,
  396. this.update,
  397. this.callback
  398. )
  399. })
  400. return it('should apply the update but with surrogate pairs removed', function () {
  401. this.ShareJsUpdateManager.applyUpdate
  402. .calledWith(this.project_id, this.doc_id, this.update)
  403. .should.equal(true)
  404. // \uFFFD is 'replacement character'
  405. return this.update.op[0].i.should.equal('\uFFFD\uFFFD')
  406. })
  407. })
  408. describe('with an error', function () {
  409. beforeEach(function () {
  410. this.error = new Error('something went wrong')
  411. this.ShareJsUpdateManager.applyUpdate = sinon.stub().yields(this.error)
  412. return this.UpdateManager.applyUpdate(
  413. this.project_id,
  414. this.doc_id,
  415. this.update,
  416. this.callback
  417. )
  418. })
  419. it('should call RealTimeRedisManager.sendData with the error', function () {
  420. return this.RealTimeRedisManager.sendData
  421. .calledWith({
  422. project_id: this.project_id,
  423. doc_id: this.doc_id,
  424. error: this.error.message,
  425. })
  426. .should.equal(true)
  427. })
  428. return it('should call the callback with the error', function () {
  429. return this.callback.calledWith(this.error).should.equal(true)
  430. })
  431. })
  432. return describe('when ranges get collapsed', function () {
  433. beforeEach(function () {
  434. this.RangesManager.applyUpdate = sinon
  435. .stub()
  436. .yields(null, this.updated_ranges, true)
  437. this.SnapshotManager.recordSnapshot = sinon.stub().yields()
  438. return this.UpdateManager.applyUpdate(
  439. this.project_id,
  440. this.doc_id,
  441. this.update,
  442. this.callback
  443. )
  444. })
  445. it('should increment the doc-snapshot metric', function () {
  446. this.Metrics.inc.calledWith('doc-snapshot').should.equal(true)
  447. })
  448. return it('should call SnapshotManager.recordSnapshot', function () {
  449. return this.SnapshotManager.recordSnapshot
  450. .calledWith(
  451. this.project_id,
  452. this.doc_id,
  453. this.version,
  454. this.pathname,
  455. this.lines,
  456. this.ranges
  457. )
  458. .should.equal(true)
  459. })
  460. })
  461. })
  462. describe('_addProjectHistoryMetadataToOps', function () {
  463. return it('should add projectHistoryId, pathname and doc_length metadata to the ops', function () {
  464. const lines = ['some', 'test', 'data']
  465. const appliedOps = [
  466. {
  467. v: 42,
  468. op: [
  469. { i: 'foo', p: 4 },
  470. { i: 'bar', p: 6 },
  471. ],
  472. },
  473. {
  474. v: 45,
  475. op: [
  476. { d: 'qux', p: 4 },
  477. { i: 'bazbaz', p: 14 },
  478. ],
  479. },
  480. { v: 49, op: [{ i: 'penguin', p: 18 }] },
  481. ]
  482. this.UpdateManager._addProjectHistoryMetadataToOps(
  483. appliedOps,
  484. this.pathname,
  485. this.projectHistoryId,
  486. lines
  487. )
  488. return appliedOps.should.deep.equal([
  489. {
  490. projectHistoryId: this.projectHistoryId,
  491. v: 42,
  492. op: [
  493. { i: 'foo', p: 4 },
  494. { i: 'bar', p: 6 },
  495. ],
  496. meta: {
  497. pathname: this.pathname,
  498. doc_length: 14,
  499. },
  500. },
  501. {
  502. projectHistoryId: this.projectHistoryId,
  503. v: 45,
  504. op: [
  505. { d: 'qux', p: 4 },
  506. { i: 'bazbaz', p: 14 },
  507. ],
  508. meta: {
  509. pathname: this.pathname,
  510. doc_length: 20,
  511. }, // 14 + 'foo' + 'bar'
  512. },
  513. {
  514. projectHistoryId: this.projectHistoryId,
  515. v: 49,
  516. op: [{ i: 'penguin', p: 18 }],
  517. meta: {
  518. pathname: this.pathname,
  519. doc_length: 23,
  520. }, // 14 - 'qux' + 'bazbaz'
  521. },
  522. ])
  523. })
  524. })
  525. return describe('lockUpdatesAndDo', function () {
  526. beforeEach(function () {
  527. this.method = sinon.stub().callsArgWith(3, null, this.response_arg1)
  528. this.callback = sinon.stub()
  529. this.arg1 = 'argument 1'
  530. this.response_arg1 = 'response argument 1'
  531. this.lockValue = 'mock-lock-value'
  532. this.LockManager.getLock = sinon
  533. .stub()
  534. .callsArgWith(1, null, this.lockValue)
  535. return (this.LockManager.releaseLock = sinon.stub().callsArg(2))
  536. })
  537. describe('successfully', function () {
  538. beforeEach(function () {
  539. this.UpdateManager.continueProcessingUpdatesWithLock = sinon.stub()
  540. this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2)
  541. return this.UpdateManager.lockUpdatesAndDo(
  542. this.method,
  543. this.project_id,
  544. this.doc_id,
  545. this.arg1,
  546. this.callback
  547. )
  548. })
  549. it('should lock the doc', function () {
  550. return this.LockManager.getLock
  551. .calledWith(this.doc_id)
  552. .should.equal(true)
  553. })
  554. it('should process any outstanding updates', function () {
  555. return this.UpdateManager.processOutstandingUpdates
  556. .calledWith(this.project_id, this.doc_id)
  557. .should.equal(true)
  558. })
  559. it('should call the method', function () {
  560. return this.method
  561. .calledWith(this.project_id, this.doc_id, this.arg1)
  562. .should.equal(true)
  563. })
  564. it('should return the method response to the callback', function () {
  565. return this.callback
  566. .calledWith(null, this.response_arg1)
  567. .should.equal(true)
  568. })
  569. it('should release the lock', function () {
  570. return this.LockManager.releaseLock
  571. .calledWith(this.doc_id, this.lockValue)
  572. .should.equal(true)
  573. })
  574. return it('should continue processing updates', function () {
  575. return this.UpdateManager.continueProcessingUpdatesWithLock
  576. .calledWith(this.project_id, this.doc_id)
  577. .should.equal(true)
  578. })
  579. })
  580. describe('when processOutstandingUpdates returns an error', function () {
  581. beforeEach(function () {
  582. this.UpdateManager.processOutstandingUpdates = sinon
  583. .stub()
  584. .callsArgWith(2, (this.error = new Error('Something went wrong')))
  585. return this.UpdateManager.lockUpdatesAndDo(
  586. this.method,
  587. this.project_id,
  588. this.doc_id,
  589. this.arg1,
  590. this.callback
  591. )
  592. })
  593. it('should free the lock', function () {
  594. return this.LockManager.releaseLock
  595. .calledWith(this.doc_id, this.lockValue)
  596. .should.equal(true)
  597. })
  598. return it('should return the error in the callback', function () {
  599. return this.callback.calledWith(this.error).should.equal(true)
  600. })
  601. })
  602. return describe('when the method returns an error', function () {
  603. beforeEach(function () {
  604. this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2)
  605. this.method = sinon
  606. .stub()
  607. .callsArgWith(
  608. 3,
  609. (this.error = new Error('something went wrong')),
  610. this.response_arg1
  611. )
  612. return this.UpdateManager.lockUpdatesAndDo(
  613. this.method,
  614. this.project_id,
  615. this.doc_id,
  616. this.arg1,
  617. this.callback
  618. )
  619. })
  620. it('should free the lock', function () {
  621. return this.LockManager.releaseLock
  622. .calledWith(this.doc_id, this.lockValue)
  623. .should.equal(true)
  624. })
  625. return it('should return the error in the callback', function () {
  626. return this.callback.calledWith(this.error).should.equal(true)
  627. })
  628. })
  629. })
  630. })