| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917 |
- const { createHash } = require('node:crypto')
- const sinon = require('sinon')
- const { expect } = require('chai')
- const SandboxedModule = require('sandboxed-module')
- const MODULE_PATH = '../../../../app/js/UpdateManager.js'
- describe('UpdateManager', function () {
- beforeEach(function () {
- this.project_id = 'project-id-123'
- this.projectHistoryId = 'history-id-123'
- this.doc_id = 'document-id-123'
- this.lockValue = 'mock-lock-value'
- this.pathname = '/a/b/c.tex'
- this.Metrics = {
- inc: sinon.stub(),
- Timer: class Timer {},
- }
- this.Metrics.Timer.prototype.done = sinon.stub()
- this.Profiler = class Profiler {}
- this.Profiler.prototype.log = sinon.stub().returns({ end: sinon.stub() })
- this.Profiler.prototype.end = sinon.stub()
- this.LockManager = {
- promises: {
- tryLock: sinon.stub().resolves(this.lockValue),
- getLock: sinon.stub().resolves(this.lockValue),
- releaseLock: sinon.stub().resolves(),
- },
- }
- this.RedisManager = {
- promises: {
- setDocument: sinon.stub().resolves(),
- updateDocument: sinon.stub(),
- },
- }
- this.RealTimeRedisManager = {
- sendData: sinon.stub(),
- promises: {
- getUpdatesLength: sinon.stub(),
- getPendingUpdatesForDoc: sinon.stub(),
- },
- }
- this.ShareJsUpdateManager = {
- promises: {
- applyUpdate: sinon.stub(),
- },
- }
- this.HistoryManager = {
- recordAndFlushHistoryOps: sinon.stub(),
- }
- this.Settings = {}
- this.DocumentManager = {
- promises: {
- getDoc: sinon.stub(),
- },
- }
- this.RangesManager = {
- applyUpdate: sinon.stub(),
- }
- this.SnapshotManager = {
- promises: {
- recordSnapshot: sinon.stub().resolves(),
- },
- }
- this.WebApiManager = {
- promises: {
- notifyTrackChangesRejected: sinon.stub().resolves(),
- },
- }
- this.ProjectHistoryRedisManager = {
- promises: {
- queueOps: sinon
- .stub()
- .callsFake(async (projectId, ...ops) => ops.length),
- },
- }
- this.UpdateManager = SandboxedModule.require(MODULE_PATH, {
- requires: {
- './LockManager': this.LockManager,
- './RedisManager': this.RedisManager,
- './RealTimeRedisManager': this.RealTimeRedisManager,
- './ShareJsUpdateManager': this.ShareJsUpdateManager,
- './HistoryManager': this.HistoryManager,
- './Metrics': this.Metrics,
- '@overleaf/settings': this.Settings,
- './DocumentManager': this.DocumentManager,
- './RangesManager': this.RangesManager,
- './SnapshotManager': this.SnapshotManager,
- './WebApiManager': this.WebApiManager,
- './Profiler': this.Profiler,
- './ProjectHistoryRedisManager': this.ProjectHistoryRedisManager,
- },
- })
- })
- describe('processOutstandingUpdates', function () {
- beforeEach(async function () {
- this.UpdateManager.promises.fetchAndApplyUpdates = sinon.stub().resolves()
- await this.UpdateManager.promises.processOutstandingUpdates(
- this.project_id,
- this.doc_id
- )
- })
- it('should apply the updates', function () {
- this.UpdateManager.promises.fetchAndApplyUpdates
- .calledWith(this.project_id, this.doc_id)
- .should.equal(true)
- })
- it('should time the execution', function () {
- this.Metrics.Timer.prototype.done.called.should.equal(true)
- })
- })
- describe('processOutstandingUpdatesWithLock', function () {
- describe('when the lock is free', function () {
- beforeEach(function () {
- this.UpdateManager.promises.continueProcessingUpdatesWithLock = sinon
- .stub()
- .resolves()
- this.UpdateManager.promises.processOutstandingUpdates = sinon
- .stub()
- .resolves()
- })
- describe('successfully', function () {
- beforeEach(async function () {
- await this.UpdateManager.promises.processOutstandingUpdatesWithLock(
- this.project_id,
- this.doc_id
- )
- })
- it('should acquire the lock', function () {
- this.LockManager.promises.tryLock
- .calledWith(this.doc_id)
- .should.equal(true)
- })
- it('should free the lock', function () {
- this.LockManager.promises.releaseLock
- .calledWith(this.doc_id, this.lockValue)
- .should.equal(true)
- })
- it('should process the outstanding updates', function () {
- this.UpdateManager.promises.processOutstandingUpdates
- .calledWith(this.project_id, this.doc_id)
- .should.equal(true)
- })
- it('should do everything with the lock acquired', function () {
- this.UpdateManager.promises.processOutstandingUpdates
- .calledAfter(this.LockManager.promises.tryLock)
- .should.equal(true)
- this.UpdateManager.promises.processOutstandingUpdates
- .calledBefore(this.LockManager.promises.releaseLock)
- .should.equal(true)
- })
- it('should continue processing new updates that may have come in', function () {
- this.UpdateManager.promises.continueProcessingUpdatesWithLock
- .calledWith(this.project_id, this.doc_id)
- .should.equal(true)
- })
- })
- describe('when processOutstandingUpdates returns an error', function () {
- beforeEach(async function () {
- this.error = new Error('Something went wrong')
- this.UpdateManager.promises.processOutstandingUpdates = sinon
- .stub()
- .rejects(this.error)
- await expect(
- this.UpdateManager.promises.processOutstandingUpdatesWithLock(
- this.project_id,
- this.doc_id
- )
- ).to.be.rejectedWith(this.error)
- })
- it('should free the lock', function () {
- this.LockManager.promises.releaseLock
- .calledWith(this.doc_id, this.lockValue)
- .should.equal(true)
- })
- })
- })
- describe('when the lock is taken', function () {
- beforeEach(async function () {
- this.LockManager.promises.tryLock.resolves(null)
- this.UpdateManager.promises.processOutstandingUpdates = sinon
- .stub()
- .resolves()
- await this.UpdateManager.promises.processOutstandingUpdatesWithLock(
- this.project_id,
- this.doc_id
- )
- })
- it('should not process the updates', function () {
- this.UpdateManager.promises.processOutstandingUpdates.called.should.equal(
- false
- )
- })
- })
- })
- describe('continueProcessingUpdatesWithLock', function () {
- describe('when there are outstanding updates', function () {
- beforeEach(async function () {
- this.RealTimeRedisManager.promises.getUpdatesLength.resolves(3)
- this.UpdateManager.promises.processOutstandingUpdatesWithLock = sinon
- .stub()
- .resolves()
- await this.UpdateManager.promises.continueProcessingUpdatesWithLock(
- this.project_id,
- this.doc_id
- )
- })
- it('should process the outstanding updates', function () {
- this.UpdateManager.promises.processOutstandingUpdatesWithLock
- .calledWith(this.project_id, this.doc_id)
- .should.equal(true)
- })
- })
- describe('when there are no outstanding updates', function () {
- beforeEach(async function () {
- this.RealTimeRedisManager.promises.getUpdatesLength.resolves(0)
- this.UpdateManager.promises.processOutstandingUpdatesWithLock = sinon
- .stub()
- .resolves()
- await this.UpdateManager.promises.continueProcessingUpdatesWithLock(
- this.project_id,
- this.doc_id
- )
- })
- it('should not try to process the outstanding updates', function () {
- this.UpdateManager.promises.processOutstandingUpdatesWithLock.called.should.equal(
- false
- )
- })
- })
- })
- describe('fetchAndApplyUpdates', function () {
- describe('with updates', function () {
- beforeEach(async function () {
- this.updates = [{ p: 1, t: 'foo' }]
- this.updatedDocLines = ['updated', 'lines']
- this.version = 34
- this.RealTimeRedisManager.promises.getPendingUpdatesForDoc.resolves(
- this.updates
- )
- this.UpdateManager.promises.applyUpdate = sinon.stub().resolves()
- await this.UpdateManager.promises.fetchAndApplyUpdates(
- this.project_id,
- this.doc_id
- )
- })
- it('should get the pending updates', function () {
- this.RealTimeRedisManager.promises.getPendingUpdatesForDoc
- .calledWith(this.doc_id)
- .should.equal(true)
- })
- it('should apply the updates', function () {
- this.updates.map(update =>
- this.UpdateManager.promises.applyUpdate
- .calledWith(this.project_id, this.doc_id, update)
- .should.equal(true)
- )
- })
- })
- describe('when there are no updates', function () {
- beforeEach(async function () {
- this.updates = []
- this.RealTimeRedisManager.promises.getPendingUpdatesForDoc.resolves(
- this.updates
- )
- this.UpdateManager.promises.applyUpdate = sinon.stub().resolves()
- await this.UpdateManager.promises.fetchAndApplyUpdates(
- this.project_id,
- this.doc_id
- )
- })
- it('should not call applyUpdate', function () {
- this.UpdateManager.promises.applyUpdate.called.should.equal(false)
- })
- })
- })
- describe('applyUpdate', function () {
- beforeEach(function () {
- this.updateMeta = { user_id: 'last-author-fake-id' }
- this.update = { op: [{ p: 42, i: 'foo' }], meta: this.updateMeta }
- this.updatedDocLines = ['updated', 'lines']
- this.version = 34
- this.lines = ['original', 'lines']
- this.ranges = { entries: 'mock', comments: 'mock' }
- this.updated_ranges = { entries: 'updated', comments: 'updated' }
- this.appliedOps = [
- { v: 42, op: 'mock-op-42' },
- { v: 45, op: 'mock-op-45' },
- ]
- this.historyUpdates = [
- 'history-update-1',
- 'history-update-2',
- 'history-update-3',
- ]
- this.project_ops_length = 123
- this.DocumentManager.promises.getDoc.resolves({
- lines: this.lines,
- version: this.version,
- ranges: this.ranges,
- pathname: this.pathname,
- projectHistoryId: this.projectHistoryId,
- historyRangesSupport: false,
- type: 'sharejs-text-ot',
- })
- this.RangesManager.applyUpdate.returns({
- newRanges: this.updated_ranges,
- rangesWereCollapsed: false,
- historyUpdates: this.historyUpdates,
- removedChangeIds: [],
- })
- this.ShareJsUpdateManager.promises.applyUpdate = sinon.stub().resolves({
- updatedDocLines: this.updatedDocLines,
- version: this.version,
- appliedOps: this.appliedOps,
- })
- this.RedisManager.promises.updateDocument.resolves()
- this.UpdateManager.promises._adjustHistoryUpdatesMetadata = sinon.stub()
- })
- describe('normally', function () {
- beforeEach(async function () {
- await this.UpdateManager.promises.applyUpdate(
- this.project_id,
- this.doc_id,
- this.update
- )
- })
- it('should apply the updates via ShareJS', function () {
- this.ShareJsUpdateManager.promises.applyUpdate
- .calledWith(
- this.project_id,
- this.doc_id,
- this.update,
- this.lines,
- this.version
- )
- .should.equal(true)
- })
- it('should update the ranges', function () {
- this.RangesManager.applyUpdate
- .calledWith(
- this.project_id,
- this.doc_id,
- this.ranges,
- this.appliedOps,
- this.updatedDocLines
- )
- .should.equal(true)
- })
- it('should save the document', function () {
- this.RedisManager.promises.updateDocument
- .calledWith(
- this.project_id,
- this.doc_id,
- this.updatedDocLines,
- this.version,
- this.appliedOps,
- this.updated_ranges,
- this.updateMeta
- )
- .should.equal(true)
- })
- it('should add metadata to the ops', function () {
- this.UpdateManager.promises._adjustHistoryUpdatesMetadata.should.have.been.calledWith(
- this.historyUpdates,
- this.pathname,
- this.projectHistoryId,
- this.lines,
- this.ranges,
- this.updatedDocLines
- )
- })
- it('should push the applied ops into the history queue', function () {
- this.ProjectHistoryRedisManager.promises.queueOps.should.have.been.calledWith(
- this.project_id,
- ...this.historyUpdates.map(op => JSON.stringify(op))
- )
- this.HistoryManager.recordAndFlushHistoryOps.should.have.been.calledWith(
- this.project_id,
- this.historyUpdates,
- this.historyUpdates.length
- )
- })
- })
- describe('with UTF-16 surrogate pairs in the update', function () {
- beforeEach(async function () {
- this.update = { op: [{ p: 42, i: '\uD835\uDC00' }] }
- await this.UpdateManager.promises.applyUpdate(
- this.project_id,
- this.doc_id,
- this.update
- )
- })
- it('should apply the update but with surrogate pairs removed', function () {
- this.ShareJsUpdateManager.promises.applyUpdate
- .calledWith(this.project_id, this.doc_id, this.update)
- .should.equal(true)
- // \uFFFD is 'replacement character'
- this.update.op[0].i.should.equal('\uFFFD\uFFFD')
- })
- })
- describe('with an error', function () {
- beforeEach(async function () {
- this.error = new Error('something went wrong')
- this.ShareJsUpdateManager.promises.applyUpdate.rejects(this.error)
- await expect(
- this.UpdateManager.promises.applyUpdate(
- this.project_id,
- this.doc_id,
- this.update
- )
- ).to.be.rejectedWith(this.error)
- })
- it('should call RealTimeRedisManager.sendData with the error', function () {
- this.RealTimeRedisManager.sendData
- .calledWith({
- project_id: this.project_id,
- doc_id: this.doc_id,
- error: this.error.message,
- })
- .should.equal(true)
- })
- })
- describe('when ranges get collapsed', function () {
- beforeEach(async function () {
- this.RangesManager.applyUpdate.returns({
- newRanges: this.updated_ranges,
- rangesWereCollapsed: true,
- historyUpdates: this.historyUpdates,
- removedChangeIds: [],
- })
- await this.UpdateManager.promises.applyUpdate(
- this.project_id,
- this.doc_id,
- this.update
- )
- })
- it('should increment the doc-snapshot metric', function () {
- this.Metrics.inc.calledWith('doc-snapshot').should.equal(true)
- })
- it('should call SnapshotManager.recordSnapshot', function () {
- this.SnapshotManager.promises.recordSnapshot
- .calledWith(
- this.project_id,
- this.doc_id,
- this.version,
- this.pathname,
- this.lines,
- this.ranges
- )
- .should.equal(true)
- })
- })
- describe('when history ranges are supported', function () {
- beforeEach(async function () {
- this.DocumentManager.promises.getDoc.resolves({
- lines: this.lines,
- version: this.version,
- ranges: this.ranges,
- pathname: this.pathname,
- projectHistoryId: this.projectHistoryId,
- historyRangesSupport: true,
- type: 'sharejs-text-ot',
- })
- await this.UpdateManager.promises.applyUpdate(
- this.project_id,
- this.doc_id,
- this.update
- )
- })
- it('should push the history updates into the history queue', function () {
- this.ProjectHistoryRedisManager.promises.queueOps.should.have.been.calledWith(
- this.project_id,
- ...this.historyUpdates.map(op => JSON.stringify(op))
- )
- this.HistoryManager.recordAndFlushHistoryOps.should.have.been.calledWith(
- this.project_id,
- this.historyUpdates,
- this.historyUpdates.length
- )
- })
- })
- describe('when tracked changes are rejected', function () {
- beforeEach(async function () {
- this.rejectedChangeAuthorIds = ['author-1', 'author-2']
- // The ranges that getDoc returned must include the changes whose IDs
- // RangesManager reports as removed so UpdateManager can look up the
- // authors locally.
- this.ranges = {
- changes: [
- {
- id: 'change-1',
- metadata: { user_id: 'author-1' },
- },
- {
- id: 'change-2',
- metadata: { user_id: 'author-2' },
- },
- {
- id: 'change-untouched',
- metadata: { user_id: 'author-3' },
- },
- ],
- }
- this.DocumentManager.promises.getDoc.resolves({
- lines: this.lines,
- version: this.version,
- ranges: this.ranges,
- pathname: this.pathname,
- projectHistoryId: this.projectHistoryId,
- historyRangesSupport: false,
- type: 'sharejs-text-ot',
- })
- this.RangesManager.applyUpdate.returns({
- newRanges: this.updated_ranges,
- rangesWereCollapsed: false,
- historyUpdates: this.historyUpdates,
- removedChangeIds: ['change-1', 'change-2'],
- })
- await this.UpdateManager.promises.applyUpdate(
- this.project_id,
- this.doc_id,
- this.update
- )
- })
- it('should notify web of the rejected tracked changes', function () {
- this.WebApiManager.promises.notifyTrackChangesRejected.should.have.been.calledWith(
- this.project_id,
- this.doc_id,
- this.rejectedChangeAuthorIds,
- this.updateMeta.user_id
- )
- })
- })
- describe('when no tracked changes are rejected', function () {
- beforeEach(async function () {
- await this.UpdateManager.promises.applyUpdate(
- this.project_id,
- this.doc_id,
- this.update
- )
- })
- it('should not notify web', function () {
- this.WebApiManager.promises.notifyTrackChangesRejected.called.should.equal(
- false
- )
- })
- })
- })
- describe('_adjustHistoryUpdatesMetadata', function () {
- beforeEach(function () {
- this.lines = ['some', 'test', 'data']
- this.updatedDocLines = ['after', 'updates']
- this.historyUpdates = [
- {
- v: 42,
- op: [
- { i: 'bing', p: 12, trackedDeleteRejection: true },
- { i: 'foo', p: 4 },
- { i: 'bar', p: 6 },
- ],
- },
- {
- v: 45,
- op: [
- { d: 'qux', p: 4 },
- { i: 'bazbaz', p: 14 },
- {
- d: 'bong',
- p: 28,
- trackedChanges: [{ type: 'insert', offset: 0, length: 4 }],
- },
- ],
- meta: {
- tc: 'tracking-info',
- },
- },
- {
- v: 47,
- op: [{ d: 'so', p: 0 }],
- },
- { v: 49, op: [{ i: 'penguin', p: 18 }] },
- ]
- this.ranges = {
- changes: [
- { op: { d: 'bingbong', p: 12 } },
- { op: { i: 'test', p: 5 } },
- ],
- }
- })
- it('should add projectHistoryId, pathname and doc_length metadata to the ops', function () {
- this.UpdateManager._adjustHistoryUpdatesMetadata(
- this.historyUpdates,
- this.pathname,
- this.projectHistoryId,
- this.lines,
- this.updatedDocLines,
- this.ranges,
- false
- )
- this.historyUpdates.should.deep.equal([
- {
- projectHistoryId: this.projectHistoryId,
- v: 42,
- op: [
- { i: 'bing', p: 12, trackedDeleteRejection: true },
- { i: 'foo', p: 4 },
- { i: 'bar', p: 6 },
- ],
- meta: {
- pathname: this.pathname,
- doc_length: 14,
- },
- },
- {
- projectHistoryId: this.projectHistoryId,
- v: 45,
- op: [
- { d: 'qux', p: 4 },
- { i: 'bazbaz', p: 14 },
- {
- d: 'bong',
- p: 28,
- trackedChanges: [{ type: 'insert', offset: 0, length: 4 }],
- },
- ],
- meta: {
- pathname: this.pathname,
- doc_length: 24, // 14 + 'bing' + 'foo' + 'bar'
- },
- },
- {
- projectHistoryId: this.projectHistoryId,
- v: 47,
- op: [{ d: 'so', p: 0 }],
- meta: {
- pathname: this.pathname,
- doc_length: 23, // 24 - 'qux' + 'bazbaz' - 'bong'
- },
- },
- {
- projectHistoryId: this.projectHistoryId,
- v: 49,
- op: [{ i: 'penguin', p: 18 }],
- meta: {
- pathname: this.pathname,
- doc_length: 21, // 23 - 'so'
- },
- },
- ])
- })
- it('should add additional metadata when ranges support is enabled', function () {
- this.UpdateManager._adjustHistoryUpdatesMetadata(
- this.historyUpdates,
- this.pathname,
- this.projectHistoryId,
- this.lines,
- this.ranges,
- this.updatedDocLines,
- true
- )
- this.historyUpdates.should.deep.equal([
- {
- projectHistoryId: this.projectHistoryId,
- v: 42,
- op: [
- { i: 'bing', p: 12, trackedDeleteRejection: true },
- { i: 'foo', p: 4 },
- { i: 'bar', p: 6 },
- ],
- meta: {
- pathname: this.pathname,
- doc_length: 14,
- history_doc_length: 22,
- },
- },
- {
- projectHistoryId: this.projectHistoryId,
- v: 45,
- op: [
- { d: 'qux', p: 4 },
- { i: 'bazbaz', p: 14 },
- {
- d: 'bong',
- p: 28,
- trackedChanges: [{ type: 'insert', offset: 0, length: 4 }],
- },
- ],
- meta: {
- pathname: this.pathname,
- doc_length: 24, // 14 + 'bing' + 'foo' + 'bar'
- history_doc_length: 28, // 22 + 'foo' + 'bar'
- tc: 'tracking-info',
- },
- },
- {
- projectHistoryId: this.projectHistoryId,
- v: 47,
- op: [{ d: 'so', p: 0 }],
- meta: {
- pathname: this.pathname,
- doc_length: 23, // 24 - 'qux' + 'bazbaz' - 'bong'
- history_doc_length: 30, // 28 - 'bong' + 'bazbaz'
- },
- },
- {
- projectHistoryId: this.projectHistoryId,
- v: 49,
- op: [{ i: 'penguin', p: 18 }],
- meta: {
- pathname: this.pathname,
- doc_length: 21, // 23 - 'so'
- doc_hash: stringHash(this.updatedDocLines.join('\n')),
- history_doc_length: 28, // 30 - 'so'
- },
- },
- ])
- })
- it('should calculate the right doc length for an empty document', function () {
- this.historyUpdates = [{ v: 42, op: [{ i: 'foobar', p: 0 }] }]
- this.UpdateManager._adjustHistoryUpdatesMetadata(
- this.historyUpdates,
- this.pathname,
- this.projectHistoryId,
- [],
- {},
- ['foobar'],
- false
- )
- this.historyUpdates.should.deep.equal([
- {
- projectHistoryId: this.projectHistoryId,
- v: 42,
- op: [{ i: 'foobar', p: 0 }],
- meta: {
- pathname: this.pathname,
- doc_length: 0,
- },
- },
- ])
- })
- })
- describe('lockUpdatesAndDo', function () {
- beforeEach(function () {
- this.methodResult = 'method result'
- this.method = sinon.stub().resolves(this.methodResult)
- this.arg1 = 'argument 1'
- })
- describe('successfully', function () {
- beforeEach(async function () {
- this.UpdateManager.promises.continueProcessingUpdatesWithLock = sinon
- .stub()
- .resolves()
- this.UpdateManager.promises.processOutstandingUpdates = sinon
- .stub()
- .resolves()
- this.response = await this.UpdateManager.promises.lockUpdatesAndDo(
- this.method,
- this.project_id,
- this.doc_id,
- this.arg1
- )
- })
- it('should lock the doc', function () {
- this.LockManager.promises.getLock
- .calledWith(this.doc_id)
- .should.equal(true)
- })
- it('should process any outstanding updates', function () {
- this.UpdateManager.promises.processOutstandingUpdates.should.have.been.calledWith(
- this.project_id,
- this.doc_id
- )
- })
- it('should call the method', function () {
- this.method
- .calledWith(this.project_id, this.doc_id, this.arg1)
- .should.equal(true)
- })
- it('should return the method response arguments', function () {
- expect(this.response).to.equal(this.methodResult)
- })
- it('should release the lock', function () {
- this.LockManager.promises.releaseLock
- .calledWith(this.doc_id, this.lockValue)
- .should.equal(true)
- })
- it('should continue processing updates', function () {
- this.UpdateManager.promises.continueProcessingUpdatesWithLock
- .calledWith(this.project_id, this.doc_id)
- .should.equal(true)
- })
- })
- describe('when processOutstandingUpdates returns an error', function () {
- beforeEach(async function () {
- this.error = new Error('Something went wrong')
- this.UpdateManager.promises.processOutstandingUpdates = sinon
- .stub()
- .rejects(this.error)
- await expect(
- this.UpdateManager.promises.lockUpdatesAndDo(
- this.method,
- this.project_id,
- this.doc_id,
- this.arg1
- )
- ).to.be.rejectedWith(this.error)
- })
- it('should free the lock', function () {
- this.LockManager.promises.releaseLock
- .calledWith(this.doc_id, this.lockValue)
- .should.equal(true)
- })
- })
- describe('when the method returns an error', function () {
- beforeEach(async function () {
- this.error = new Error('something went wrong')
- this.UpdateManager.promises.processOutstandingUpdates = sinon
- .stub()
- .resolves()
- this.method = sinon.stub().rejects(this.error)
- await expect(
- this.UpdateManager.promises.lockUpdatesAndDo(
- this.method,
- this.project_id,
- this.doc_id,
- this.arg1
- )
- ).to.be.rejectedWith(this.error)
- })
- it('should free the lock', function () {
- this.LockManager.promises.releaseLock
- .calledWith(this.doc_id, this.lockValue)
- .should.equal(true)
- })
- })
- })
- })
- function stringHash(s) {
- const hash = createHash('sha1')
- hash.update(s)
- return hash.digest('hex')
- }
|