UpdateManagerTests.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824
  1. // @ts-check
  2. const sinon = require('sinon')
  3. const { expect } = require('chai')
  4. const SandboxedModule = require('sandboxed-module')
  5. const MODULE_PATH = '../../../../app/js/UpdateManager.js'
  6. describe('UpdateManager', function () {
  7. beforeEach(function () {
  8. this.project_id = 'project-id-123'
  9. this.projectHistoryId = 'history-id-123'
  10. this.doc_id = 'document-id-123'
  11. this.lockValue = 'mock-lock-value'
  12. this.pathname = '/a/b/c.tex'
  13. this.Metrics = {
  14. inc: sinon.stub(),
  15. Timer: class Timer {},
  16. }
  17. this.Metrics.Timer.prototype.done = sinon.stub()
  18. this.Profiler = class Profiler {}
  19. this.Profiler.prototype.log = sinon.stub().returns({ end: sinon.stub() })
  20. this.Profiler.prototype.end = sinon.stub()
  21. this.LockManager = {
  22. promises: {
  23. tryLock: sinon.stub().resolves(this.lockValue),
  24. getLock: sinon.stub().resolves(this.lockValue),
  25. releaseLock: sinon.stub().resolves(),
  26. },
  27. }
  28. this.RedisManager = {
  29. promises: {
  30. setDocument: sinon.stub().resolves(),
  31. updateDocument: sinon.stub(),
  32. },
  33. }
  34. this.RealTimeRedisManager = {
  35. sendData: sinon.stub(),
  36. promises: {
  37. getUpdatesLength: sinon.stub(),
  38. getPendingUpdatesForDoc: sinon.stub(),
  39. },
  40. }
  41. this.ShareJsUpdateManager = {
  42. promises: {
  43. applyUpdate: sinon.stub(),
  44. },
  45. }
  46. this.HistoryManager = {
  47. recordAndFlushHistoryOps: sinon.stub(),
  48. }
  49. this.Settings = {}
  50. this.DocumentManager = {
  51. promises: {
  52. getDoc: sinon.stub(),
  53. },
  54. }
  55. this.RangesManager = {
  56. applyUpdate: sinon.stub(),
  57. }
  58. this.SnapshotManager = {
  59. promises: {
  60. recordSnapshot: sinon.stub().resolves(),
  61. },
  62. }
  63. this.ProjectHistoryRedisManager = {
  64. promises: {
  65. queueOps: sinon
  66. .stub()
  67. .callsFake(async (projectId, ...ops) => ops.length),
  68. },
  69. }
  70. this.UpdateManager = SandboxedModule.require(MODULE_PATH, {
  71. requires: {
  72. './LockManager': this.LockManager,
  73. './RedisManager': this.RedisManager,
  74. './RealTimeRedisManager': this.RealTimeRedisManager,
  75. './ShareJsUpdateManager': this.ShareJsUpdateManager,
  76. './HistoryManager': this.HistoryManager,
  77. './Metrics': this.Metrics,
  78. '@overleaf/settings': this.Settings,
  79. './DocumentManager': this.DocumentManager,
  80. './RangesManager': this.RangesManager,
  81. './SnapshotManager': this.SnapshotManager,
  82. './Profiler': this.Profiler,
  83. './ProjectHistoryRedisManager': this.ProjectHistoryRedisManager,
  84. },
  85. })
  86. })
  87. describe('processOutstandingUpdates', function () {
  88. beforeEach(async function () {
  89. this.UpdateManager.promises.fetchAndApplyUpdates = sinon.stub().resolves()
  90. await this.UpdateManager.promises.processOutstandingUpdates(
  91. this.project_id,
  92. this.doc_id
  93. )
  94. })
  95. it('should apply the updates', function () {
  96. this.UpdateManager.promises.fetchAndApplyUpdates
  97. .calledWith(this.project_id, this.doc_id)
  98. .should.equal(true)
  99. })
  100. it('should time the execution', function () {
  101. this.Metrics.Timer.prototype.done.called.should.equal(true)
  102. })
  103. })
  104. describe('processOutstandingUpdatesWithLock', function () {
  105. describe('when the lock is free', function () {
  106. beforeEach(function () {
  107. this.UpdateManager.promises.continueProcessingUpdatesWithLock = sinon
  108. .stub()
  109. .resolves()
  110. this.UpdateManager.promises.processOutstandingUpdates = sinon
  111. .stub()
  112. .resolves()
  113. })
  114. describe('successfully', function () {
  115. beforeEach(async function () {
  116. await this.UpdateManager.promises.processOutstandingUpdatesWithLock(
  117. this.project_id,
  118. this.doc_id
  119. )
  120. })
  121. it('should acquire the lock', function () {
  122. this.LockManager.promises.tryLock
  123. .calledWith(this.doc_id)
  124. .should.equal(true)
  125. })
  126. it('should free the lock', function () {
  127. this.LockManager.promises.releaseLock
  128. .calledWith(this.doc_id, this.lockValue)
  129. .should.equal(true)
  130. })
  131. it('should process the outstanding updates', function () {
  132. this.UpdateManager.promises.processOutstandingUpdates
  133. .calledWith(this.project_id, this.doc_id)
  134. .should.equal(true)
  135. })
  136. it('should do everything with the lock acquired', function () {
  137. this.UpdateManager.promises.processOutstandingUpdates
  138. .calledAfter(this.LockManager.promises.tryLock)
  139. .should.equal(true)
  140. this.UpdateManager.promises.processOutstandingUpdates
  141. .calledBefore(this.LockManager.promises.releaseLock)
  142. .should.equal(true)
  143. })
  144. it('should continue processing new updates that may have come in', function () {
  145. this.UpdateManager.promises.continueProcessingUpdatesWithLock
  146. .calledWith(this.project_id, this.doc_id)
  147. .should.equal(true)
  148. })
  149. })
  150. describe('when processOutstandingUpdates returns an error', function () {
  151. beforeEach(async function () {
  152. this.error = new Error('Something went wrong')
  153. this.UpdateManager.promises.processOutstandingUpdates = sinon
  154. .stub()
  155. .rejects(this.error)
  156. await expect(
  157. this.UpdateManager.promises.processOutstandingUpdatesWithLock(
  158. this.project_id,
  159. this.doc_id
  160. )
  161. ).to.be.rejectedWith(this.error)
  162. })
  163. it('should free the lock', function () {
  164. this.LockManager.promises.releaseLock
  165. .calledWith(this.doc_id, this.lockValue)
  166. .should.equal(true)
  167. })
  168. })
  169. })
  170. describe('when the lock is taken', function () {
  171. beforeEach(async function () {
  172. this.LockManager.promises.tryLock.resolves(null)
  173. this.UpdateManager.promises.processOutstandingUpdates = sinon
  174. .stub()
  175. .resolves()
  176. await this.UpdateManager.promises.processOutstandingUpdatesWithLock(
  177. this.project_id,
  178. this.doc_id
  179. )
  180. })
  181. it('should not process the updates', function () {
  182. this.UpdateManager.promises.processOutstandingUpdates.called.should.equal(
  183. false
  184. )
  185. })
  186. })
  187. })
  188. describe('continueProcessingUpdatesWithLock', function () {
  189. describe('when there are outstanding updates', function () {
  190. beforeEach(async function () {
  191. this.RealTimeRedisManager.promises.getUpdatesLength.resolves(3)
  192. this.UpdateManager.promises.processOutstandingUpdatesWithLock = sinon
  193. .stub()
  194. .resolves()
  195. await this.UpdateManager.promises.continueProcessingUpdatesWithLock(
  196. this.project_id,
  197. this.doc_id
  198. )
  199. })
  200. it('should process the outstanding updates', function () {
  201. this.UpdateManager.promises.processOutstandingUpdatesWithLock
  202. .calledWith(this.project_id, this.doc_id)
  203. .should.equal(true)
  204. })
  205. })
  206. describe('when there are no outstanding updates', function () {
  207. beforeEach(async function () {
  208. this.RealTimeRedisManager.promises.getUpdatesLength.resolves(0)
  209. this.UpdateManager.promises.processOutstandingUpdatesWithLock = sinon
  210. .stub()
  211. .resolves()
  212. await this.UpdateManager.promises.continueProcessingUpdatesWithLock(
  213. this.project_id,
  214. this.doc_id
  215. )
  216. })
  217. it('should not try to process the outstanding updates', function () {
  218. this.UpdateManager.promises.processOutstandingUpdatesWithLock.called.should.equal(
  219. false
  220. )
  221. })
  222. })
  223. })
  224. describe('fetchAndApplyUpdates', function () {
  225. describe('with updates', function () {
  226. beforeEach(async function () {
  227. this.updates = [{ p: 1, t: 'foo' }]
  228. this.updatedDocLines = ['updated', 'lines']
  229. this.version = 34
  230. this.RealTimeRedisManager.promises.getPendingUpdatesForDoc.resolves(
  231. this.updates
  232. )
  233. this.UpdateManager.promises.applyUpdate = sinon.stub().resolves()
  234. await this.UpdateManager.promises.fetchAndApplyUpdates(
  235. this.project_id,
  236. this.doc_id
  237. )
  238. })
  239. it('should get the pending updates', function () {
  240. this.RealTimeRedisManager.promises.getPendingUpdatesForDoc
  241. .calledWith(this.doc_id)
  242. .should.equal(true)
  243. })
  244. it('should apply the updates', function () {
  245. this.updates.map(update =>
  246. this.UpdateManager.promises.applyUpdate
  247. .calledWith(this.project_id, this.doc_id, update)
  248. .should.equal(true)
  249. )
  250. })
  251. })
  252. describe('when there are no updates', function () {
  253. beforeEach(async function () {
  254. this.updates = []
  255. this.RealTimeRedisManager.promises.getPendingUpdatesForDoc.resolves(
  256. this.updates
  257. )
  258. this.UpdateManager.promises.applyUpdate = sinon.stub().resolves()
  259. await this.UpdateManager.promises.fetchAndApplyUpdates(
  260. this.project_id,
  261. this.doc_id
  262. )
  263. })
  264. it('should not call applyUpdate', function () {
  265. this.UpdateManager.promises.applyUpdate.called.should.equal(false)
  266. })
  267. })
  268. })
  269. describe('applyUpdate', function () {
  270. beforeEach(function () {
  271. this.updateMeta = { user_id: 'last-author-fake-id' }
  272. this.update = { op: [{ p: 42, i: 'foo' }], meta: this.updateMeta }
  273. this.updatedDocLines = ['updated', 'lines']
  274. this.version = 34
  275. this.lines = ['original', 'lines']
  276. this.ranges = { entries: 'mock', comments: 'mock' }
  277. this.updated_ranges = { entries: 'updated', comments: 'updated' }
  278. this.appliedOps = [
  279. { v: 42, op: 'mock-op-42' },
  280. { v: 45, op: 'mock-op-45' },
  281. ]
  282. this.historyUpdates = [
  283. 'history-update-1',
  284. 'history-update-2',
  285. 'history-update-3',
  286. ]
  287. this.project_ops_length = 123
  288. this.DocumentManager.promises.getDoc.resolves({
  289. lines: this.lines,
  290. version: this.version,
  291. ranges: this.ranges,
  292. pathname: this.pathname,
  293. projectHistoryId: this.projectHistoryId,
  294. historyRangesSupport: false,
  295. })
  296. this.RangesManager.applyUpdate.returns({
  297. newRanges: this.updated_ranges,
  298. rangesWereCollapsed: false,
  299. historyUpdates: this.historyUpdates,
  300. })
  301. this.ShareJsUpdateManager.promises.applyUpdate = sinon.stub().resolves({
  302. updatedDocLines: this.updatedDocLines,
  303. version: this.version,
  304. appliedOps: this.appliedOps,
  305. })
  306. this.RedisManager.promises.updateDocument.resolves()
  307. this.UpdateManager.promises._adjustHistoryUpdatesMetadata = sinon.stub()
  308. })
  309. describe('normally', function () {
  310. beforeEach(async function () {
  311. await this.UpdateManager.promises.applyUpdate(
  312. this.project_id,
  313. this.doc_id,
  314. this.update
  315. )
  316. })
  317. it('should apply the updates via ShareJS', function () {
  318. this.ShareJsUpdateManager.promises.applyUpdate
  319. .calledWith(
  320. this.project_id,
  321. this.doc_id,
  322. this.update,
  323. this.lines,
  324. this.version
  325. )
  326. .should.equal(true)
  327. })
  328. it('should update the ranges', function () {
  329. this.RangesManager.applyUpdate
  330. .calledWith(
  331. this.project_id,
  332. this.doc_id,
  333. this.ranges,
  334. this.appliedOps,
  335. this.updatedDocLines
  336. )
  337. .should.equal(true)
  338. })
  339. it('should save the document', function () {
  340. this.RedisManager.promises.updateDocument
  341. .calledWith(
  342. this.project_id,
  343. this.doc_id,
  344. this.updatedDocLines,
  345. this.version,
  346. this.appliedOps,
  347. this.updated_ranges,
  348. this.updateMeta
  349. )
  350. .should.equal(true)
  351. })
  352. it('should add metadata to the ops', function () {
  353. this.UpdateManager.promises._adjustHistoryUpdatesMetadata.should.have.been.calledWith(
  354. this.historyUpdates,
  355. this.pathname,
  356. this.projectHistoryId,
  357. this.lines
  358. )
  359. })
  360. it('should push the applied ops into the history queue', function () {
  361. this.ProjectHistoryRedisManager.promises.queueOps.should.have.been.calledWith(
  362. this.project_id,
  363. ...this.historyUpdates.map(op => JSON.stringify(op))
  364. )
  365. this.HistoryManager.recordAndFlushHistoryOps.should.have.been.calledWith(
  366. this.project_id,
  367. this.historyUpdates,
  368. this.historyUpdates.length
  369. )
  370. })
  371. })
  372. describe('with UTF-16 surrogate pairs in the update', function () {
  373. beforeEach(async function () {
  374. this.update = { op: [{ p: 42, i: '\uD835\uDC00' }] }
  375. await this.UpdateManager.promises.applyUpdate(
  376. this.project_id,
  377. this.doc_id,
  378. this.update
  379. )
  380. })
  381. it('should apply the update but with surrogate pairs removed', function () {
  382. this.ShareJsUpdateManager.promises.applyUpdate
  383. .calledWith(this.project_id, this.doc_id, this.update)
  384. .should.equal(true)
  385. // \uFFFD is 'replacement character'
  386. this.update.op[0].i.should.equal('\uFFFD\uFFFD')
  387. })
  388. })
  389. describe('with an error', function () {
  390. beforeEach(async function () {
  391. this.error = new Error('something went wrong')
  392. this.ShareJsUpdateManager.promises.applyUpdate.rejects(this.error)
  393. await expect(
  394. this.UpdateManager.promises.applyUpdate(
  395. this.project_id,
  396. this.doc_id,
  397. this.update
  398. )
  399. ).to.be.rejectedWith(this.error)
  400. })
  401. it('should call RealTimeRedisManager.sendData with the error', function () {
  402. this.RealTimeRedisManager.sendData
  403. .calledWith({
  404. project_id: this.project_id,
  405. doc_id: this.doc_id,
  406. error: this.error.message,
  407. })
  408. .should.equal(true)
  409. })
  410. })
  411. describe('when ranges get collapsed', function () {
  412. beforeEach(async function () {
  413. this.RangesManager.applyUpdate.returns({
  414. newRanges: this.updated_ranges,
  415. rangesWereCollapsed: true,
  416. historyUpdates: this.historyUpdates,
  417. })
  418. await this.UpdateManager.promises.applyUpdate(
  419. this.project_id,
  420. this.doc_id,
  421. this.update
  422. )
  423. })
  424. it('should increment the doc-snapshot metric', function () {
  425. this.Metrics.inc.calledWith('doc-snapshot').should.equal(true)
  426. })
  427. it('should call SnapshotManager.recordSnapshot', function () {
  428. this.SnapshotManager.promises.recordSnapshot
  429. .calledWith(
  430. this.project_id,
  431. this.doc_id,
  432. this.version,
  433. this.pathname,
  434. this.lines,
  435. this.ranges
  436. )
  437. .should.equal(true)
  438. })
  439. })
  440. describe('when history ranges are supported', function () {
  441. beforeEach(async function () {
  442. this.DocumentManager.promises.getDoc.resolves({
  443. lines: this.lines,
  444. version: this.version,
  445. ranges: this.ranges,
  446. pathname: this.pathname,
  447. projectHistoryId: this.projectHistoryId,
  448. historyRangesSupport: true,
  449. })
  450. await this.UpdateManager.promises.applyUpdate(
  451. this.project_id,
  452. this.doc_id,
  453. this.update
  454. )
  455. })
  456. it('should push the history updates into the history queue', function () {
  457. this.ProjectHistoryRedisManager.promises.queueOps.should.have.been.calledWith(
  458. this.project_id,
  459. ...this.historyUpdates.map(op => JSON.stringify(op))
  460. )
  461. this.HistoryManager.recordAndFlushHistoryOps.should.have.been.calledWith(
  462. this.project_id,
  463. this.historyUpdates,
  464. this.historyUpdates.length
  465. )
  466. })
  467. })
  468. })
  469. describe('_adjustHistoryUpdatesMetadata', function () {
  470. beforeEach(function () {
  471. this.lines = ['some', 'test', 'data']
  472. this.historyUpdates = [
  473. {
  474. v: 42,
  475. op: [
  476. { i: 'bing', p: 12, trackedDeleteRejection: true },
  477. { i: 'foo', p: 4 },
  478. { i: 'bar', p: 6 },
  479. ],
  480. },
  481. {
  482. v: 45,
  483. op: [
  484. { d: 'qux', p: 4 },
  485. { i: 'bazbaz', p: 14 },
  486. {
  487. d: 'bong',
  488. p: 28,
  489. trackedChanges: [{ type: 'insert', offset: 0, length: 4 }],
  490. },
  491. ],
  492. meta: {
  493. tc: 'tracking-info',
  494. },
  495. },
  496. {
  497. v: 47,
  498. op: [{ d: 'so', p: 0 }],
  499. },
  500. { v: 49, op: [{ i: 'penguin', p: 18 }] },
  501. ]
  502. this.ranges = {
  503. changes: [
  504. { op: { d: 'bingbong', p: 12 } },
  505. { op: { i: 'test', p: 5 } },
  506. ],
  507. }
  508. })
  509. it('should add projectHistoryId, pathname and doc_length metadata to the ops', function () {
  510. this.UpdateManager._adjustHistoryUpdatesMetadata(
  511. this.historyUpdates,
  512. this.pathname,
  513. this.projectHistoryId,
  514. this.lines,
  515. this.ranges,
  516. false
  517. )
  518. this.historyUpdates.should.deep.equal([
  519. {
  520. projectHistoryId: this.projectHistoryId,
  521. v: 42,
  522. op: [
  523. { i: 'bing', p: 12, trackedDeleteRejection: true },
  524. { i: 'foo', p: 4 },
  525. { i: 'bar', p: 6 },
  526. ],
  527. meta: {
  528. pathname: this.pathname,
  529. doc_length: 14,
  530. },
  531. },
  532. {
  533. projectHistoryId: this.projectHistoryId,
  534. v: 45,
  535. op: [
  536. { d: 'qux', p: 4 },
  537. { i: 'bazbaz', p: 14 },
  538. {
  539. d: 'bong',
  540. p: 28,
  541. trackedChanges: [{ type: 'insert', offset: 0, length: 4 }],
  542. },
  543. ],
  544. meta: {
  545. pathname: this.pathname,
  546. doc_length: 24, // 14 + 'bing' + 'foo' + 'bar'
  547. },
  548. },
  549. {
  550. projectHistoryId: this.projectHistoryId,
  551. v: 47,
  552. op: [{ d: 'so', p: 0 }],
  553. meta: {
  554. pathname: this.pathname,
  555. doc_length: 23, // 24 - 'qux' + 'bazbaz' - 'bong'
  556. },
  557. },
  558. {
  559. projectHistoryId: this.projectHistoryId,
  560. v: 49,
  561. op: [{ i: 'penguin', p: 18 }],
  562. meta: {
  563. pathname: this.pathname,
  564. doc_length: 21, // 23 - 'so'
  565. },
  566. },
  567. ])
  568. })
  569. it('should add additional metadata when ranges support is enabled', function () {
  570. this.UpdateManager._adjustHistoryUpdatesMetadata(
  571. this.historyUpdates,
  572. this.pathname,
  573. this.projectHistoryId,
  574. this.lines,
  575. this.ranges,
  576. true
  577. )
  578. this.historyUpdates.should.deep.equal([
  579. {
  580. projectHistoryId: this.projectHistoryId,
  581. v: 42,
  582. op: [
  583. { i: 'bing', p: 12, trackedDeleteRejection: true },
  584. { i: 'foo', p: 4 },
  585. { i: 'bar', p: 6 },
  586. ],
  587. meta: {
  588. pathname: this.pathname,
  589. doc_length: 14,
  590. history_doc_length: 22,
  591. },
  592. },
  593. {
  594. projectHistoryId: this.projectHistoryId,
  595. v: 45,
  596. op: [
  597. { d: 'qux', p: 4 },
  598. { i: 'bazbaz', p: 14 },
  599. {
  600. d: 'bong',
  601. p: 28,
  602. trackedChanges: [{ type: 'insert', offset: 0, length: 4 }],
  603. },
  604. ],
  605. meta: {
  606. pathname: this.pathname,
  607. doc_length: 24, // 14 + 'bing' + 'foo' + 'bar'
  608. history_doc_length: 28, // 22 + 'foo' + 'bar'
  609. tc: 'tracking-info',
  610. },
  611. },
  612. {
  613. projectHistoryId: this.projectHistoryId,
  614. v: 47,
  615. op: [{ d: 'so', p: 0 }],
  616. meta: {
  617. pathname: this.pathname,
  618. doc_length: 23, // 24 - 'qux' + 'bazbaz' - 'bong'
  619. history_doc_length: 30, // 28 - 'bong' + 'bazbaz'
  620. },
  621. },
  622. {
  623. projectHistoryId: this.projectHistoryId,
  624. v: 49,
  625. op: [{ i: 'penguin', p: 18 }],
  626. meta: {
  627. pathname: this.pathname,
  628. doc_length: 21, // 23 - 'so'
  629. history_doc_length: 28, // 30 - 'so'
  630. },
  631. },
  632. ])
  633. })
  634. it('should calculate the right doc length for an empty document', function () {
  635. this.historyUpdates = [{ v: 42, op: [{ i: 'foobar', p: 0 }] }]
  636. this.UpdateManager._adjustHistoryUpdatesMetadata(
  637. this.historyUpdates,
  638. this.pathname,
  639. this.projectHistoryId,
  640. [],
  641. {},
  642. false
  643. )
  644. this.historyUpdates.should.deep.equal([
  645. {
  646. projectHistoryId: this.projectHistoryId,
  647. v: 42,
  648. op: [{ i: 'foobar', p: 0 }],
  649. meta: {
  650. pathname: this.pathname,
  651. doc_length: 0,
  652. },
  653. },
  654. ])
  655. })
  656. })
  657. describe('lockUpdatesAndDo', function () {
  658. beforeEach(function () {
  659. this.methodResult = 'method result'
  660. this.method = sinon.stub().resolves(this.methodResult)
  661. this.arg1 = 'argument 1'
  662. })
  663. describe('successfully', function () {
  664. beforeEach(async function () {
  665. this.UpdateManager.promises.continueProcessingUpdatesWithLock = sinon
  666. .stub()
  667. .resolves()
  668. this.UpdateManager.promises.processOutstandingUpdates = sinon
  669. .stub()
  670. .resolves()
  671. this.response = await this.UpdateManager.promises.lockUpdatesAndDo(
  672. this.method,
  673. this.project_id,
  674. this.doc_id,
  675. this.arg1
  676. )
  677. })
  678. it('should lock the doc', function () {
  679. this.LockManager.promises.getLock
  680. .calledWith(this.doc_id)
  681. .should.equal(true)
  682. })
  683. it('should process any outstanding updates', function () {
  684. this.UpdateManager.promises.processOutstandingUpdates.should.have.been.calledWith(
  685. this.project_id,
  686. this.doc_id
  687. )
  688. })
  689. it('should call the method', function () {
  690. this.method
  691. .calledWith(this.project_id, this.doc_id, this.arg1)
  692. .should.equal(true)
  693. })
  694. it('should return the method response arguments', function () {
  695. expect(this.response).to.equal(this.methodResult)
  696. })
  697. it('should release the lock', function () {
  698. this.LockManager.promises.releaseLock
  699. .calledWith(this.doc_id, this.lockValue)
  700. .should.equal(true)
  701. })
  702. it('should continue processing updates', function () {
  703. this.UpdateManager.promises.continueProcessingUpdatesWithLock
  704. .calledWith(this.project_id, this.doc_id)
  705. .should.equal(true)
  706. })
  707. })
  708. describe('when processOutstandingUpdates returns an error', function () {
  709. beforeEach(async function () {
  710. this.error = new Error('Something went wrong')
  711. this.UpdateManager.promises.processOutstandingUpdates = sinon
  712. .stub()
  713. .rejects(this.error)
  714. await expect(
  715. this.UpdateManager.promises.lockUpdatesAndDo(
  716. this.method,
  717. this.project_id,
  718. this.doc_id,
  719. this.arg1
  720. )
  721. ).to.be.rejectedWith(this.error)
  722. })
  723. it('should free the lock', function () {
  724. this.LockManager.promises.releaseLock
  725. .calledWith(this.doc_id, this.lockValue)
  726. .should.equal(true)
  727. })
  728. })
  729. describe('when the method returns an error', function () {
  730. beforeEach(async function () {
  731. this.error = new Error('something went wrong')
  732. this.UpdateManager.promises.processOutstandingUpdates = sinon
  733. .stub()
  734. .resolves()
  735. this.method = sinon.stub().rejects(this.error)
  736. await expect(
  737. this.UpdateManager.promises.lockUpdatesAndDo(
  738. this.method,
  739. this.project_id,
  740. this.doc_id,
  741. this.arg1
  742. )
  743. ).to.be.rejectedWith(this.error)
  744. })
  745. it('should free the lock', function () {
  746. this.LockManager.promises.releaseLock
  747. .calledWith(this.doc_id, this.lockValue)
  748. .should.equal(true)
  749. })
  750. })
  751. })
  752. })