| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927 |
- const sinon = require('sinon')
- const { expect } = require('chai')
- const { setTimeout } = require('node:timers/promises')
- const Settings = require('@overleaf/settings')
- const rclientProjectHistory = require('@overleaf/redis-wrapper').createClient(
- Settings.redis.project_history
- )
- const rclientDU = require('@overleaf/redis-wrapper').createClient(
- Settings.redis.documentupdater
- )
- const Keys = Settings.redis.documentupdater.key_schema
- const ProjectHistoryKeys = Settings.redis.project_history.key_schema
- const MockWebApi = require('./helpers/MockWebApi')
- const DocUpdaterClient = require('./helpers/DocUpdaterClient')
- const DocUpdaterApp = require('./helpers/DocUpdaterApp')
- const { RequestFailedError } = require('@overleaf/fetch-utils')
- async function sendUpdateAndWait(projectId, docId, update) {
- await DocUpdaterClient.sendUpdate(projectId, docId, update)
- // It seems that we need to wait for a little while
- await setTimeout(200)
- }
- describe('Applying updates to a doc', function () {
- beforeEach(async function () {
- sinon.spy(MockWebApi, 'getDocument')
- this.lines = ['one', 'two', 'three']
- this.version = 42
- this.op = {
- i: 'one and a half\n',
- p: 4,
- }
- this.project_id = DocUpdaterClient.randomId()
- this.doc_id = DocUpdaterClient.randomId()
- this.update = {
- doc: this.doc_id,
- op: [this.op],
- v: this.version,
- }
- this.historyOTUpdate = {
- doc: this.doc_id,
- op: [{ textOperation: [4, 'one and a half\n', 9] }],
- v: this.version,
- meta: { source: 'random-publicId' },
- }
- this.result = ['one', 'one and a half', 'two', 'three']
- await DocUpdaterApp.ensureRunning()
- })
- afterEach(function () {
- sinon.restore()
- })
- describe('when the document is not loaded', function () {
- beforeEach(async function () {
- this.startTime = Date.now()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- })
- await sendUpdateAndWait(this.project_id, this.doc_id, this.update)
- const result = await rclientProjectHistory.get(
- ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
- project_id: this.project_id,
- })
- )
- this.firstOpTimestamp = parseInt(result, 10)
- })
- it('should load the document from the web API', function () {
- MockWebApi.getDocument
- .calledWith(this.project_id, this.doc_id)
- .should.equal(true)
- })
- it('should update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.result)
- })
- it('should push the applied updates to the project history changes api', function (done) {
- rclientProjectHistory.lrange(
- ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
- 0,
- -1,
- (error, updates) => {
- if (error != null) {
- throw error
- }
- JSON.parse(updates[0]).op.should.deep.equal([this.op])
- done()
- }
- )
- })
- it('should set the first op timestamp', function () {
- this.firstOpTimestamp.should.be.within(this.startTime, Date.now())
- })
- it('should yield last updated time', async function () {
- const { lastUpdatedAt } = await DocUpdaterClient.getProjectLastUpdatedAt(
- this.project_id
- )
- lastUpdatedAt.should.be.within(this.startTime, Date.now())
- })
- it('should yield no last updated time for another project', async function () {
- const body = await DocUpdaterClient.getProjectLastUpdatedAt(
- DocUpdaterClient.randomId()
- )
- body.should.deep.equal({})
- })
- it('should set the project notification timestamp in Redis', async function () {
- const timestamp = await rclientDU.get(
- Keys.projectNotificationTimestamp({ project_id: this.project_id })
- )
- expect(timestamp).to.exist
- const timestampValue = parseInt(timestamp, 10)
- timestampValue.should.be.within(this.startTime, Date.now())
- })
- describe('when sending another update', function () {
- beforeEach(async function () {
- this.timeout(10000)
- this.second_update = Object.assign({}, this.update)
- this.second_update.v = this.version + 1
- this.secondStartTime = Date.now()
- await sendUpdateAndWait(
- this.project_id,
- this.doc_id,
- this.second_update
- )
- })
- it('should update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal([
- 'one',
- 'one and a half',
- 'one and a half',
- 'two',
- 'three',
- ])
- })
- it('should not change the first op timestamp', function (done) {
- rclientProjectHistory.get(
- ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
- project_id: this.project_id,
- }),
- (error, result) => {
- if (error != null) {
- throw error
- }
- result = parseInt(result, 10)
- result.should.equal(this.firstOpTimestamp)
- done()
- }
- )
- })
- it('should yield last updated time', async function () {
- const { lastUpdatedAt } =
- await DocUpdaterClient.getProjectLastUpdatedAt(this.project_id)
- lastUpdatedAt.should.be.within(this.secondStartTime, Date.now())
- })
- it('should not change the project notification timestamp', async function () {
- const timestamp = await rclientDU.get(
- Keys.projectNotificationTimestamp({ project_id: this.project_id })
- )
- expect(timestamp).to.exist
- const timestampValue = parseInt(timestamp, 10)
- // Should still be within the first update time range, not the second
- timestampValue.should.be.within(this.startTime, this.secondStartTime)
- })
- })
- describe('when another client is sending a concurrent update', function () {
- beforeEach(async function () {
- this.timeout(10000)
- this.otherUpdate = {
- doc: this.doc_id,
- op: [{ p: 8, i: 'two and a half\n' }],
- v: this.version,
- meta: { source: 'other-random-publicId' },
- }
- this.secondStartTime = Date.now()
- await sendUpdateAndWait(this.project_id, this.doc_id, this.otherUpdate)
- })
- it('should update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal([
- 'one',
- 'one and a half',
- 'two',
- 'two and a half',
- 'three',
- ])
- })
- it('should not change the first op timestamp', function (done) {
- rclientProjectHistory.get(
- ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
- project_id: this.project_id,
- }),
- (error, result) => {
- if (error != null) {
- throw error
- }
- result = parseInt(result, 10)
- result.should.equal(this.firstOpTimestamp)
- done()
- }
- )
- })
- it('should yield last updated time', async function () {
- const { lastUpdatedAt } =
- await DocUpdaterClient.getProjectLastUpdatedAt(this.project_id)
- lastUpdatedAt.should.be.within(this.secondStartTime, Date.now())
- })
- })
- })
- describe('when the document is not loaded (history-ot)', function () {
- beforeEach(async function () {
- this.startTime = Date.now()
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- otMigrationStage: 1,
- })
- await sendUpdateAndWait(
- this.project_id,
- this.doc_id,
- this.historyOTUpdate
- )
- const result = await rclientProjectHistory.get(
- ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
- project_id: this.project_id,
- })
- )
- this.firstOpTimestamp = parseInt(result, 10)
- })
- it('should load the document from the web API', function () {
- MockWebApi.getDocument
- .calledWith(this.project_id, this.doc_id)
- .should.equal(true)
- })
- it('should update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.result)
- })
- it('should push the applied updates to the project history changes api', function (done) {
- rclientProjectHistory.lrange(
- ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
- 0,
- -1,
- (error, updates) => {
- if (error != null) {
- throw error
- }
- JSON.parse(updates[0]).op.should.deep.equal(this.historyOTUpdate.op)
- JSON.parse(updates[0]).meta.pathname.should.equal('/a/b/c.tex')
- done()
- }
- )
- })
- it('should set the first op timestamp', function () {
- this.firstOpTimestamp.should.be.within(this.startTime, Date.now())
- })
- it('should yield last updated time', async function () {
- const { lastUpdatedAt } = await DocUpdaterClient.getProjectLastUpdatedAt(
- this.project_id
- )
- lastUpdatedAt.should.be.within(this.startTime, Date.now())
- })
- it('should yield no last updated time for another project', async function () {
- const body = await DocUpdaterClient.getProjectLastUpdatedAt(
- DocUpdaterClient.randomId()
- )
- body.should.deep.equal({})
- })
- it('should set the project notification timestamp in Redis', async function () {
- const timestamp = await rclientDU.get(
- Keys.projectNotificationTimestamp({ project_id: this.project_id })
- )
- expect(timestamp).to.exist
- const timestampValue = parseInt(timestamp, 10)
- timestampValue.should.be.within(this.startTime, Date.now())
- })
- describe('when sending another update', function () {
- beforeEach(async function () {
- this.timeout(10000)
- this.second_update = Object.assign({}, this.historyOTUpdate)
- this.second_update.op = [
- {
- textOperation: [4, 'one and a half\n', 24],
- },
- ]
- this.second_update.v = this.version + 1
- this.secondStartTime = Date.now()
- await sendUpdateAndWait(
- this.project_id,
- this.doc_id,
- this.second_update
- )
- })
- it('should update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal([
- 'one',
- 'one and a half',
- 'one and a half',
- 'two',
- 'three',
- ])
- })
- it('should not change the first op timestamp', function (done) {
- rclientProjectHistory.get(
- ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
- project_id: this.project_id,
- }),
- (error, result) => {
- if (error != null) {
- throw error
- }
- result = parseInt(result, 10)
- result.should.equal(this.firstOpTimestamp)
- done()
- }
- )
- })
- it('should yield last updated time', async function () {
- const { lastUpdatedAt } =
- await DocUpdaterClient.getProjectLastUpdatedAt(this.project_id)
- lastUpdatedAt.should.be.within(this.secondStartTime, Date.now())
- })
- it('should not change the project notification timestamp', async function () {
- const timestamp = await rclientDU.get(
- Keys.projectNotificationTimestamp({ project_id: this.project_id })
- )
- expect(timestamp).to.exist
- const timestampValue = parseInt(timestamp, 10)
- // Should still be within the first update time range, not the second
- timestampValue.should.be.within(this.startTime, this.secondStartTime)
- })
- })
- describe('when another client is sending a concurrent update', function () {
- beforeEach(async function () {
- this.timeout(10000)
- this.otherUpdate = {
- doc: this.doc_id,
- op: [{ textOperation: [8, 'two and a half\n', 5] }],
- v: this.version,
- meta: { source: 'other-random-publicId' },
- }
- this.secondStartTime = Date.now()
- await sendUpdateAndWait(this.project_id, this.doc_id, this.otherUpdate)
- })
- it('should update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal([
- 'one',
- 'one and a half',
- 'two',
- 'two and a half',
- 'three',
- ])
- })
- it('should not change the first op timestamp', function (done) {
- rclientProjectHistory.get(
- ProjectHistoryKeys.projectHistoryFirstOpTimestamp({
- project_id: this.project_id,
- }),
- (error, result) => {
- if (error != null) {
- throw error
- }
- result = parseInt(result, 10)
- result.should.equal(this.firstOpTimestamp)
- done()
- }
- )
- })
- it('should yield last updated time', async function () {
- const { lastUpdatedAt } =
- await DocUpdaterClient.getProjectLastUpdatedAt(this.project_id)
- lastUpdatedAt.should.be.within(this.secondStartTime, Date.now())
- })
- })
- })
- describe('when the document is loaded', function () {
- beforeEach(async function () {
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- sinon.resetHistory()
- await sendUpdateAndWait(this.project_id, this.doc_id, this.update)
- })
- it('should not need to call the web api', function () {
- MockWebApi.getDocument.called.should.equal(false)
- })
- it('should update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.result)
- })
- it('should push the applied updates to the project history changes api', function (done) {
- rclientProjectHistory.lrange(
- ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
- 0,
- -1,
- (error, updates) => {
- if (error) return done(error)
- JSON.parse(updates[0]).op.should.deep.equal([this.op])
- done()
- }
- )
- })
- })
- describe('when the document is loaded and is using project-history only', function () {
- beforeEach(async function () {
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- })
- await DocUpdaterClient.preloadDoc(this.project_id, this.doc_id)
- sinon.resetHistory()
- await sendUpdateAndWait(this.project_id, this.doc_id, this.update)
- })
- it('should update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.result)
- })
- it('should push the applied updates to the project history changes api', function (done) {
- rclientProjectHistory.lrange(
- ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
- 0,
- -1,
- (error, updates) => {
- if (error) return done(error)
- JSON.parse(updates[0]).op.should.deep.equal([this.op])
- done()
- }
- )
- })
- })
- describe('when the document is loaded (history-ot)', function () {
- beforeEach(async function () {
- 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 sendUpdateAndWait(
- this.project_id,
- this.doc_id,
- this.historyOTUpdate
- )
- })
- it('should update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.result)
- })
- it('should push the applied updates to the project history changes api', function (done) {
- rclientProjectHistory.lrange(
- ProjectHistoryKeys.projectHistoryOps({ project_id: this.project_id }),
- 0,
- -1,
- (error, updates) => {
- if (error) return done(error)
- JSON.parse(updates[0]).op.should.deep.equal(this.historyOTUpdate.op)
- JSON.parse(updates[0]).meta.pathname.should.equal('/a/b/c.tex')
- done()
- }
- )
- })
- })
- describe('when the document has been deleted', function () {
- describe('when the ops come in a single linear order', function () {
- beforeEach(async function () {
- const lines = ['', '', '']
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines,
- version: 0,
- })
- this.updates = [
- { doc_id: this.doc_id, v: 0, op: [{ i: 'h', p: 0 }] },
- { doc_id: this.doc_id, v: 1, op: [{ i: 'e', p: 1 }] },
- { doc_id: this.doc_id, v: 2, op: [{ i: 'l', p: 2 }] },
- { doc_id: this.doc_id, v: 3, op: [{ i: 'l', p: 3 }] },
- { doc_id: this.doc_id, v: 4, op: [{ i: 'o', p: 4 }] },
- { doc_id: this.doc_id, v: 5, op: [{ i: ' ', p: 5 }] },
- { doc_id: this.doc_id, v: 6, op: [{ i: 'w', p: 6 }] },
- { doc_id: this.doc_id, v: 7, op: [{ i: 'o', p: 7 }] },
- { doc_id: this.doc_id, v: 8, op: [{ i: 'r', p: 8 }] },
- { doc_id: this.doc_id, v: 9, op: [{ i: 'l', p: 9 }] },
- { doc_id: this.doc_id, v: 10, op: [{ i: 'd', p: 10 }] },
- ]
- this.my_result = ['hello world', '', '']
- for (const update of this.updates.slice(0, 6)) {
- await DocUpdaterClient.sendUpdate(
- this.project_id,
- this.doc_id,
- update
- )
- }
- await DocUpdaterClient.deleteDoc(this.project_id, this.doc_id)
- for (const update of this.updates.slice(6)) {
- await DocUpdaterClient.sendUpdate(
- this.project_id,
- this.doc_id,
- update
- )
- }
- await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- })
- it('should be able to continue applying updates when the project has been deleted', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.my_result)
- })
- it('should store the doc ops in the correct order', function (done) {
- rclientDU.lrange(
- Keys.docOps({ doc_id: this.doc_id }),
- 0,
- -1,
- (error, updates) => {
- if (error) return done(error)
- updates = updates.map(u => JSON.parse(u))
- for (let i = 0; i < this.updates.length; i++) {
- const appliedUpdate = this.updates[i]
- appliedUpdate.op.should.deep.equal(updates[i].op)
- }
- done()
- }
- )
- })
- })
- describe('when older ops come in after the delete', function () {
- beforeEach(function () {
- const lines = ['', '', '']
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines,
- version: 0,
- })
- this.updates = [
- { doc_id: this.doc_id, v: 0, op: [{ i: 'h', p: 0 }] },
- { doc_id: this.doc_id, v: 1, op: [{ i: 'e', p: 1 }] },
- { doc_id: this.doc_id, v: 2, op: [{ i: 'l', p: 2 }] },
- { doc_id: this.doc_id, v: 3, op: [{ i: 'l', p: 3 }] },
- { doc_id: this.doc_id, v: 4, op: [{ i: 'o', p: 4 }] },
- { doc_id: this.doc_id, v: 0, op: [{ i: 'world', p: 1 }] },
- ]
- this.my_result = ['hello', 'world', '']
- })
- it('should be able to continue applying updates when the project has been deleted', async function () {
- for (const update of this.updates.slice(0, 5)) {
- await DocUpdaterClient.sendUpdate(
- this.project_id,
- this.doc_id,
- update
- )
- }
- await DocUpdaterClient.deleteDoc(this.project_id, this.doc_id)
- for (const update of this.updates.slice(5)) {
- await DocUpdaterClient.sendUpdate(
- this.project_id,
- this.doc_id,
- update
- )
- }
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.my_result)
- })
- })
- })
- describe('with a broken update', function () {
- beforeEach(async function () {
- this.broken_update = {
- doc: this.doc_id,
- v: this.version,
- op: [{ d: 'not the correct content', p: 0 }],
- }
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- })
- DocUpdaterClient.subscribeToAppliedOps(
- (this.messageCallback = sinon.stub())
- )
- await sendUpdateAndWait(this.project_id, this.doc_id, this.broken_update)
- })
- it('should not update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.lines)
- })
- it('should send a message with an error', function () {
- this.messageCallback.called.should.equal(true)
- const [channel, message] = this.messageCallback.args[0]
- channel.should.equal('applied-ops')
- JSON.parse(message).should.deep.include({
- project_id: this.project_id,
- doc_id: this.doc_id,
- error: 'Delete component does not match',
- })
- })
- })
- describe('with a broken update (history-ot)', function () {
- beforeEach(async function () {
- this.broken_update = {
- doc: this.doc_id,
- v: this.version,
- op: [{ textOperation: [99, -1] }],
- meta: { source: '42' },
- }
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- otMigrationStage: 1,
- })
- DocUpdaterClient.subscribeToAppliedOps(
- (this.messageCallback = sinon.stub())
- )
- await sendUpdateAndWait(this.project_id, this.doc_id, this.broken_update)
- })
- it('should not update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.lines)
- })
- it('should send a message with an error', function () {
- this.messageCallback.called.should.equal(true)
- const [channel, message] = this.messageCallback.args[0]
- channel.should.equal('applied-ops')
- JSON.parse(message).should.deep.include({
- project_id: this.project_id,
- doc_id: this.doc_id,
- error:
- "The operation's base length must be equal to the string's length.",
- })
- })
- })
- describe('when mixing ot types (sharejs-text-ot -> history-ot)', function () {
- beforeEach(async function () {
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- otMigrationStage: 0,
- })
- DocUpdaterClient.subscribeToAppliedOps(
- (this.messageCallback = sinon.stub())
- )
- await sendUpdateAndWait(
- this.project_id,
- this.doc_id,
- this.historyOTUpdate
- )
- })
- it('should not update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.lines)
- })
- it('should send a message with an error', function () {
- this.messageCallback.called.should.equal(true)
- const [channel, message] = this.messageCallback.args[0]
- channel.should.equal('applied-ops')
- JSON.parse(message).should.deep.include({
- project_id: this.project_id,
- doc_id: this.doc_id,
- error: 'ot type mismatch',
- })
- })
- })
- describe('when mixing ot types (history-ot -> sharejs-text-ot)', function () {
- beforeEach(async function () {
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- otMigrationStage: 1,
- })
- DocUpdaterClient.subscribeToAppliedOps(
- (this.messageCallback = sinon.stub())
- )
- await sendUpdateAndWait(this.project_id, this.doc_id, this.update)
- })
- it('should not update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.lines)
- })
- it('should send a message with an error', function () {
- this.messageCallback.called.should.equal(true)
- const [channel, message] = this.messageCallback.args[0]
- channel.should.equal('applied-ops')
- JSON.parse(message).should.deep.include({
- project_id: this.project_id,
- doc_id: this.doc_id,
- error: 'ot type mismatch',
- })
- })
- })
- describe('when there is no version in Mongo', function () {
- beforeEach(async function () {
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- })
- const update = {
- doc: this.doc_id,
- op: this.update.op,
- v: 0,
- }
- await sendUpdateAndWait(this.project_id, this.doc_id, update)
- })
- it('should update the doc (using version = 0)', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.result)
- })
- })
- describe('when the sending duplicate ops', function () {
- beforeEach(async function () {
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- })
- DocUpdaterClient.subscribeToAppliedOps(
- (this.messageCallback = sinon.stub())
- )
- // One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
- await sendUpdateAndWait(this.project_id, this.doc_id, {
- doc: this.doc_id,
- op: [
- {
- i: 'one and a half\n',
- p: 4,
- },
- ],
- v: this.version,
- meta: {
- source: 'ikHceq3yfAdQYzBo4-xZ',
- },
- })
- await sendUpdateAndWait(this.project_id, this.doc_id, {
- doc: this.doc_id,
- op: [
- {
- i: 'one and a half\n',
- p: 4,
- },
- ],
- v: this.version,
- dupIfSource: ['ikHceq3yfAdQYzBo4-xZ'],
- meta: {
- source: 'ikHceq3yfAdQYzBo4-xZ',
- },
- })
- })
- it('should update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.result)
- })
- it('should return a message about duplicate ops', function () {
- this.messageCallback.calledTwice.should.equal(true)
- this.messageCallback.args[0][0].should.equal('applied-ops')
- expect(JSON.parse(this.messageCallback.args[0][1]).op.dup).to.be.undefined
- this.messageCallback.args[1][0].should.equal('applied-ops')
- expect(JSON.parse(this.messageCallback.args[1][1]).op.dup).to.equal(true)
- })
- })
- describe('when sending duplicate ops (history-ot)', function () {
- beforeEach(async function () {
- MockWebApi.insertDoc(this.project_id, this.doc_id, {
- lines: this.lines,
- version: this.version,
- otMigrationStage: 1,
- })
- DocUpdaterClient.subscribeToAppliedOps(
- (this.messageCallback = sinon.stub())
- )
- // One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
- await sendUpdateAndWait(this.project_id, this.doc_id, {
- doc: this.doc_id,
- op: [{ textOperation: [4, 'one and a half\n', 9] }],
- v: this.version,
- meta: {
- source: 'ikHceq3yfAdQYzBo4-xZ',
- },
- })
- await sendUpdateAndWait(this.project_id, this.doc_id, {
- doc: this.doc_id,
- op: [
- {
- textOperation: [4, 'one and a half\n', 9],
- },
- ],
- v: this.version,
- dupIfSource: ['ikHceq3yfAdQYzBo4-xZ'],
- meta: {
- source: 'ikHceq3yfAdQYzBo4-xZ',
- },
- })
- })
- it('should update the doc', async function () {
- const doc = await DocUpdaterClient.getDoc(this.project_id, this.doc_id)
- doc.lines.should.deep.equal(this.result)
- })
- it('should return a message about duplicate ops', function () {
- this.messageCallback.calledTwice.should.equal(true)
- this.messageCallback.args[0][0].should.equal('applied-ops')
- expect(JSON.parse(this.messageCallback.args[0][1]).op.dup).to.be.undefined
- this.messageCallback.args[1][0].should.equal('applied-ops')
- expect(JSON.parse(this.messageCallback.args[1][1]).op.dup).to.equal(true)
- })
- })
- describe('when sending updates for a non-existing doc id', function () {
- beforeEach(async function () {
- this.non_existing = {
- doc: this.doc_id,
- v: this.version,
- op: [{ d: 'content', p: 0 }],
- }
- DocUpdaterClient.subscribeToAppliedOps(
- (this.messageCallback = sinon.stub())
- )
- await sendUpdateAndWait(this.project_id, this.doc_id, this.non_existing)
- })
- it('should not update or create a doc', async function () {
- await expect(DocUpdaterClient.getDoc(this.project_id, this.doc_id))
- .to.be.rejectedWith(RequestFailedError)
- .and.eventually.have.nested.property('response.status', 404)
- })
- it('should send a message with an error', function () {
- this.messageCallback.called.should.equal(true)
- const [channel, message] = this.messageCallback.args[0]
- channel.should.equal('applied-ops')
- JSON.parse(message).should.deep.include({
- project_id: this.project_id,
- doc_id: this.doc_id,
- error: 'doc not found',
- })
- })
- })
- })
|