UpdateManagerTests.js 21 KB

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