UpdateManagerTests.js 21 KB

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