UpdateManagerTests.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  1. const { createHash } = require('node:crypto')
  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. this.ranges,
  359. this.updatedDocLines
  360. )
  361. })
  362. it('should push the applied ops into the history queue', function () {
  363. this.ProjectHistoryRedisManager.promises.queueOps.should.have.been.calledWith(
  364. this.project_id,
  365. ...this.historyUpdates.map(op => JSON.stringify(op))
  366. )
  367. this.HistoryManager.recordAndFlushHistoryOps.should.have.been.calledWith(
  368. this.project_id,
  369. this.historyUpdates,
  370. this.historyUpdates.length
  371. )
  372. })
  373. })
  374. describe('with UTF-16 surrogate pairs in the update', function () {
  375. beforeEach(async function () {
  376. this.update = { op: [{ p: 42, i: '\uD835\uDC00' }] }
  377. await this.UpdateManager.promises.applyUpdate(
  378. this.project_id,
  379. this.doc_id,
  380. this.update
  381. )
  382. })
  383. it('should apply the update but with surrogate pairs removed', function () {
  384. this.ShareJsUpdateManager.promises.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(async function () {
  393. this.error = new Error('something went wrong')
  394. this.ShareJsUpdateManager.promises.applyUpdate.rejects(this.error)
  395. await expect(
  396. this.UpdateManager.promises.applyUpdate(
  397. this.project_id,
  398. this.doc_id,
  399. this.update
  400. )
  401. ).to.be.rejectedWith(this.error)
  402. })
  403. it('should call RealTimeRedisManager.sendData with the error', function () {
  404. this.RealTimeRedisManager.sendData
  405. .calledWith({
  406. project_id: this.project_id,
  407. doc_id: this.doc_id,
  408. error: this.error.message,
  409. })
  410. .should.equal(true)
  411. })
  412. })
  413. describe('when ranges get collapsed', function () {
  414. beforeEach(async function () {
  415. this.RangesManager.applyUpdate.returns({
  416. newRanges: this.updated_ranges,
  417. rangesWereCollapsed: true,
  418. historyUpdates: this.historyUpdates,
  419. })
  420. await this.UpdateManager.promises.applyUpdate(
  421. this.project_id,
  422. this.doc_id,
  423. this.update
  424. )
  425. })
  426. it('should increment the doc-snapshot metric', function () {
  427. this.Metrics.inc.calledWith('doc-snapshot').should.equal(true)
  428. })
  429. it('should call SnapshotManager.recordSnapshot', function () {
  430. this.SnapshotManager.promises.recordSnapshot
  431. .calledWith(
  432. this.project_id,
  433. this.doc_id,
  434. this.version,
  435. this.pathname,
  436. this.lines,
  437. this.ranges
  438. )
  439. .should.equal(true)
  440. })
  441. })
  442. describe('when history ranges are supported', function () {
  443. beforeEach(async function () {
  444. this.DocumentManager.promises.getDoc.resolves({
  445. lines: this.lines,
  446. version: this.version,
  447. ranges: this.ranges,
  448. pathname: this.pathname,
  449. projectHistoryId: this.projectHistoryId,
  450. historyRangesSupport: true,
  451. })
  452. await this.UpdateManager.promises.applyUpdate(
  453. this.project_id,
  454. this.doc_id,
  455. this.update
  456. )
  457. })
  458. it('should push the history updates into the history queue', function () {
  459. this.ProjectHistoryRedisManager.promises.queueOps.should.have.been.calledWith(
  460. this.project_id,
  461. ...this.historyUpdates.map(op => JSON.stringify(op))
  462. )
  463. this.HistoryManager.recordAndFlushHistoryOps.should.have.been.calledWith(
  464. this.project_id,
  465. this.historyUpdates,
  466. this.historyUpdates.length
  467. )
  468. })
  469. })
  470. })
  471. describe('_adjustHistoryUpdatesMetadata', function () {
  472. beforeEach(function () {
  473. this.lines = ['some', 'test', 'data']
  474. this.updatedDocLines = ['after', 'updates']
  475. this.historyUpdates = [
  476. {
  477. v: 42,
  478. op: [
  479. { i: 'bing', p: 12, trackedDeleteRejection: true },
  480. { i: 'foo', p: 4 },
  481. { i: 'bar', p: 6 },
  482. ],
  483. },
  484. {
  485. v: 45,
  486. op: [
  487. { d: 'qux', p: 4 },
  488. { i: 'bazbaz', p: 14 },
  489. {
  490. d: 'bong',
  491. p: 28,
  492. trackedChanges: [{ type: 'insert', offset: 0, length: 4 }],
  493. },
  494. ],
  495. meta: {
  496. tc: 'tracking-info',
  497. },
  498. },
  499. {
  500. v: 47,
  501. op: [{ d: 'so', p: 0 }],
  502. },
  503. { v: 49, op: [{ i: 'penguin', p: 18 }] },
  504. ]
  505. this.ranges = {
  506. changes: [
  507. { op: { d: 'bingbong', p: 12 } },
  508. { op: { i: 'test', p: 5 } },
  509. ],
  510. }
  511. })
  512. it('should add projectHistoryId, pathname and doc_length metadata to the ops', function () {
  513. this.UpdateManager._adjustHistoryUpdatesMetadata(
  514. this.historyUpdates,
  515. this.pathname,
  516. this.projectHistoryId,
  517. this.lines,
  518. this.updatedDocLines,
  519. this.ranges,
  520. false
  521. )
  522. this.historyUpdates.should.deep.equal([
  523. {
  524. projectHistoryId: this.projectHistoryId,
  525. v: 42,
  526. op: [
  527. { i: 'bing', p: 12, trackedDeleteRejection: true },
  528. { i: 'foo', p: 4 },
  529. { i: 'bar', p: 6 },
  530. ],
  531. meta: {
  532. pathname: this.pathname,
  533. doc_length: 14,
  534. },
  535. },
  536. {
  537. projectHistoryId: this.projectHistoryId,
  538. v: 45,
  539. op: [
  540. { d: 'qux', p: 4 },
  541. { i: 'bazbaz', p: 14 },
  542. {
  543. d: 'bong',
  544. p: 28,
  545. trackedChanges: [{ type: 'insert', offset: 0, length: 4 }],
  546. },
  547. ],
  548. meta: {
  549. pathname: this.pathname,
  550. doc_length: 24, // 14 + 'bing' + 'foo' + 'bar'
  551. },
  552. },
  553. {
  554. projectHistoryId: this.projectHistoryId,
  555. v: 47,
  556. op: [{ d: 'so', p: 0 }],
  557. meta: {
  558. pathname: this.pathname,
  559. doc_length: 23, // 24 - 'qux' + 'bazbaz' - 'bong'
  560. },
  561. },
  562. {
  563. projectHistoryId: this.projectHistoryId,
  564. v: 49,
  565. op: [{ i: 'penguin', p: 18 }],
  566. meta: {
  567. pathname: this.pathname,
  568. doc_length: 21, // 23 - 'so'
  569. },
  570. },
  571. ])
  572. })
  573. it('should add additional metadata when ranges support is enabled', function () {
  574. this.UpdateManager._adjustHistoryUpdatesMetadata(
  575. this.historyUpdates,
  576. this.pathname,
  577. this.projectHistoryId,
  578. this.lines,
  579. this.ranges,
  580. this.updatedDocLines,
  581. true
  582. )
  583. this.historyUpdates.should.deep.equal([
  584. {
  585. projectHistoryId: this.projectHistoryId,
  586. v: 42,
  587. op: [
  588. { i: 'bing', p: 12, trackedDeleteRejection: true },
  589. { i: 'foo', p: 4 },
  590. { i: 'bar', p: 6 },
  591. ],
  592. meta: {
  593. pathname: this.pathname,
  594. doc_length: 14,
  595. history_doc_length: 22,
  596. },
  597. },
  598. {
  599. projectHistoryId: this.projectHistoryId,
  600. v: 45,
  601. op: [
  602. { d: 'qux', p: 4 },
  603. { i: 'bazbaz', p: 14 },
  604. {
  605. d: 'bong',
  606. p: 28,
  607. trackedChanges: [{ type: 'insert', offset: 0, length: 4 }],
  608. },
  609. ],
  610. meta: {
  611. pathname: this.pathname,
  612. doc_length: 24, // 14 + 'bing' + 'foo' + 'bar'
  613. history_doc_length: 28, // 22 + 'foo' + 'bar'
  614. tc: 'tracking-info',
  615. },
  616. },
  617. {
  618. projectHistoryId: this.projectHistoryId,
  619. v: 47,
  620. op: [{ d: 'so', p: 0 }],
  621. meta: {
  622. pathname: this.pathname,
  623. doc_length: 23, // 24 - 'qux' + 'bazbaz' - 'bong'
  624. history_doc_length: 30, // 28 - 'bong' + 'bazbaz'
  625. },
  626. },
  627. {
  628. projectHistoryId: this.projectHistoryId,
  629. v: 49,
  630. op: [{ i: 'penguin', p: 18 }],
  631. meta: {
  632. pathname: this.pathname,
  633. doc_length: 21, // 23 - 'so'
  634. doc_hash: stringHash(this.updatedDocLines.join('\n')),
  635. history_doc_length: 28, // 30 - 'so'
  636. },
  637. },
  638. ])
  639. })
  640. it('should calculate the right doc length for an empty document', function () {
  641. this.historyUpdates = [{ v: 42, op: [{ i: 'foobar', p: 0 }] }]
  642. this.UpdateManager._adjustHistoryUpdatesMetadata(
  643. this.historyUpdates,
  644. this.pathname,
  645. this.projectHistoryId,
  646. [],
  647. {},
  648. ['foobar'],
  649. false
  650. )
  651. this.historyUpdates.should.deep.equal([
  652. {
  653. projectHistoryId: this.projectHistoryId,
  654. v: 42,
  655. op: [{ i: 'foobar', p: 0 }],
  656. meta: {
  657. pathname: this.pathname,
  658. doc_length: 0,
  659. },
  660. },
  661. ])
  662. })
  663. })
  664. describe('lockUpdatesAndDo', function () {
  665. beforeEach(function () {
  666. this.methodResult = 'method result'
  667. this.method = sinon.stub().resolves(this.methodResult)
  668. this.arg1 = 'argument 1'
  669. })
  670. describe('successfully', function () {
  671. beforeEach(async function () {
  672. this.UpdateManager.promises.continueProcessingUpdatesWithLock = sinon
  673. .stub()
  674. .resolves()
  675. this.UpdateManager.promises.processOutstandingUpdates = sinon
  676. .stub()
  677. .resolves()
  678. this.response = await this.UpdateManager.promises.lockUpdatesAndDo(
  679. this.method,
  680. this.project_id,
  681. this.doc_id,
  682. this.arg1
  683. )
  684. })
  685. it('should lock the doc', function () {
  686. this.LockManager.promises.getLock
  687. .calledWith(this.doc_id)
  688. .should.equal(true)
  689. })
  690. it('should process any outstanding updates', function () {
  691. this.UpdateManager.promises.processOutstandingUpdates.should.have.been.calledWith(
  692. this.project_id,
  693. this.doc_id
  694. )
  695. })
  696. it('should call the method', function () {
  697. this.method
  698. .calledWith(this.project_id, this.doc_id, this.arg1)
  699. .should.equal(true)
  700. })
  701. it('should return the method response arguments', function () {
  702. expect(this.response).to.equal(this.methodResult)
  703. })
  704. it('should release the lock', function () {
  705. this.LockManager.promises.releaseLock
  706. .calledWith(this.doc_id, this.lockValue)
  707. .should.equal(true)
  708. })
  709. it('should continue processing updates', function () {
  710. this.UpdateManager.promises.continueProcessingUpdatesWithLock
  711. .calledWith(this.project_id, this.doc_id)
  712. .should.equal(true)
  713. })
  714. })
  715. describe('when processOutstandingUpdates returns an error', function () {
  716. beforeEach(async function () {
  717. this.error = new Error('Something went wrong')
  718. this.UpdateManager.promises.processOutstandingUpdates = sinon
  719. .stub()
  720. .rejects(this.error)
  721. await expect(
  722. this.UpdateManager.promises.lockUpdatesAndDo(
  723. this.method,
  724. this.project_id,
  725. this.doc_id,
  726. this.arg1
  727. )
  728. ).to.be.rejectedWith(this.error)
  729. })
  730. it('should free the lock', function () {
  731. this.LockManager.promises.releaseLock
  732. .calledWith(this.doc_id, this.lockValue)
  733. .should.equal(true)
  734. })
  735. })
  736. describe('when the method returns an error', function () {
  737. beforeEach(async function () {
  738. this.error = new Error('something went wrong')
  739. this.UpdateManager.promises.processOutstandingUpdates = sinon
  740. .stub()
  741. .resolves()
  742. this.method = sinon.stub().rejects(this.error)
  743. await expect(
  744. this.UpdateManager.promises.lockUpdatesAndDo(
  745. this.method,
  746. this.project_id,
  747. this.doc_id,
  748. this.arg1
  749. )
  750. ).to.be.rejectedWith(this.error)
  751. })
  752. it('should free the lock', function () {
  753. this.LockManager.promises.releaseLock
  754. .calledWith(this.doc_id, this.lockValue)
  755. .should.equal(true)
  756. })
  757. })
  758. })
  759. })
  760. function stringHash(s) {
  761. const hash = createHash('sha1')
  762. hash.update(s)
  763. return hash.digest('hex')
  764. }