Files
overleaf-cep/services/real-time/test/unit/js/RoomManager.test.js
T
Jakob Ackermann e714810632 [real-time] remove per-doc rooms (#35691)
* [real-time] remove applied-ops pub/sub

* [real-time] remove per-doc rooms

GitOrigin-RevId: ec3b76b37a029df15bf20d6f2832fb7c089c0042
2026-09-17 11:34:11 +00:00

273 lines
9.1 KiB
JavaScript

import { vi, expect, describe, beforeEach, afterEach, it } from 'vitest'
import sinon from 'sinon'
const modulePath = '../../../app/js/RoomManager.js'
describe('RoomManager', function () {
beforeEach(async function (ctx) {
ctx.project_id = 'project-id-123'
ctx.doc_id = 'doc-id-456'
ctx.other_doc_id = 'doc-id-789'
ctx.client = { namespace: { name: '' }, id: 'first-client' }
vi.doMock('@overleaf/settings', () => ({
default: (ctx.settings = {}),
}))
vi.doMock('@overleaf/metrics', () => ({
default: (ctx.metrics = { gauge: sinon.stub() }),
}))
ctx.RoomManager = (await import(modulePath)).default
ctx.RoomManager._clientsInRoom = sinon.stub()
ctx.RoomManager._clientAlreadyInRoom = sinon.stub()
ctx.RoomEvents = ctx.RoomManager.eventSource()
sinon.spy(ctx.RoomEvents, 'emit')
sinon.spy(ctx.RoomEvents, 'once')
})
describe('emitOnCompletion', function () {
describe('when a subscribe errors', function () {
afterEach(function (ctx) {
process.removeListener('unhandledRejection', ctx.onUnhandled)
})
beforeEach(async function (ctx) {
await new Promise((resolve, reject) => {
ctx.onUnhandled = error => {
ctx.unhandledError = error
reject(new Error(`unhandledRejection: ${error.message}`))
}
process.on('unhandledRejection', ctx.onUnhandled)
let rejectSubscribePromise
const subscribePromise = new Promise(
// eslint-disable-next-line promise/param-names
(_, r) => (rejectSubscribePromise = r)
)
const promises = [subscribePromise]
const eventName = 'project-subscribed-123'
ctx.RoomEvents.once(eventName, () => setTimeout(resolve, 100))
ctx.RoomManager.emitOnCompletion(promises, eventName)
setTimeout(() =>
rejectSubscribePromise(new Error('subscribe failed'))
)
})
})
it('should keep going', function (ctx) {
expect(ctx.unhandledError).to.not.exist
})
})
})
describe('joinProject', function () {
describe('when the project room is empty', function () {
beforeEach(async function (ctx) {
await new Promise((resolve, reject) => {
ctx.RoomManager._clientsInRoom
.withArgs(ctx.client, ctx.project_id)
.onFirstCall()
.returns(0)
ctx.client.join = sinon.stub()
ctx.callback = sinon.stub()
ctx.RoomEvents.on('project-active', id => {
setTimeout(() => {
ctx.RoomEvents.emit(`project-subscribed-${id}`)
}, 100)
})
ctx.RoomManager.joinProject(ctx.client, ctx.project_id, err => {
ctx.callback(err)
resolve()
})
})
})
it("should emit a 'project-active' event with the id", function (ctx) {
ctx.RoomEvents.emit
.calledWithExactly('project-active', ctx.project_id)
.should.equal(true)
})
it("should listen for the 'project-subscribed-id' event", function (ctx) {
ctx.RoomEvents.once
.calledWith(`project-subscribed-${ctx.project_id}`)
.should.equal(true)
})
it('should join the room using the id', function (ctx) {
ctx.client.join.calledWithExactly(ctx.project_id).should.equal(true)
})
})
describe('when there are other clients in the project room', function () {
beforeEach(async function (ctx) {
await new Promise((resolve, reject) => {
ctx.RoomManager._clientsInRoom
.withArgs(ctx.client, ctx.project_id)
.onFirstCall()
.returns(123)
.onSecondCall()
.returns(124)
ctx.client.join = sinon.stub()
ctx.RoomManager.joinProject(ctx.client, ctx.project_id, err => {
if (err) return reject(err)
resolve()
})
})
})
it('should join the room using the id', function (ctx) {
ctx.client.join.called.should.equal(true)
})
it('should not emit any events', function (ctx) {
ctx.RoomEvents.emit.called.should.equal(false)
})
})
})
describe('leaveProjectAndDocs', function () {
describe('when the client is connected to the project and multiple docs', function () {
beforeEach(function (ctx) {
ctx.RoomManager._roomsClientIsIn = sinon
.stub()
.returns([ctx.project_id, ctx.doc_id, ctx.other_doc_id])
ctx.client.join = sinon.stub()
ctx.client.leave = sinon.stub()
})
describe('when this is the only client connected', function () {
beforeEach(async function (ctx) {
await new Promise((resolve, reject) => {
// first call is for the join,
// second for the leave
ctx.RoomManager._clientsInRoom
.withArgs(ctx.client, ctx.doc_id)
.onCall(0)
.returns(0)
.onCall(1)
.returns(0)
ctx.RoomManager._clientsInRoom
.withArgs(ctx.client, ctx.other_doc_id)
.onCall(0)
.returns(0)
.onCall(1)
.returns(0)
ctx.RoomManager._clientsInRoom
.withArgs(ctx.client, ctx.project_id)
.onCall(0)
.returns(0)
.onCall(1)
.returns(0)
ctx.RoomManager._clientAlreadyInRoom
.withArgs(ctx.client, ctx.doc_id)
.returns(true)
.withArgs(ctx.client, ctx.other_doc_id)
.returns(true)
.withArgs(ctx.client, ctx.project_id)
.returns(true)
ctx.RoomEvents.on('project-active', id => {
setTimeout(() => {
ctx.RoomEvents.emit(`project-subscribed-${id}`)
}, 100)
})
ctx.RoomEvents.on('doc-active', id => {
setTimeout(() => {
ctx.RoomEvents.emit(`doc-subscribed-${id}`)
}, 100)
})
// put the client in the rooms
ctx.RoomManager.joinProject(ctx.client, ctx.project_id, () => {
ctx.RoomManager.joinEntity(ctx.client, 'doc', ctx.doc_id, () => {
ctx.RoomManager.joinEntity(
ctx.client,
'doc',
ctx.other_doc_id,
() => {
// now leave the project
ctx.RoomManager.leaveProjectAndDocs(ctx.client)
resolve()
}
)
})
})
})
})
it('should leave all the docs', function (ctx) {
ctx.client.leave.calledWithExactly(ctx.doc_id).should.equal(true)
ctx.client.leave
.calledWithExactly(ctx.other_doc_id)
.should.equal(true)
})
it('should leave the project', function (ctx) {
ctx.client.leave.calledWithExactly(ctx.project_id).should.equal(true)
})
it("should emit a 'doc-empty' event with the id for each doc", function (ctx) {
ctx.RoomEvents.emit
.calledWithExactly('doc-empty', ctx.doc_id)
.should.equal(true)
ctx.RoomEvents.emit
.calledWithExactly('doc-empty', ctx.other_doc_id)
.should.equal(true)
})
it("should emit a 'project-empty' event with the id for the project", function (ctx) {
ctx.RoomEvents.emit
.calledWithExactly('project-empty', ctx.project_id)
.should.equal(true)
})
})
describe('when other clients are still connected', function () {
beforeEach(function (ctx) {
ctx.RoomManager._clientsInRoom
.withArgs(ctx.client, ctx.doc_id)
.onFirstCall()
.returns(123)
.onSecondCall()
.returns(122)
ctx.RoomManager._clientsInRoom
.withArgs(ctx.client, ctx.other_doc_id)
.onFirstCall()
.returns(123)
.onSecondCall()
.returns(122)
ctx.RoomManager._clientsInRoom
.withArgs(ctx.client, ctx.project_id)
.onFirstCall()
.returns(123)
.onSecondCall()
.returns(122)
ctx.RoomManager._clientAlreadyInRoom
.withArgs(ctx.client, ctx.doc_id)
.returns(true)
.withArgs(ctx.client, ctx.other_doc_id)
.returns(true)
.withArgs(ctx.client, ctx.project_id)
.returns(true)
ctx.RoomManager.leaveProjectAndDocs(ctx.client)
})
it('should leave all the docs', function (ctx) {
ctx.client.leave.calledWithExactly(ctx.doc_id).should.equal(true)
ctx.client.leave
.calledWithExactly(ctx.other_doc_id)
.should.equal(true)
})
it('should leave the project', function (ctx) {
ctx.client.leave.calledWithExactly(ctx.project_id).should.equal(true)
})
it('should not emit any events', function (ctx) {
ctx.RoomEvents.emit.called.should.equal(false)
})
})
})
})
})