RoomManager.test.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. import { vi, expect, describe, beforeEach, afterEach, it } from 'vitest'
  2. import sinon from 'sinon'
  3. const modulePath = '../../../app/js/RoomManager.js'
  4. describe('RoomManager', function () {
  5. beforeEach(async function (ctx) {
  6. ctx.project_id = 'project-id-123'
  7. ctx.doc_id = 'doc-id-456'
  8. ctx.other_doc_id = 'doc-id-789'
  9. ctx.client = { namespace: { name: '' }, id: 'first-client' }
  10. vi.doMock('@overleaf/settings', () => ({
  11. default: (ctx.settings = {}),
  12. }))
  13. vi.doMock('@overleaf/metrics', () => ({
  14. default: (ctx.metrics = { gauge: sinon.stub() }),
  15. }))
  16. ctx.RoomManager = (await import(modulePath)).default
  17. ctx.RoomManager._clientsInRoom = sinon.stub()
  18. ctx.RoomManager._clientAlreadyInRoom = sinon.stub()
  19. ctx.RoomEvents = ctx.RoomManager.eventSource()
  20. sinon.spy(ctx.RoomEvents, 'emit')
  21. sinon.spy(ctx.RoomEvents, 'once')
  22. })
  23. describe('emitOnCompletion', function () {
  24. describe('when a subscribe errors', function () {
  25. afterEach(function (ctx) {
  26. process.removeListener('unhandledRejection', ctx.onUnhandled)
  27. })
  28. beforeEach(async function (ctx) {
  29. await new Promise((resolve, reject) => {
  30. ctx.onUnhandled = error => {
  31. ctx.unhandledError = error
  32. reject(new Error(`unhandledRejection: ${error.message}`))
  33. }
  34. process.on('unhandledRejection', ctx.onUnhandled)
  35. let rejectSubscribePromise
  36. const subscribePromise = new Promise(
  37. // eslint-disable-next-line promise/param-names
  38. (_, r) => (rejectSubscribePromise = r)
  39. )
  40. const promises = [subscribePromise]
  41. const eventName = 'project-subscribed-123'
  42. ctx.RoomEvents.once(eventName, () => setTimeout(resolve, 100))
  43. ctx.RoomManager.emitOnCompletion(promises, eventName)
  44. setTimeout(() =>
  45. rejectSubscribePromise(new Error('subscribe failed'))
  46. )
  47. })
  48. })
  49. it('should keep going', function (ctx) {
  50. expect(ctx.unhandledError).to.not.exist
  51. })
  52. })
  53. })
  54. describe('joinProject', function () {
  55. describe('when the project room is empty', function () {
  56. beforeEach(async function (ctx) {
  57. await new Promise((resolve, reject) => {
  58. ctx.RoomManager._clientsInRoom
  59. .withArgs(ctx.client, ctx.project_id)
  60. .onFirstCall()
  61. .returns(0)
  62. ctx.client.join = sinon.stub()
  63. ctx.callback = sinon.stub()
  64. ctx.RoomEvents.on('project-active', id => {
  65. setTimeout(() => {
  66. ctx.RoomEvents.emit(`project-subscribed-${id}`)
  67. }, 100)
  68. })
  69. ctx.RoomManager.joinProject(ctx.client, ctx.project_id, err => {
  70. ctx.callback(err)
  71. resolve()
  72. })
  73. })
  74. })
  75. it("should emit a 'project-active' event with the id", function (ctx) {
  76. ctx.RoomEvents.emit
  77. .calledWithExactly('project-active', ctx.project_id)
  78. .should.equal(true)
  79. })
  80. it("should listen for the 'project-subscribed-id' event", function (ctx) {
  81. ctx.RoomEvents.once
  82. .calledWith(`project-subscribed-${ctx.project_id}`)
  83. .should.equal(true)
  84. })
  85. it('should join the room using the id', function (ctx) {
  86. ctx.client.join.calledWithExactly(ctx.project_id).should.equal(true)
  87. })
  88. })
  89. describe('when there are other clients in the project room', function () {
  90. beforeEach(async function (ctx) {
  91. await new Promise((resolve, reject) => {
  92. ctx.RoomManager._clientsInRoom
  93. .withArgs(ctx.client, ctx.project_id)
  94. .onFirstCall()
  95. .returns(123)
  96. .onSecondCall()
  97. .returns(124)
  98. ctx.client.join = sinon.stub()
  99. ctx.RoomManager.joinProject(ctx.client, ctx.project_id, err => {
  100. if (err) return reject(err)
  101. resolve()
  102. })
  103. })
  104. })
  105. it('should join the room using the id', function (ctx) {
  106. ctx.client.join.called.should.equal(true)
  107. })
  108. it('should not emit any events', function (ctx) {
  109. ctx.RoomEvents.emit.called.should.equal(false)
  110. })
  111. })
  112. })
  113. describe('joinDoc', function () {
  114. describe('when the doc room is empty', function () {
  115. beforeEach(async function (ctx) {
  116. await new Promise((resolve, reject) => {
  117. ctx.RoomManager._clientsInRoom
  118. .withArgs(ctx.client, ctx.doc_id)
  119. .onFirstCall()
  120. .returns(0)
  121. ctx.client.join = sinon.stub()
  122. ctx.callback = sinon.stub()
  123. ctx.RoomEvents.on('doc-active', id => {
  124. setTimeout(() => {
  125. ctx.RoomEvents.emit(`doc-subscribed-${id}`)
  126. }, 100)
  127. })
  128. ctx.RoomManager.joinDoc(ctx.client, ctx.doc_id, err => {
  129. ctx.callback(err)
  130. resolve()
  131. })
  132. })
  133. })
  134. it("should emit a 'doc-active' event with the id", function (ctx) {
  135. ctx.RoomEvents.emit
  136. .calledWithExactly('doc-active', ctx.doc_id)
  137. .should.equal(true)
  138. })
  139. it("should listen for the 'doc-subscribed-id' event", function (ctx) {
  140. ctx.RoomEvents.once
  141. .calledWith(`doc-subscribed-${ctx.doc_id}`)
  142. .should.equal(true)
  143. })
  144. it('should join the room using the id', function (ctx) {
  145. ctx.client.join.calledWithExactly(ctx.doc_id).should.equal(true)
  146. })
  147. })
  148. describe('when there are other clients in the doc room', function () {
  149. beforeEach(async function (ctx) {
  150. await new Promise((resolve, reject) => {
  151. ctx.RoomManager._clientsInRoom
  152. .withArgs(ctx.client, ctx.doc_id)
  153. .onFirstCall()
  154. .returns(123)
  155. .onSecondCall()
  156. .returns(124)
  157. ctx.client.join = sinon.stub()
  158. ctx.RoomManager.joinDoc(ctx.client, ctx.doc_id, err => {
  159. if (err) return reject(err)
  160. resolve()
  161. })
  162. })
  163. })
  164. it('should join the room using the id', function (ctx) {
  165. ctx.client.join.called.should.equal(true)
  166. })
  167. it('should not emit any events', function (ctx) {
  168. ctx.RoomEvents.emit.called.should.equal(false)
  169. })
  170. })
  171. })
  172. describe('leaveDoc', function () {
  173. describe('when doc room will be empty after this client has left', function () {
  174. beforeEach(function (ctx) {
  175. ctx.RoomManager._clientAlreadyInRoom
  176. .withArgs(ctx.client, ctx.doc_id)
  177. .returns(true)
  178. ctx.RoomManager._clientsInRoom
  179. .withArgs(ctx.client, ctx.doc_id)
  180. .onCall(0)
  181. .returns(0)
  182. ctx.client.leave = sinon.stub()
  183. ctx.RoomManager.leaveDoc(ctx.client, ctx.doc_id)
  184. })
  185. it('should leave the room using the id', function (ctx) {
  186. ctx.client.leave.calledWithExactly(ctx.doc_id).should.equal(true)
  187. })
  188. it("should emit a 'doc-empty' event with the id", function (ctx) {
  189. ctx.RoomEvents.emit
  190. .calledWithExactly('doc-empty', ctx.doc_id)
  191. .should.equal(true)
  192. })
  193. })
  194. describe('when there are other clients in the doc room', function () {
  195. beforeEach(function (ctx) {
  196. ctx.RoomManager._clientAlreadyInRoom
  197. .withArgs(ctx.client, ctx.doc_id)
  198. .returns(true)
  199. ctx.RoomManager._clientsInRoom
  200. .withArgs(ctx.client, ctx.doc_id)
  201. .onCall(0)
  202. .returns(123)
  203. ctx.client.leave = sinon.stub()
  204. ctx.RoomManager.leaveDoc(ctx.client, ctx.doc_id)
  205. })
  206. it('should leave the room using the id', function (ctx) {
  207. ctx.client.leave.calledWithExactly(ctx.doc_id).should.equal(true)
  208. })
  209. it('should not emit any events', function (ctx) {
  210. ctx.RoomEvents.emit.called.should.equal(false)
  211. })
  212. })
  213. describe('when the client is not in the doc room', function () {
  214. beforeEach(function (ctx) {
  215. ctx.RoomManager._clientAlreadyInRoom
  216. .withArgs(ctx.client, ctx.doc_id)
  217. .returns(false)
  218. ctx.RoomManager._clientsInRoom
  219. .withArgs(ctx.client, ctx.doc_id)
  220. .onCall(0)
  221. .returns(0)
  222. ctx.client.leave = sinon.stub()
  223. ctx.RoomManager.leaveDoc(ctx.client, ctx.doc_id)
  224. })
  225. it('should not leave the room', function (ctx) {
  226. ctx.client.leave.called.should.equal(false)
  227. })
  228. it('should not emit any events', function (ctx) {
  229. ctx.RoomEvents.emit.called.should.equal(false)
  230. })
  231. })
  232. })
  233. describe('leaveProjectAndDocs', function () {
  234. describe('when the client is connected to the project and multiple docs', function () {
  235. beforeEach(function (ctx) {
  236. ctx.RoomManager._roomsClientIsIn = sinon
  237. .stub()
  238. .returns([ctx.project_id, ctx.doc_id, ctx.other_doc_id])
  239. ctx.client.join = sinon.stub()
  240. ctx.client.leave = sinon.stub()
  241. })
  242. describe('when this is the only client connected', function () {
  243. beforeEach(async function (ctx) {
  244. await new Promise((resolve, reject) => {
  245. // first call is for the join,
  246. // second for the leave
  247. ctx.RoomManager._clientsInRoom
  248. .withArgs(ctx.client, ctx.doc_id)
  249. .onCall(0)
  250. .returns(0)
  251. .onCall(1)
  252. .returns(0)
  253. ctx.RoomManager._clientsInRoom
  254. .withArgs(ctx.client, ctx.other_doc_id)
  255. .onCall(0)
  256. .returns(0)
  257. .onCall(1)
  258. .returns(0)
  259. ctx.RoomManager._clientsInRoom
  260. .withArgs(ctx.client, ctx.project_id)
  261. .onCall(0)
  262. .returns(0)
  263. .onCall(1)
  264. .returns(0)
  265. ctx.RoomManager._clientAlreadyInRoom
  266. .withArgs(ctx.client, ctx.doc_id)
  267. .returns(true)
  268. .withArgs(ctx.client, ctx.other_doc_id)
  269. .returns(true)
  270. .withArgs(ctx.client, ctx.project_id)
  271. .returns(true)
  272. ctx.RoomEvents.on('project-active', id => {
  273. setTimeout(() => {
  274. ctx.RoomEvents.emit(`project-subscribed-${id}`)
  275. }, 100)
  276. })
  277. ctx.RoomEvents.on('doc-active', id => {
  278. setTimeout(() => {
  279. ctx.RoomEvents.emit(`doc-subscribed-${id}`)
  280. }, 100)
  281. })
  282. // put the client in the rooms
  283. ctx.RoomManager.joinProject(ctx.client, ctx.project_id, () => {
  284. ctx.RoomManager.joinDoc(ctx.client, ctx.doc_id, () => {
  285. ctx.RoomManager.joinDoc(ctx.client, ctx.other_doc_id, () => {
  286. // now leave the project
  287. ctx.RoomManager.leaveProjectAndDocs(ctx.client)
  288. resolve()
  289. })
  290. })
  291. })
  292. })
  293. })
  294. it('should leave all the docs', function (ctx) {
  295. ctx.client.leave.calledWithExactly(ctx.doc_id).should.equal(true)
  296. ctx.client.leave
  297. .calledWithExactly(ctx.other_doc_id)
  298. .should.equal(true)
  299. })
  300. it('should leave the project', function (ctx) {
  301. ctx.client.leave.calledWithExactly(ctx.project_id).should.equal(true)
  302. })
  303. it("should emit a 'doc-empty' event with the id for each doc", function (ctx) {
  304. ctx.RoomEvents.emit
  305. .calledWithExactly('doc-empty', ctx.doc_id)
  306. .should.equal(true)
  307. ctx.RoomEvents.emit
  308. .calledWithExactly('doc-empty', ctx.other_doc_id)
  309. .should.equal(true)
  310. })
  311. it("should emit a 'project-empty' event with the id for the project", function (ctx) {
  312. ctx.RoomEvents.emit
  313. .calledWithExactly('project-empty', ctx.project_id)
  314. .should.equal(true)
  315. })
  316. })
  317. describe('when other clients are still connected', function () {
  318. beforeEach(function (ctx) {
  319. ctx.RoomManager._clientsInRoom
  320. .withArgs(ctx.client, ctx.doc_id)
  321. .onFirstCall()
  322. .returns(123)
  323. .onSecondCall()
  324. .returns(122)
  325. ctx.RoomManager._clientsInRoom
  326. .withArgs(ctx.client, ctx.other_doc_id)
  327. .onFirstCall()
  328. .returns(123)
  329. .onSecondCall()
  330. .returns(122)
  331. ctx.RoomManager._clientsInRoom
  332. .withArgs(ctx.client, ctx.project_id)
  333. .onFirstCall()
  334. .returns(123)
  335. .onSecondCall()
  336. .returns(122)
  337. ctx.RoomManager._clientAlreadyInRoom
  338. .withArgs(ctx.client, ctx.doc_id)
  339. .returns(true)
  340. .withArgs(ctx.client, ctx.other_doc_id)
  341. .returns(true)
  342. .withArgs(ctx.client, ctx.project_id)
  343. .returns(true)
  344. ctx.RoomManager.leaveProjectAndDocs(ctx.client)
  345. })
  346. it('should leave all the docs', function (ctx) {
  347. ctx.client.leave.calledWithExactly(ctx.doc_id).should.equal(true)
  348. ctx.client.leave
  349. .calledWithExactly(ctx.other_doc_id)
  350. .should.equal(true)
  351. })
  352. it('should leave the project', function (ctx) {
  353. ctx.client.leave.calledWithExactly(ctx.project_id).should.equal(true)
  354. })
  355. it('should not emit any events', function (ctx) {
  356. ctx.RoomEvents.emit.called.should.equal(false)
  357. })
  358. })
  359. })
  360. })
  361. })