UpdateManagerTests.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812
  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. { d: 'bong', p: 28, u: true },
  487. ],
  488. meta: {
  489. tc: 'tracking-info',
  490. },
  491. },
  492. {
  493. v: 47,
  494. op: [{ d: 'so', p: 0 }],
  495. },
  496. { v: 49, op: [{ i: 'penguin', p: 18 }] },
  497. ]
  498. this.ranges = {
  499. changes: [
  500. { op: { d: 'bingbong', p: 12 } },
  501. { op: { i: 'test', p: 5 } },
  502. ],
  503. }
  504. })
  505. it('should add projectHistoryId, pathname and doc_length metadata to the ops', function () {
  506. this.UpdateManager._adjustHistoryUpdatesMetadata(
  507. this.historyUpdates,
  508. this.pathname,
  509. this.projectHistoryId,
  510. this.lines,
  511. this.ranges,
  512. false
  513. )
  514. this.historyUpdates.should.deep.equal([
  515. {
  516. projectHistoryId: this.projectHistoryId,
  517. v: 42,
  518. op: [
  519. { i: 'bing', p: 12, trackedDeleteRejection: true },
  520. { i: 'foo', p: 4 },
  521. { i: 'bar', p: 6 },
  522. ],
  523. meta: {
  524. pathname: this.pathname,
  525. doc_length: 14,
  526. },
  527. },
  528. {
  529. projectHistoryId: this.projectHistoryId,
  530. v: 45,
  531. op: [
  532. { d: 'qux', p: 4 },
  533. { i: 'bazbaz', p: 14 },
  534. { d: 'bong', p: 28, u: true },
  535. ],
  536. meta: {
  537. pathname: this.pathname,
  538. doc_length: 24, // 14 + 'bing' + 'foo' + 'bar'
  539. },
  540. },
  541. {
  542. projectHistoryId: this.projectHistoryId,
  543. v: 47,
  544. op: [{ d: 'so', p: 0 }],
  545. meta: {
  546. pathname: this.pathname,
  547. doc_length: 23, // 24 - 'qux' + 'bazbaz' - 'bong'
  548. },
  549. },
  550. {
  551. projectHistoryId: this.projectHistoryId,
  552. v: 49,
  553. op: [{ i: 'penguin', p: 18 }],
  554. meta: {
  555. pathname: this.pathname,
  556. doc_length: 21, // 23 - 'so'
  557. },
  558. },
  559. ])
  560. })
  561. it('should add additional metadata when ranges support is enabled', function () {
  562. this.UpdateManager._adjustHistoryUpdatesMetadata(
  563. this.historyUpdates,
  564. this.pathname,
  565. this.projectHistoryId,
  566. this.lines,
  567. this.ranges,
  568. true
  569. )
  570. this.historyUpdates.should.deep.equal([
  571. {
  572. projectHistoryId: this.projectHistoryId,
  573. v: 42,
  574. op: [
  575. { i: 'bing', p: 12, trackedDeleteRejection: true },
  576. { i: 'foo', p: 4 },
  577. { i: 'bar', p: 6 },
  578. ],
  579. meta: {
  580. pathname: this.pathname,
  581. doc_length: 14,
  582. history_doc_length: 22,
  583. },
  584. },
  585. {
  586. projectHistoryId: this.projectHistoryId,
  587. v: 45,
  588. op: [
  589. { d: 'qux', p: 4 },
  590. { i: 'bazbaz', p: 14 },
  591. { d: 'bong', p: 28, u: true },
  592. ],
  593. meta: {
  594. pathname: this.pathname,
  595. doc_length: 24, // 14 + 'bing' + 'foo' + 'bar'
  596. history_doc_length: 28, // 22 + 'foo' + 'bar'
  597. tc: 'tracking-info',
  598. },
  599. },
  600. {
  601. projectHistoryId: this.projectHistoryId,
  602. v: 47,
  603. op: [{ d: 'so', p: 0 }],
  604. meta: {
  605. pathname: this.pathname,
  606. doc_length: 23, // 24 - 'qux' + 'bazbaz' - 'bong'
  607. history_doc_length: 30, // 28 - 'bong' + 'bazbaz'
  608. },
  609. },
  610. {
  611. projectHistoryId: this.projectHistoryId,
  612. v: 49,
  613. op: [{ i: 'penguin', p: 18 }],
  614. meta: {
  615. pathname: this.pathname,
  616. doc_length: 21, // 23 - 'so'
  617. history_doc_length: 28, // 30 - 'so'
  618. },
  619. },
  620. ])
  621. })
  622. it('should calculate the right doc length for an empty document', function () {
  623. this.historyUpdates = [{ v: 42, op: [{ i: 'foobar', p: 0 }] }]
  624. this.UpdateManager._adjustHistoryUpdatesMetadata(
  625. this.historyUpdates,
  626. this.pathname,
  627. this.projectHistoryId,
  628. [],
  629. {},
  630. false
  631. )
  632. this.historyUpdates.should.deep.equal([
  633. {
  634. projectHistoryId: this.projectHistoryId,
  635. v: 42,
  636. op: [{ i: 'foobar', p: 0 }],
  637. meta: {
  638. pathname: this.pathname,
  639. doc_length: 0,
  640. },
  641. },
  642. ])
  643. })
  644. })
  645. describe('lockUpdatesAndDo', function () {
  646. beforeEach(function () {
  647. this.methodResult = 'method result'
  648. this.method = sinon.stub().resolves(this.methodResult)
  649. this.arg1 = 'argument 1'
  650. })
  651. describe('successfully', function () {
  652. beforeEach(async function () {
  653. this.UpdateManager.promises.continueProcessingUpdatesWithLock = sinon
  654. .stub()
  655. .resolves()
  656. this.UpdateManager.promises.processOutstandingUpdates = sinon
  657. .stub()
  658. .resolves()
  659. this.response = await this.UpdateManager.promises.lockUpdatesAndDo(
  660. this.method,
  661. this.project_id,
  662. this.doc_id,
  663. this.arg1
  664. )
  665. })
  666. it('should lock the doc', function () {
  667. this.LockManager.promises.getLock
  668. .calledWith(this.doc_id)
  669. .should.equal(true)
  670. })
  671. it('should process any outstanding updates', function () {
  672. this.UpdateManager.promises.processOutstandingUpdates.should.have.been.calledWith(
  673. this.project_id,
  674. this.doc_id
  675. )
  676. })
  677. it('should call the method', function () {
  678. this.method
  679. .calledWith(this.project_id, this.doc_id, this.arg1)
  680. .should.equal(true)
  681. })
  682. it('should return the method response arguments', function () {
  683. expect(this.response).to.equal(this.methodResult)
  684. })
  685. it('should release the lock', function () {
  686. this.LockManager.promises.releaseLock
  687. .calledWith(this.doc_id, this.lockValue)
  688. .should.equal(true)
  689. })
  690. it('should continue processing updates', function () {
  691. this.UpdateManager.promises.continueProcessingUpdatesWithLock
  692. .calledWith(this.project_id, this.doc_id)
  693. .should.equal(true)
  694. })
  695. })
  696. describe('when processOutstandingUpdates returns an error', function () {
  697. beforeEach(async function () {
  698. this.error = new Error('Something went wrong')
  699. this.UpdateManager.promises.processOutstandingUpdates = sinon
  700. .stub()
  701. .rejects(this.error)
  702. await expect(
  703. this.UpdateManager.promises.lockUpdatesAndDo(
  704. this.method,
  705. this.project_id,
  706. this.doc_id,
  707. this.arg1
  708. )
  709. ).to.be.rejectedWith(this.error)
  710. })
  711. it('should free the lock', function () {
  712. this.LockManager.promises.releaseLock
  713. .calledWith(this.doc_id, this.lockValue)
  714. .should.equal(true)
  715. })
  716. })
  717. describe('when the method returns an error', function () {
  718. beforeEach(async function () {
  719. this.error = new Error('something went wrong')
  720. this.UpdateManager.promises.processOutstandingUpdates = sinon
  721. .stub()
  722. .resolves()
  723. this.method = sinon.stub().rejects(this.error)
  724. await expect(
  725. this.UpdateManager.promises.lockUpdatesAndDo(
  726. this.method,
  727. this.project_id,
  728. this.doc_id,
  729. this.arg1
  730. )
  731. ).to.be.rejectedWith(this.error)
  732. })
  733. it('should free the lock', function () {
  734. this.LockManager.promises.releaseLock
  735. .calledWith(this.doc_id, this.lockValue)
  736. .should.equal(true)
  737. })
  738. })
  739. })
  740. })