Files
overleaf-cep/services/real-time/test/unit/js/DocumentUpdaterController.test.js
Andrew Rumble 3073c94522 Merge pull request #30215 from overleaf/ar/convert-real-time-to-esm
[real-time] convert real time to esm

GitOrigin-RevId: 7cc530cc977549d3274be42585735e1fd72cad5f
2026-01-13 09:06:30 +00:00

272 lines
8.0 KiB
JavaScript

import { vi, describe, beforeEach, it } from 'vitest'
import sinon from 'sinon'
import MockClient from './helpers/MockClient.js'
import path from 'node:path'
const modulePath = path.join(
import.meta.dirname,
'../../../app/js/DocumentUpdaterController'
)
describe('DocumentUpdaterController', function () {
beforeEach(async function (ctx) {
ctx.project_id = 'project-id-123'
ctx.doc_id = 'doc-id-123'
ctx.callback = sinon.stub()
ctx.io = { mock: 'socket.io' }
ctx.rclient = []
ctx.RoomEvents = { on: sinon.stub() }
vi.doMock('@overleaf/settings', () => ({
default: (ctx.settings = {
redis: {
documentupdater: {
key_schema: {
pendingUpdates({ doc_id: docId }) {
return `PendingUpdates:${docId}`
},
},
},
pubsub: null,
},
}),
}))
vi.doMock('../../../app/js/RedisClientManager', () => ({
default: {
createClientList: () => {
ctx.redis = {
createClient: name => {
let rclientStub
ctx.rclient.push((rclientStub = { name }))
return rclientStub
},
}
},
},
}))
vi.doMock('../../../app/js/SafeJsonParse', () => ({
default: (ctx.SafeJsonParse = {
parse: (data, cb) => cb(null, JSON.parse(data)),
}),
}))
vi.doMock('../../../app/js/EventLogger', () => ({
default: (ctx.EventLogger = { checkEventOrder: sinon.stub() }),
}))
vi.doMock('../../../app/js/HealthCheckManager', () => ({
default: { check: sinon.stub() },
}))
vi.doMock('@overleaf/metrics', () => ({
default: (ctx.metrics = {
inc: sinon.stub(),
histogram: sinon.stub(),
}),
}))
vi.doMock('../../../app/js/RoomManager', () => ({
default: (ctx.RoomManager = {
eventSource: sinon.stub().returns(ctx.RoomEvents),
}),
}))
vi.doMock('../../../app/js/ChannelManager', () => ({
default: (ctx.ChannelManager = {}),
}))
ctx.EditorUpdatesController = (await import(modulePath)).default
})
describe('listenForUpdatesFromDocumentUpdater', function () {
beforeEach(function (ctx) {
ctx.rclient.length = 0 // clear any existing clients
ctx.EditorUpdatesController.rclientList = [
ctx.redis.createClient('first'),
ctx.redis.createClient('second'),
]
ctx.rclient[0].subscribe = sinon.stub()
ctx.rclient[0].on = sinon.stub()
ctx.rclient[1].subscribe = sinon.stub()
ctx.rclient[1].on = sinon.stub()
ctx.EditorUpdatesController.listenForUpdatesFromDocumentUpdater()
})
it('should subscribe to the doc-updater stream', function (ctx) {
ctx.rclient[0].subscribe.calledWith('applied-ops').should.equal(true)
})
it('should register a callback to handle updates', function (ctx) {
ctx.rclient[0].on.calledWith('message').should.equal(true)
})
it('should subscribe to any additional doc-updater stream', function (ctx) {
ctx.rclient[1].subscribe.calledWith('applied-ops').should.equal(true)
ctx.rclient[1].on.calledWith('message').should.equal(true)
})
})
describe('_processMessageFromDocumentUpdater', function () {
describe('with bad JSON', function () {
beforeEach(function (ctx) {
ctx.SafeJsonParse.parse = sinon
.stub()
.callsArgWith(1, new Error('oops'))
ctx.EditorUpdatesController._processMessageFromDocumentUpdater(
ctx.io,
'applied-ops',
'blah'
)
})
it('should log an error', function (ctx) {
ctx.logger.error.called.should.equal(true)
})
})
describe('with update', function () {
beforeEach(function (ctx) {
ctx.message = {
doc_id: ctx.doc_id,
op: { t: 'foo', p: 12 },
}
ctx.EditorUpdatesController._applyUpdateFromDocumentUpdater =
sinon.stub()
ctx.EditorUpdatesController._processMessageFromDocumentUpdater(
ctx.io,
'applied-ops',
JSON.stringify(ctx.message)
)
})
it('should apply the update', function (ctx) {
ctx.EditorUpdatesController._applyUpdateFromDocumentUpdater
.calledWith(ctx.io, ctx.doc_id, ctx.message.op)
.should.equal(true)
})
})
describe('with error', function () {
beforeEach(function (ctx) {
ctx.message = {
doc_id: ctx.doc_id,
error: 'Something went wrong',
}
ctx.EditorUpdatesController._processErrorFromDocumentUpdater =
sinon.stub()
ctx.EditorUpdatesController._processMessageFromDocumentUpdater(
ctx.io,
'applied-ops',
JSON.stringify(ctx.message)
)
})
it('should process the error', function (ctx) {
ctx.EditorUpdatesController._processErrorFromDocumentUpdater
.calledWith(ctx.io, ctx.doc_id, ctx.message.error)
.should.equal(true)
})
})
})
describe('_applyUpdateFromDocumentUpdater', function () {
beforeEach(function (ctx) {
ctx.sourceClient = new MockClient()
ctx.otherClients = [new MockClient(), new MockClient()]
ctx.update = {
op: [{ t: 'foo', p: 12 }],
meta: { source: ctx.sourceClient.publicId },
v: (ctx.version = 42),
doc: ctx.doc_id,
}
ctx.io.sockets = {
clients: sinon
.stub()
.returns([
ctx.sourceClient,
...Array.from(ctx.otherClients),
ctx.sourceClient,
]),
}
}) // include a duplicate client
describe('normally', function () {
beforeEach(function (ctx) {
ctx.EditorUpdatesController._applyUpdateFromDocumentUpdater(
ctx.io,
ctx.doc_id,
ctx.update
)
})
it('should send a version bump to the source client', function (ctx) {
ctx.sourceClient.emit
.calledWith('otUpdateApplied', { v: ctx.version, doc: ctx.doc_id })
.should.equal(true)
ctx.sourceClient.emit.calledOnce.should.equal(true)
})
it('should get the clients connected to the document', function (ctx) {
ctx.io.sockets.clients.calledWith(ctx.doc_id).should.equal(true)
})
it('should send the full update to the other clients', function (ctx) {
Array.from(ctx.otherClients).map(client =>
client.emit
.calledWith('otUpdateApplied', ctx.update)
.should.equal(true)
)
})
})
describe('with a duplicate op', function () {
beforeEach(function (ctx) {
ctx.update.dup = true
ctx.EditorUpdatesController._applyUpdateFromDocumentUpdater(
ctx.io,
ctx.doc_id,
ctx.update
)
})
it('should send a version bump to the source client as usual', function (ctx) {
ctx.sourceClient.emit
.calledWith('otUpdateApplied', { v: ctx.version, doc: ctx.doc_id })
.should.equal(true)
})
it("should not send anything to the other clients (they've already had the op)", function (ctx) {
Array.from(ctx.otherClients).map(client =>
client.emit.calledWith('otUpdateApplied').should.equal(false)
)
})
})
})
describe('_processErrorFromDocumentUpdater', function () {
beforeEach(function (ctx) {
ctx.clients = [new MockClient(), new MockClient()]
ctx.io.sockets = { clients: sinon.stub().returns(ctx.clients) }
ctx.EditorUpdatesController._processErrorFromDocumentUpdater(
ctx.io,
ctx.doc_id,
'Something went wrong'
)
})
it('should log a warning', function (ctx) {
ctx.logger.warn.called.should.equal(true)
})
it('should disconnect all clients in that document', function (ctx) {
ctx.io.sockets.clients.calledWith(ctx.doc_id).should.equal(true)
Array.from(ctx.clients).map(client =>
client.disconnect.called.should.equal(true)
)
})
})
})