| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- 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)
- )
- })
- })
- })
|