UpdateManagerTests.js 20 KB

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