mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-11 12:56:42 -05:00
7f48c67512
* Add `unicorn/prefer-node-protocol` * Fix `unicorn/prefer-node-protocol` ESLint errors * Run `npm run format:fix` * Add sandboxed-module sourceTransformers in mocha setups Fix `no such file or directory, open 'node:fs'` in `sandboxed-module` * Remove `node:` in the SandboxedModule requires * Fix new linting errors with `node:` GitOrigin-RevId: 68f6e31e2191fcff4cb8058dd0a6914c14f59926
423 lines
13 KiB
JavaScript
423 lines
13 KiB
JavaScript
/* eslint-disable
|
|
no-return-assign,
|
|
no-unused-vars,
|
|
*/
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
// Fix any style issues and re-enable lint.
|
|
/*
|
|
* decaffeinate suggestions:
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
*/
|
|
const sinon = require('sinon')
|
|
const SandboxedModule = require('sandboxed-module')
|
|
const path = require('node:path')
|
|
const modulePath = '../../../app/js/DocumentUpdaterManager'
|
|
const _ = require('lodash')
|
|
|
|
describe('DocumentUpdaterManager', function () {
|
|
beforeEach(function () {
|
|
let Timer
|
|
this.project_id = 'project-id-923'
|
|
this.doc_id = 'doc-id-394'
|
|
this.lines = ['one', 'two', 'three']
|
|
this.version = 42
|
|
this.settings = {
|
|
apis: { documentupdater: { url: 'http://doc-updater.example.com' } },
|
|
redis: {
|
|
documentupdater: {
|
|
key_schema: {
|
|
pendingUpdates({ doc_id: docId }) {
|
|
return `PendingUpdates:${docId}`
|
|
},
|
|
},
|
|
},
|
|
},
|
|
maxUpdateSize: 7 * 1024 * 1024,
|
|
pendingUpdateListShardCount: 10,
|
|
}
|
|
this.rclient = { auth() {} }
|
|
|
|
return (this.DocumentUpdaterManager = SandboxedModule.require(modulePath, {
|
|
requires: {
|
|
'@overleaf/settings': this.settings,
|
|
request: (this.request = {}),
|
|
'@overleaf/redis-wrapper': { createClient: () => this.rclient },
|
|
'@overleaf/metrics': (this.Metrics = {
|
|
summary: sinon.stub(),
|
|
Timer: (Timer = class Timer {
|
|
done() {}
|
|
}),
|
|
}),
|
|
},
|
|
}))
|
|
}) // avoid modifying JSON object directly
|
|
|
|
describe('getDocument', function () {
|
|
beforeEach(function () {
|
|
return (this.callback = sinon.stub())
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.body = JSON.stringify({
|
|
lines: this.lines,
|
|
version: this.version,
|
|
ops: (this.ops = ['mock-op-1', 'mock-op-2']),
|
|
ranges: (this.ranges = { mock: 'ranges' }),
|
|
})
|
|
this.fromVersion = 2
|
|
this.request.get = sinon
|
|
.stub()
|
|
.callsArgWith(1, null, { statusCode: 200 }, this.body)
|
|
return this.DocumentUpdaterManager.getDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.fromVersion,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should get the document from the document updater', function () {
|
|
const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`
|
|
return this.request.get.calledWith(url).should.equal(true)
|
|
})
|
|
|
|
return it('should call the callback with the lines, version, ranges and ops', function () {
|
|
return this.callback
|
|
.calledWith(null, this.lines, this.version, this.ranges, this.ops)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.get = sinon
|
|
.stub()
|
|
.callsArgWith(
|
|
1,
|
|
(this.error = new Error('something went wrong')),
|
|
null,
|
|
null
|
|
)
|
|
return this.DocumentUpdaterManager.getDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.fromVersion,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
return it('should return an error to the callback', function () {
|
|
return this.callback.calledWith(this.error).should.equal(true)
|
|
})
|
|
})
|
|
;[404, 422].forEach(statusCode =>
|
|
describe(`when the document updater returns a ${statusCode} status code`, function () {
|
|
beforeEach(function () {
|
|
this.request.get = sinon
|
|
.stub()
|
|
.callsArgWith(1, null, { statusCode }, '')
|
|
return this.DocumentUpdaterManager.getDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.fromVersion,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
return it('should return the callback with an error', function () {
|
|
this.callback.called.should.equal(true)
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match({
|
|
message: 'doc updater could not load requested ops',
|
|
info: { statusCode },
|
|
})
|
|
)
|
|
.should.equal(true)
|
|
this.logger.error.called.should.equal(false)
|
|
this.logger.warn.called.should.equal(false)
|
|
})
|
|
})
|
|
)
|
|
|
|
return describe('when the document updater returns a failure error code', function () {
|
|
beforeEach(function () {
|
|
this.request.get = sinon
|
|
.stub()
|
|
.callsArgWith(1, null, { statusCode: 500 }, '')
|
|
return this.DocumentUpdaterManager.getDocument(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.fromVersion,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
return it('should return the callback with an error', function () {
|
|
this.callback.called.should.equal(true)
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match({
|
|
message: 'doc updater returned a non-success status code',
|
|
info: {
|
|
action: 'getDocument',
|
|
statusCode: 500,
|
|
},
|
|
})
|
|
)
|
|
.should.equal(true)
|
|
this.logger.error.called.should.equal(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('flushProjectToMongoAndDelete', function () {
|
|
beforeEach(function () {
|
|
return (this.callback = sinon.stub())
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.request.del = sinon
|
|
.stub()
|
|
.callsArgWith(1, null, { statusCode: 204 }, '')
|
|
return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(
|
|
this.project_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should delete the project from the document updater', function () {
|
|
const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}?background=true`
|
|
return this.request.del.calledWith(url).should.equal(true)
|
|
})
|
|
|
|
return it('should call the callback with no error', function () {
|
|
return this.callback.calledWith(null).should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('when the document updater API returns an error', function () {
|
|
beforeEach(function () {
|
|
this.request.del = sinon
|
|
.stub()
|
|
.callsArgWith(
|
|
1,
|
|
(this.error = new Error('something went wrong')),
|
|
null,
|
|
null
|
|
)
|
|
return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(
|
|
this.project_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
return it('should return an error to the callback', function () {
|
|
return this.callback.calledWith(this.error).should.equal(true)
|
|
})
|
|
})
|
|
|
|
return describe('when the document updater returns a failure error code', function () {
|
|
beforeEach(function () {
|
|
this.request.del = sinon
|
|
.stub()
|
|
.callsArgWith(1, null, { statusCode: 500 }, '')
|
|
return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(
|
|
this.project_id,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
return it('should return the callback with an error', function () {
|
|
this.callback.called.should.equal(true)
|
|
this.callback
|
|
.calledWith(
|
|
sinon.match({
|
|
message: 'doc updater returned a non-success status code',
|
|
info: {
|
|
action: 'flushProjectToMongoAndDelete',
|
|
statusCode: 500,
|
|
},
|
|
})
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('queueChange', function () {
|
|
beforeEach(function () {
|
|
this.change = {
|
|
doc: '1234567890',
|
|
op: [{ d: 'test', p: 345 }],
|
|
v: 789,
|
|
}
|
|
this.rclient.rpush = sinon.stub().yields()
|
|
return (this.callback = sinon.stub())
|
|
})
|
|
|
|
describe('successfully', function () {
|
|
beforeEach(function () {
|
|
this.pendingUpdateListKey = `pending-updates-list-key-${Math.random()}`
|
|
|
|
this.DocumentUpdaterManager._getPendingUpdateListKey = sinon
|
|
.stub()
|
|
.returns(this.pendingUpdateListKey)
|
|
this.DocumentUpdaterManager.queueChange(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.change,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should push the change', function () {
|
|
this.rclient.rpush
|
|
.calledWith(
|
|
`PendingUpdates:${this.doc_id}`,
|
|
JSON.stringify(this.change)
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should notify the doc updater of the change via the pending-updates-list queue', function () {
|
|
this.rclient.rpush
|
|
.calledWith(
|
|
this.pendingUpdateListKey,
|
|
`${this.project_id}:${this.doc_id}`
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with error talking to redis during rpush', function () {
|
|
beforeEach(function () {
|
|
this.rclient.rpush = sinon
|
|
.stub()
|
|
.yields(new Error('something went wrong'))
|
|
return this.DocumentUpdaterManager.queueChange(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.change,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
return it('should return an error', function () {
|
|
return this.callback
|
|
.calledWithExactly(sinon.match(Error))
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
|
|
describe('with null byte corruption', function () {
|
|
beforeEach(function () {
|
|
this.stringifyStub = sinon
|
|
.stub(JSON, 'stringify')
|
|
.callsFake(() => '["bad bytes! \u0000 <- here"]')
|
|
return this.DocumentUpdaterManager.queueChange(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.change,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
afterEach(function () {
|
|
this.stringifyStub.restore()
|
|
})
|
|
|
|
it('should return an error', function () {
|
|
return this.callback
|
|
.calledWithExactly(sinon.match(Error))
|
|
.should.equal(true)
|
|
})
|
|
|
|
return it('should not push the change onto the pending-updates-list queue', function () {
|
|
return this.rclient.rpush.called.should.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('when the update is too large', function () {
|
|
beforeEach(function () {
|
|
this.change = {
|
|
op: { p: 12, t: 'update is too large'.repeat(1024 * 400) },
|
|
}
|
|
return this.DocumentUpdaterManager.queueChange(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.change,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should return an error', function () {
|
|
return this.callback
|
|
.calledWithExactly(sinon.match(Error))
|
|
.should.equal(true)
|
|
})
|
|
|
|
it('should add the size to the error', function () {
|
|
return this.callback.args[0][0].info.updateSize.should.equal(7782422)
|
|
})
|
|
|
|
return it('should not push the change onto the pending-updates-list queue', function () {
|
|
return this.rclient.rpush.called.should.equal(false)
|
|
})
|
|
})
|
|
|
|
describe('with invalid keys', function () {
|
|
beforeEach(function () {
|
|
this.change = {
|
|
op: [{ d: 'test', p: 345 }],
|
|
version: 789, // not a valid key
|
|
}
|
|
return this.DocumentUpdaterManager.queueChange(
|
|
this.project_id,
|
|
this.doc_id,
|
|
this.change,
|
|
this.callback
|
|
)
|
|
})
|
|
|
|
it('should remove the invalid keys from the change', function () {
|
|
return this.rclient.rpush
|
|
.calledWith(
|
|
`PendingUpdates:${this.doc_id}`,
|
|
JSON.stringify({ op: this.change.op })
|
|
)
|
|
.should.equal(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('_getPendingUpdateListKey', function () {
|
|
beforeEach(function () {
|
|
const keys = _.times(
|
|
10000,
|
|
this.DocumentUpdaterManager._getPendingUpdateListKey
|
|
)
|
|
this.keys = _.uniq(keys)
|
|
})
|
|
it('should return normal pending updates key', function () {
|
|
_.includes(this.keys, 'pending-updates-list').should.equal(true)
|
|
})
|
|
|
|
it('should return pending-updates-list-n keys', function () {
|
|
_.includes(this.keys, 'pending-updates-list-1').should.equal(true)
|
|
_.includes(this.keys, 'pending-updates-list-3').should.equal(true)
|
|
_.includes(this.keys, 'pending-updates-list-9').should.equal(true)
|
|
})
|
|
|
|
it('should not include pending-updates-list-0 key', function () {
|
|
_.includes(this.keys, 'pending-updates-list-0').should.equal(false)
|
|
})
|
|
|
|
it('should not include maximum as pendingUpdateListShardCount value', function () {
|
|
_.includes(this.keys, 'pending-updates-list-10').should.equal(false)
|
|
})
|
|
})
|
|
})
|