| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004 |
- const sinon = require('sinon')
- const { expect } = require('chai')
- const { setTimeout } = require('node:timers/promises')
- const Settings = require('@overleaf/settings')
- const docUpdaterRedis = require('@overleaf/redis-wrapper').createClient(
- Settings.redis.documentupdater
- )
- const Keys = Settings.redis.documentupdater.key_schema
- const MockProjectHistoryApi = require('./helpers/MockProjectHistoryApi')
- const MockWebApi = require('./helpers/MockWebApi')
- const DocUpdaterClient = require('./helpers/DocUpdaterClient')
- const DocUpdaterApp = require('./helpers/DocUpdaterApp')
- const { RequestFailedError } = require('@overleaf/fetch-utils')
- describe('Setting a document', function () {
- let numberOfReceivedUpdates = 0
- before(async function () {
- DocUpdaterClient.subscribeToAppliedOps(() => {
- numberOfReceivedUpdates++
- })
- this.lines = ['one', 'two', 'three']
- this.version = 42
- this.update = {
- doc: this.doc_id,
- op: [
- {
- i: 'one and a half\n',
- p: 4,
- },
- ],
- v: this.version,
- }
- this.result = ['one', 'one and a half', 'two', 'three']
- this.newLines = ['these', 'are', 'the', 'new', 'lines']
- this.source = 'dropbox'
- this.user_id = 'user-id-123'
- sinon.spy(MockProjectHistoryApi, 'flushProject')
- sinon.spy(MockWebApi, 'setDocument')
- await DocUpdaterApp.ensureRunning()
- })
- after(function () {
- MockProjectHistoryApi.flushProject.restore()
- MockWebApi.setDocument.restore()
- })
- describe('when the updated doc exists in the doc updater', function () {
- before(async function () {
- numberOfReceivedUpdates = 0
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- await DocUpdaterClient.sendUpdate(
- this.project_id,
- this.doc_id,
- this.update
- )
- await setTimeout(200)
- this.body = await DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- })
- after(function () {
- MockProjectHistoryApi.flushProject.resetHistory()
- MockWebApi.setDocument.resetHistory()
- })
- it('should emit two updates (from sendUpdate and setDocLines)', function () {
- expect(numberOfReceivedUpdates).to.equal(2)
- })
- it('should send the updated doc lines and version to the web api', function () {
- MockWebApi.setDocument
- .calledWith(this.project_id, this.doc_id, this.newLines)
- .should.equal(true)
- })
- it('should update the lines in the doc updater', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.newLines)
- })
- it('should bump the version in the doc updater', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.version.should.equal(this.version + 2)
- })
- it('should leave the document in redis', function (done) {
- docUpdaterRedis.get(
- Keys.docLines({ doc_id: this.doc_id }),
- (error, lines) => {
- if (error) {
- throw error
- }
- expect(JSON.parse(lines)).to.deep.equal(this.newLines)
- done()
- }
- )
- })
- it('should return the mongo rev in the json response', function () {
- this.body.should.deep.equal({ rev: '123' })
- })
- describe('when doc has the same contents', function () {
- beforeEach(async function () {
- numberOfReceivedUpdates = 0
- await DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- })
- it('should not bump the version in doc updater', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.version.should.equal(this.version + 2)
- })
- it('should not emit any updates', async function () {
- // delay by 100ms: make sure we do not check too early!
- await setTimeout(100)
- expect(numberOfReceivedUpdates).to.equal(0)
- })
- })
- })
- describe('when the updated doc exists in the doc updater (history-ot)', function () {
- before(async function () {
- numberOfReceivedUpdates = 0
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- this.historyOTUpdate = {
- doc: this.doc_id,
- op: [{ textOperation: [4, 'one and a half\n', 9] }],
- v: this.version,
- meta: { source: 'random-publicId' },
- }
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- otMigrationStage: 1,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- await DocUpdaterClient.sendUpdate(
- this.project_id,
- this.doc_id,
- this.historyOTUpdate
- )
- await setTimeout(200)
- this.body = await DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- })
- after(function () {
- MockProjectHistoryApi.flushProject.resetHistory()
- MockWebApi.setDocument.resetHistory()
- })
- it('should emit two updates (from sendUpdate and setDocLines)', function () {
- expect(numberOfReceivedUpdates).to.equal(2)
- })
- it('should send the updated doc lines and version to the web api', function () {
- MockWebApi.setDocument
- .calledWith(this.project_id, this.doc_id, this.newLines)
- .should.equal(true)
- })
- it('should update the lines in the doc updater', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.newLines)
- })
- it('should bump the version in the doc updater', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.version.should.equal(this.version + 2)
- })
- it('should leave the document in redis', function (done) {
- docUpdaterRedis.get(
- Keys.docLines({ doc_id: this.doc_id }),
- (error, lines) => {
- if (error) {
- throw error
- }
- expect(JSON.parse(lines)).to.deep.equal({
- content: this.newLines.join('\n'),
- })
- done()
- }
- )
- })
- it('should return the mongo rev in the json response', function () {
- this.body.should.deep.equal({ rev: '123' })
- })
- describe('when doc has the same contents', function () {
- beforeEach(async function () {
- numberOfReceivedUpdates = 0
- this.body = await DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- })
- it('should not bump the version in doc updater', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.version.should.equal(this.version + 2)
- })
- it('should not emit any updates', async function () {
- // delay by 100ms: make sure we do not check too early!
- await setTimeout(100)
- expect(numberOfReceivedUpdates).to.equal(0)
- })
- })
- })
- describe('when the updated doc does not exist in the doc updater', function () {
- before(async function () {
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- numberOfReceivedUpdates = 0
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- })
- this.body = await DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- await setTimeout(200)
- })
- after(function () {
- MockProjectHistoryApi.flushProject.resetHistory()
- MockWebApi.setDocument.resetHistory()
- })
- it('should emit an update', function () {
- expect(numberOfReceivedUpdates).to.equal(1)
- })
- it('should send the updated doc lines to the web api', function () {
- MockWebApi.setDocument
- .calledWith(this.project_id, this.doc_id, this.newLines)
- .should.equal(true)
- })
- it('should flush project history', function () {
- MockProjectHistoryApi.flushProject
- .calledWith(this.project_id)
- .should.equal(true)
- })
- it('should remove the document from redis', function (done) {
- docUpdaterRedis.get(
- Keys.docLines({ doc_id: this.doc_id }),
- (error, lines) => {
- if (error) {
- throw error
- }
- expect(lines).to.not.exist
- done()
- }
- )
- })
- it('should return the mongo rev in the json response', function () {
- this.body.should.deep.equal({ rev: '123' })
- })
- })
- const DOC_TOO_LARGE_TEST_CASES = [
- {
- desc: 'when the updated doc is too large for the body parser',
- size: Settings.maxJsonRequestSize,
- expectedStatusCode: 413,
- },
- {
- desc: 'when the updated doc is larger than the HTTP controller limit',
- size: Settings.max_doc_length,
- expectedStatusCode: 406,
- },
- ]
- DOC_TOO_LARGE_TEST_CASES.forEach(testCase => {
- describe(testCase.desc, function () {
- before(async function () {
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- })
- this.newLines = []
- while (JSON.stringify(this.newLines).length <= testCase.size) {
- this.newLines.push('(a long line of text)'.repeat(10000))
- }
- try {
- await DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- this.statusCode = 200
- } catch (err) {
- if (err instanceof RequestFailedError) {
- this.statusCode = err.response.status
- } else {
- throw err
- }
- }
- await setTimeout(200)
- })
- after(function () {
- MockProjectHistoryApi.flushProject.resetHistory()
- MockWebApi.setDocument.resetHistory()
- })
- it(`should return a ${testCase.expectedStatusCode} status code`, function () {
- this.statusCode.should.equal(testCase.expectedStatusCode)
- })
- it('should not send the updated doc lines to the web api', function () {
- MockWebApi.setDocument.called.should.equal(false)
- })
- it('should not flush project history', function () {
- MockProjectHistoryApi.flushProject.called.should.equal(false)
- })
- })
- })
- describe('when the updated doc is large but under the bodyParser and HTTPController size limit', function () {
- before(async function () {
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- })
- this.newLines = []
- while (JSON.stringify(this.newLines).length < 2 * 1024 * 1024) {
- // limit in HTTPController
- this.newLines.push('(a long line of text)'.repeat(10000))
- }
- this.newLines.pop() // remove the line which took it over the limit
- this.body = await DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- await setTimeout(200)
- })
- after(function () {
- MockProjectHistoryApi.flushProject.resetHistory()
- MockWebApi.setDocument.resetHistory()
- })
- it('should send the updated doc lines to the web api', function () {
- MockWebApi.setDocument
- .calledWith(this.project_id, this.doc_id, this.newLines)
- .should.equal(true)
- })
- it('should return the mongo rev in the json response', function () {
- this.body.should.deep.equal({ rev: '123' })
- })
- })
- describe('with track changes', function () {
- before(function () {
- this.lines = ['one', 'one and a half', 'two', 'three']
- this.id_seed = '587357bd35e64f6157'
- this.update = {
- doc: this.doc_id,
- op: [
- {
- d: 'one and a half\n',
- p: 4,
- },
- ],
- meta: {
- tc: this.id_seed,
- user_id: this.user_id,
- },
- v: this.version,
- }
- })
- describe('with the undo flag', function () {
- before(async function () {
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- await DocUpdaterClient.sendUpdate(
- this.project_id,
- this.doc_id,
- this.update
- )
- // Go back to old lines, with undo flag
- await DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.lines,
- this.source,
- this.user_id,
- true
- )
- await setTimeout(200)
- })
- after(function () {
- MockProjectHistoryApi.flushProject.resetHistory()
- MockWebApi.setDocument.resetHistory()
- })
- it('should undo the tracked changes', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- expect(doc.ranges.changes).to.be.undefined
- })
- })
- describe('without the undo flag', function () {
- before(async function () {
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- await DocUpdaterClient.sendUpdate(
- this.project_id,
- this.doc_id,
- this.update
- )
- // Go back to old lines, without undo flag
- await DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.lines,
- this.source,
- this.user_id,
- false
- )
- await setTimeout(200)
- })
- after(function () {
- MockProjectHistoryApi.flushProject.resetHistory()
- MockWebApi.setDocument.resetHistory()
- })
- it('should not undo the tracked changes', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- expect(doc.ranges.changes.length).to.equal(1)
- })
- })
- })
- describe('with track changes (history-ot)', function () {
- const lines = ['one', 'one and a half', 'two', 'three']
- const userId = DocUpdaterClient.randomId()
- const ts = new Date().toISOString()
- beforeEach(async function () {
- numberOfReceivedUpdates = 0
- this.newLines = ['one', 'two', 'three']
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- this.historyOTUpdate = {
- doc: this.doc_id,
- op: [
- {
- textOperation: [
- 4,
- {
- r: 'one and a half\n'.length,
- tracking: {
- type: 'delete',
- userId,
- ts,
- },
- },
- 9,
- ],
- },
- ],
- v: this.version,
- meta: { source: 'random-publicId' },
- }
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines,
- version: this.version,
- otMigrationStage: 1,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- await DocUpdaterClient.sendUpdate(
- this.project_id,
- this.doc_id,
- this.historyOTUpdate
- )
- await DocUpdaterClient.waitForPendingUpdates(this.doc_id)
- })
- afterEach(function () {
- MockProjectHistoryApi.flushProject.resetHistory()
- MockWebApi.setDocument.resetHistory()
- })
- it('should record tracked changes', function (done) {
- docUpdaterRedis.get(
- Keys.docLines({ doc_id: this.doc_id }),
- (error, data) => {
- if (error) {
- throw error
- }
- expect(JSON.parse(data)).to.deep.equal({
- content: lines.join('\n'),
- trackedChanges: [
- {
- range: {
- pos: 4,
- length: 15,
- },
- tracking: {
- ts,
- type: 'delete',
- userId,
- },
- },
- ],
- })
- done()
- }
- )
- })
- it('should apply the change', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- expect(doc.lines).to.deep.equal(this.newLines)
- })
- const cases = [
- {
- name: 'when resetting the content',
- lines,
- want: {
- content: 'one\none and a half\none and a half\ntwo\nthree',
- trackedChanges: [
- {
- range: {
- pos: 'one and a half\n'.length + 4,
- length: 15,
- },
- tracking: {
- ts,
- type: 'delete',
- userId,
- },
- },
- ],
- },
- },
- {
- name: 'when adding content before a tracked delete',
- lines: ['one', 'INSERT', 'two', 'three'],
- want: {
- content: 'one\nINSERT\none and a half\ntwo\nthree',
- trackedChanges: [
- {
- range: {
- pos: 'INSERT\n'.length + 4,
- length: 15,
- },
- tracking: {
- ts,
- type: 'delete',
- userId,
- },
- },
- ],
- },
- },
- {
- name: 'when adding content after a tracked delete',
- lines: ['one', 'two', 'INSERT', 'three'],
- want: {
- content: 'one\none and a half\ntwo\nINSERT\nthree',
- trackedChanges: [
- {
- range: {
- pos: 4,
- length: 15,
- },
- tracking: {
- ts,
- type: 'delete',
- userId,
- },
- },
- ],
- },
- },
- {
- name: 'when deleting content before a tracked delete',
- lines: ['two', 'three'],
- want: {
- content: 'one and a half\ntwo\nthree',
- trackedChanges: [
- {
- range: {
- pos: 0,
- length: 15,
- },
- tracking: {
- ts,
- type: 'delete',
- userId,
- },
- },
- ],
- },
- },
- {
- name: 'when deleting content after a tracked delete',
- lines: ['one', 'two'],
- want: {
- content: 'one\none and a half\ntwo',
- trackedChanges: [
- {
- range: {
- pos: 4,
- length: 15,
- },
- tracking: {
- ts,
- type: 'delete',
- userId,
- },
- },
- ],
- },
- },
- {
- name: 'when deleting content immediately after a tracked delete',
- lines: ['one', 'three'],
- want: {
- content: 'one\none and a half\nthree',
- trackedChanges: [
- {
- range: {
- pos: 4,
- length: 15,
- },
- tracking: {
- ts,
- type: 'delete',
- userId,
- },
- },
- ],
- },
- },
- {
- name: 'when deleting content across a tracked delete',
- lines: ['onethree'],
- want: {
- content: 'oneone and a half\nthree',
- trackedChanges: [
- {
- range: {
- pos: 3,
- length: 15,
- },
- tracking: {
- ts,
- type: 'delete',
- userId,
- },
- },
- ],
- },
- },
- ]
- for (const { name, lines, want } of cases) {
- describe(name, function () {
- beforeEach(async function () {
- this.body = await DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- lines,
- this.source,
- userId,
- false
- )
- })
- it('should update accordingly', function (done) {
- docUpdaterRedis.get(
- Keys.docLines({ doc_id: this.doc_id }),
- (error, data) => {
- if (error) {
- throw error
- }
- expect(JSON.parse(data)).to.deep.equal(want)
- done()
- }
- )
- })
- })
- }
- })
- describe('when the first request returns a connection error', function () {
- beforeEach(function () {
- const origSetDocumentController =
- MockWebApi.setDocumentController.bind(MockWebApi)
- const setDocumentStub = sinon
- .stub(MockWebApi, 'setDocumentController')
- .onCall(0)
- .callsFake((req, res, next) => {
- res.destroy() // simulate a network error
- })
- setDocumentStub.onCall(1).callsFake(origSetDocumentController)
- })
- afterEach(function () {
- MockWebApi.setDocumentController.restore()
- })
- it('should retry on connection error and set the document', async function () {
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- projectHistoryId: this.project_id,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- await expect(
- DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- ).to.eventually.deep.include({ rev: '123' })
- expect(MockWebApi.setDocumentController).to.be.calledTwice
- })
- })
- describe('when the document does not exist', function () {
- before(function () {
- sinon.spy(MockWebApi, 'setDocumentController')
- })
- after(function () {
- MockWebApi.setDocumentController.restore()
- })
- it('should return 404', async function () {
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- projectHistoryId: this.project_id,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- MockWebApi.clearDocs()
- await expect(
- DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- )
- .to.be.rejectedWith(RequestFailedError)
- .and.eventually.have.nested.property('response.status', 404)
- expect(MockWebApi.setDocumentController).to.be.calledOnce
- })
- })
- describe('when the document is too large', function () {
- beforeEach(function () {
- sinon
- .stub(MockWebApi, 'setDocumentController')
- .callsFake((req, res, next) => {
- res.sendStatus(413) // simulate a large file error
- })
- })
- afterEach(function () {
- MockWebApi.setDocumentController.restore()
- })
- it('should return 413', async function () {
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- projectHistoryId: this.project_id,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- MockWebApi.clearDocs()
- await expect(
- DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- )
- .to.be.rejectedWith(RequestFailedError)
- .and.eventually.have.nested.property('response.status', 413)
- expect(MockWebApi.setDocumentController).to.be.calledOnce
- })
- })
- describe('when the first request returns a 500 error', function () {
- beforeEach(function () {
- const origSetDocumentController =
- MockWebApi.setDocumentController.bind(MockWebApi)
- const setDocumentStub = sinon
- .stub(MockWebApi, 'setDocumentController')
- .onCall(0)
- .callsFake((req, res, next) => {
- res.sendStatus(500)
- })
- setDocumentStub.onCall(1).callsFake(origSetDocumentController)
- })
- afterEach(function () {
- MockWebApi.setDocumentController.restore()
- })
- it('should retry on a 500 error and set the document', async function () {
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- projectHistoryId: this.project_id,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- await expect(
- DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- ).to.eventually.deep.include({ rev: '123' })
- expect(MockWebApi.setDocumentController).to.be.calledTwice
- })
- })
- describe('when the web api http request times out on the first request', function () {
- beforeEach(async function () {
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- projectHistoryId: this.project_id,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- const origSetDocumentController =
- MockWebApi.setDocumentController.bind(MockWebApi)
- const setDocumentStub = sinon
- .stub(MockWebApi, 'setDocumentController')
- .onFirstCall()
- .callsFake(async (req, res, next) => {
- await setTimeout(30_000)
- })
- setDocumentStub.onCall(1).callsFake(origSetDocumentController)
- })
- afterEach(function () {
- MockWebApi.setDocumentController.restore()
- })
- it('should retry the request and return the document', async function () {
- this.timeout(10000)
- const returnedDoc = await DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- expect(returnedDoc).to.deep.include({ rev: '123' })
- expect(MockWebApi.setDocumentController).to.be.calledTwice
- })
- })
- describe('when the web api http request times out repeatedly', function () {
- beforeEach(async function () {
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- projectHistoryId: this.project_id,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- sinon
- .stub(MockWebApi, 'setDocumentController')
- .callsFake(async (req, res, next) => {
- await setTimeout(30_000)
- })
- })
- afterEach(function () {
- MockWebApi.setDocumentController.restore()
- })
- it('should return an error after two attempts', async function () {
- this.timeout(15000)
- const start = Date.now()
- await expect(
- DocUpdaterClient.setDocLines(
- this.project_id,
- this.doc_id,
- this.newLines,
- this.source,
- this.user_id,
- false
- )
- ).to.be.rejectedWith('request failed')
- const delta = Date.now() - start
- expect(delta).to.be.above(10_000) // 2 * 5000ms timeout
- expect(delta).to.be.below(20_000)
- expect(MockWebApi.setDocumentController).to.be.calledTwice
- })
- })
- })
|