ConnectedUsersManagerTests.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. /* eslint-disable
  2. no-return-assign,
  3. no-unused-vars,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS102: Remove unnecessary code created because of implicit returns
  10. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  11. */
  12. const SandboxedModule = require('sandboxed-module')
  13. const assert = require('node:assert')
  14. const path = require('node:path')
  15. const sinon = require('sinon')
  16. const modulePath = path.join(__dirname, '../../../app/js/ConnectedUsersManager')
  17. const { expect } = require('chai')
  18. const tk = require('timekeeper')
  19. describe('ConnectedUsersManager', function () {
  20. beforeEach(function () {
  21. this.settings = {
  22. redis: {
  23. realtime: {
  24. key_schema: {
  25. clientsInProject({ project_id: projectId }) {
  26. return `clients_in_project:${projectId}`
  27. },
  28. connectedUser({ project_id: projectId, client_id: clientId }) {
  29. return `connected_user:${projectId}:${clientId}`
  30. },
  31. projectNotEmptySince({ projectId }) {
  32. return `projectNotEmptySince:{${projectId}}`
  33. },
  34. },
  35. },
  36. },
  37. }
  38. this.rClient = {
  39. auth() {},
  40. getdel: sinon.stub(),
  41. scard: sinon.stub(),
  42. set: sinon.stub(),
  43. setex: sinon.stub(),
  44. sadd: sinon.stub(),
  45. get: sinon.stub(),
  46. srem: sinon.stub(),
  47. del: sinon.stub(),
  48. smembers: sinon.stub(),
  49. expire: sinon.stub(),
  50. hset: sinon.stub(),
  51. hgetall: sinon.stub(),
  52. exec: sinon.stub(),
  53. multi: () => {
  54. return this.rClient
  55. },
  56. }
  57. tk.freeze(new Date())
  58. this.Metrics = {
  59. inc: sinon.stub(),
  60. histogram: sinon.stub(),
  61. }
  62. this.ConnectedUsersManager = SandboxedModule.require(modulePath, {
  63. requires: {
  64. '@overleaf/settings': this.settings,
  65. '@overleaf/metrics': this.Metrics,
  66. '@overleaf/redis-wrapper': {
  67. createClient: () => {
  68. return this.rClient
  69. },
  70. },
  71. },
  72. })
  73. this.client_id = '32132132'
  74. this.project_id = 'dskjh2u21321'
  75. this.user = {
  76. _id: 'user-id-123',
  77. first_name: 'Joe',
  78. last_name: 'Bloggs',
  79. email: 'joe@example.com',
  80. }
  81. return (this.cursorData = {
  82. row: 12,
  83. column: 9,
  84. doc_id: '53c3b8c85fee64000023dc6e',
  85. })
  86. })
  87. afterEach(function () {
  88. return tk.reset()
  89. })
  90. describe('updateUserPosition', function () {
  91. beforeEach(function () {
  92. this.rClient.exec.yields(null, [1, 1])
  93. })
  94. it('should set a key with the date and give it a ttl', function (done) {
  95. return this.ConnectedUsersManager.updateUserPosition(
  96. this.project_id,
  97. this.client_id,
  98. this.user,
  99. null,
  100. err => {
  101. if (err) return done(err)
  102. this.rClient.hset
  103. .calledWith(
  104. `connected_user:${this.project_id}:${this.client_id}`,
  105. 'last_updated_at',
  106. Date.now()
  107. )
  108. .should.equal(true)
  109. return done()
  110. }
  111. )
  112. })
  113. it('should set a key with the user_id', function (done) {
  114. return this.ConnectedUsersManager.updateUserPosition(
  115. this.project_id,
  116. this.client_id,
  117. this.user,
  118. null,
  119. err => {
  120. if (err) return done(err)
  121. this.rClient.hset
  122. .calledWith(
  123. `connected_user:${this.project_id}:${this.client_id}`,
  124. 'user_id',
  125. this.user._id
  126. )
  127. .should.equal(true)
  128. return done()
  129. }
  130. )
  131. })
  132. it('should set a key with the first_name', function (done) {
  133. return this.ConnectedUsersManager.updateUserPosition(
  134. this.project_id,
  135. this.client_id,
  136. this.user,
  137. null,
  138. err => {
  139. if (err) return done(err)
  140. this.rClient.hset
  141. .calledWith(
  142. `connected_user:${this.project_id}:${this.client_id}`,
  143. 'first_name',
  144. this.user.first_name
  145. )
  146. .should.equal(true)
  147. return done()
  148. }
  149. )
  150. })
  151. it('should set a key with the last_name', function (done) {
  152. return this.ConnectedUsersManager.updateUserPosition(
  153. this.project_id,
  154. this.client_id,
  155. this.user,
  156. null,
  157. err => {
  158. if (err) return done(err)
  159. this.rClient.hset
  160. .calledWith(
  161. `connected_user:${this.project_id}:${this.client_id}`,
  162. 'last_name',
  163. this.user.last_name
  164. )
  165. .should.equal(true)
  166. return done()
  167. }
  168. )
  169. })
  170. it('should set a key with the email', function (done) {
  171. return this.ConnectedUsersManager.updateUserPosition(
  172. this.project_id,
  173. this.client_id,
  174. this.user,
  175. null,
  176. err => {
  177. if (err) return done(err)
  178. this.rClient.hset
  179. .calledWith(
  180. `connected_user:${this.project_id}:${this.client_id}`,
  181. 'email',
  182. this.user.email
  183. )
  184. .should.equal(true)
  185. return done()
  186. }
  187. )
  188. })
  189. it('should push the client_id on to the project list', function (done) {
  190. return this.ConnectedUsersManager.updateUserPosition(
  191. this.project_id,
  192. this.client_id,
  193. this.user,
  194. null,
  195. err => {
  196. if (err) return done(err)
  197. this.rClient.sadd
  198. .calledWith(`clients_in_project:${this.project_id}`, this.client_id)
  199. .should.equal(true)
  200. return done()
  201. }
  202. )
  203. })
  204. it('should add a ttl to the project set so it stays clean', function (done) {
  205. return this.ConnectedUsersManager.updateUserPosition(
  206. this.project_id,
  207. this.client_id,
  208. this.user,
  209. null,
  210. err => {
  211. if (err) return done(err)
  212. this.rClient.expire
  213. .calledWith(
  214. `clients_in_project:${this.project_id}`,
  215. 24 * 4 * 60 * 60
  216. )
  217. .should.equal(true)
  218. return done()
  219. }
  220. )
  221. })
  222. it('should add a ttl to the connected user so it stays clean', function (done) {
  223. return this.ConnectedUsersManager.updateUserPosition(
  224. this.project_id,
  225. this.client_id,
  226. this.user,
  227. null,
  228. err => {
  229. if (err) return done(err)
  230. this.rClient.expire
  231. .calledWith(
  232. `connected_user:${this.project_id}:${this.client_id}`,
  233. 60 * 15
  234. )
  235. .should.equal(true)
  236. return done()
  237. }
  238. )
  239. })
  240. it('should set the cursor position when provided', function (done) {
  241. return this.ConnectedUsersManager.updateUserPosition(
  242. this.project_id,
  243. this.client_id,
  244. this.user,
  245. this.cursorData,
  246. err => {
  247. if (err) return done(err)
  248. this.rClient.hset
  249. .calledWith(
  250. `connected_user:${this.project_id}:${this.client_id}`,
  251. 'cursorData',
  252. JSON.stringify(this.cursorData)
  253. )
  254. .should.equal(true)
  255. return done()
  256. }
  257. )
  258. })
  259. describe('editing_session_mode', function () {
  260. const cases = {
  261. 'should bump the metric when connecting to empty room': {
  262. nConnectedClients: 1,
  263. cursorData: null,
  264. labels: {
  265. mode: 'connect',
  266. status: 'single',
  267. },
  268. },
  269. 'should bump the metric when connecting to non-empty room': {
  270. nConnectedClients: 2,
  271. cursorData: null,
  272. labels: {
  273. mode: 'connect',
  274. status: 'multi',
  275. },
  276. },
  277. 'should bump the metric when updating in empty room': {
  278. nConnectedClients: 1,
  279. cursorData: { row: 42 },
  280. labels: {
  281. mode: 'update',
  282. status: 'single',
  283. },
  284. },
  285. 'should bump the metric when updating in non-empty room': {
  286. nConnectedClients: 2,
  287. cursorData: { row: 42 },
  288. labels: {
  289. mode: 'update',
  290. status: 'multi',
  291. },
  292. },
  293. }
  294. for (const [
  295. name,
  296. { nConnectedClients, cursorData, labels },
  297. ] of Object.entries(cases)) {
  298. it(name, function (done) {
  299. this.rClient.exec.yields(null, [1, nConnectedClients])
  300. this.ConnectedUsersManager.updateUserPosition(
  301. this.project_id,
  302. this.client_id,
  303. this.user,
  304. cursorData,
  305. err => {
  306. if (err) return done(err)
  307. expect(this.Metrics.inc).to.have.been.calledWith(
  308. 'editing_session_mode',
  309. 1,
  310. labels
  311. )
  312. done()
  313. }
  314. )
  315. })
  316. }
  317. })
  318. })
  319. describe('markUserAsDisconnected', function () {
  320. beforeEach(function () {
  321. this.rClient.exec.yields(null, [1, 0])
  322. })
  323. it('should remove the user from the set', function (done) {
  324. return this.ConnectedUsersManager.markUserAsDisconnected(
  325. this.project_id,
  326. this.client_id,
  327. err => {
  328. if (err) return done(err)
  329. this.rClient.srem
  330. .calledWith(`clients_in_project:${this.project_id}`, this.client_id)
  331. .should.equal(true)
  332. return done()
  333. }
  334. )
  335. })
  336. it('should delete the connected_user string', function (done) {
  337. return this.ConnectedUsersManager.markUserAsDisconnected(
  338. this.project_id,
  339. this.client_id,
  340. err => {
  341. if (err) return done(err)
  342. this.rClient.del
  343. .calledWith(`connected_user:${this.project_id}:${this.client_id}`)
  344. .should.equal(true)
  345. return done()
  346. }
  347. )
  348. })
  349. it('should add a ttl to the connected user set so it stays clean', function (done) {
  350. return this.ConnectedUsersManager.markUserAsDisconnected(
  351. this.project_id,
  352. this.client_id,
  353. err => {
  354. if (err) return done(err)
  355. this.rClient.expire
  356. .calledWith(
  357. `clients_in_project:${this.project_id}`,
  358. 24 * 4 * 60 * 60
  359. )
  360. .should.equal(true)
  361. return done()
  362. }
  363. )
  364. })
  365. describe('editing_session_mode', function () {
  366. const cases = {
  367. 'should bump the metric when disconnecting from now empty room': {
  368. nConnectedClients: 0,
  369. labels: {
  370. mode: 'disconnect',
  371. status: 'empty',
  372. },
  373. },
  374. 'should bump the metric when disconnecting from now single room': {
  375. nConnectedClients: 1,
  376. labels: {
  377. mode: 'disconnect',
  378. status: 'single',
  379. },
  380. },
  381. 'should bump the metric when disconnecting from now multi room': {
  382. nConnectedClients: 2,
  383. labels: {
  384. mode: 'disconnect',
  385. status: 'multi',
  386. },
  387. },
  388. }
  389. for (const [name, { nConnectedClients, labels }] of Object.entries(
  390. cases
  391. )) {
  392. it(name, function (done) {
  393. this.rClient.exec.yields(null, [1, nConnectedClients])
  394. this.ConnectedUsersManager.markUserAsDisconnected(
  395. this.project_id,
  396. this.client_id,
  397. err => {
  398. if (err) return done(err)
  399. expect(this.Metrics.inc).to.have.been.calledWith(
  400. 'editing_session_mode',
  401. 1,
  402. labels
  403. )
  404. done()
  405. }
  406. )
  407. })
  408. }
  409. })
  410. describe('projectNotEmptySince', function () {
  411. it('should clear the projectNotEmptySince key when empty and skip metric if not set', function (done) {
  412. this.rClient.exec.yields(null, [1, 0])
  413. this.rClient.getdel.yields(null, '')
  414. this.ConnectedUsersManager.markUserAsDisconnected(
  415. this.project_id,
  416. this.client_id,
  417. err => {
  418. if (err) return done(err)
  419. expect(this.rClient.getdel).to.have.been.calledWith(
  420. `projectNotEmptySince:{${this.project_id}}`
  421. )
  422. expect(this.Metrics.histogram).to.not.have.been.called
  423. done()
  424. }
  425. )
  426. })
  427. it('should clear the projectNotEmptySince key when empty and record metric if set', function (done) {
  428. this.rClient.exec.yields(null, [1, 0])
  429. tk.freeze(1_234_000)
  430. this.rClient.getdel.yields(null, '1230')
  431. this.ConnectedUsersManager.markUserAsDisconnected(
  432. this.project_id,
  433. this.client_id,
  434. err => {
  435. if (err) return done(err)
  436. expect(this.rClient.getdel).to.have.been.calledWith(
  437. `projectNotEmptySince:{${this.project_id}}`
  438. )
  439. expect(this.Metrics.histogram).to.have.been.calledWith(
  440. 'project_not_empty_since',
  441. 4,
  442. sinon.match.any,
  443. { status: 'empty' }
  444. )
  445. done()
  446. }
  447. )
  448. })
  449. it('should set projectNotEmptySince key when single and skip metric if not set before', function (done) {
  450. this.rClient.exec.yields(null, [1, 1])
  451. tk.freeze(1_233_001) // should ceil up
  452. this.rClient.set.yields(null, '')
  453. this.ConnectedUsersManager.markUserAsDisconnected(
  454. this.project_id,
  455. this.client_id,
  456. err => {
  457. if (err) return done(err)
  458. expect(this.rClient.set).to.have.been.calledWith(
  459. `projectNotEmptySince:{${this.project_id}}`,
  460. '1234',
  461. 'NX',
  462. 'GET',
  463. 'EX',
  464. 31 * 24 * 60 * 60
  465. )
  466. expect(this.Metrics.histogram).to.not.have.been.called
  467. done()
  468. }
  469. )
  470. })
  471. const cases = {
  472. 'should set projectNotEmptySince key when single and record metric if set before':
  473. {
  474. nConnectedClients: 1,
  475. labels: {
  476. status: 'single',
  477. },
  478. },
  479. 'should set projectNotEmptySince key when multi and record metric if set before':
  480. {
  481. nConnectedClients: 2,
  482. labels: {
  483. status: 'multi',
  484. },
  485. },
  486. }
  487. for (const [name, { nConnectedClients, labels }] of Object.entries(
  488. cases
  489. )) {
  490. it(name, function (done) {
  491. this.rClient.exec.yields(null, [1, nConnectedClients])
  492. tk.freeze(1_235_000)
  493. this.rClient.set.yields(null, '1230')
  494. this.ConnectedUsersManager.markUserAsDisconnected(
  495. this.project_id,
  496. this.client_id,
  497. err => {
  498. if (err) return done(err)
  499. expect(this.rClient.set).to.have.been.calledWith(
  500. `projectNotEmptySince:{${this.project_id}}`,
  501. '1235',
  502. 'NX',
  503. 'GET',
  504. 'EX',
  505. 31 * 24 * 60 * 60
  506. )
  507. expect(this.Metrics.histogram).to.have.been.calledWith(
  508. 'project_not_empty_since',
  509. 5,
  510. sinon.match.any,
  511. labels
  512. )
  513. done()
  514. }
  515. )
  516. })
  517. }
  518. })
  519. })
  520. describe('_getConnectedUser', function () {
  521. it('should return a connected user if there is a user object', function (done) {
  522. const cursorData = JSON.stringify({ cursorData: { row: 1 } })
  523. this.rClient.hgetall.callsArgWith(1, null, {
  524. connected_at: new Date(),
  525. user_id: this.user._id,
  526. last_updated_at: `${Date.now()}`,
  527. cursorData,
  528. })
  529. return this.ConnectedUsersManager._getConnectedUser(
  530. this.project_id,
  531. this.client_id,
  532. (err, result) => {
  533. if (err) return done(err)
  534. result.connected.should.equal(true)
  535. result.client_id.should.equal(this.client_id)
  536. return done()
  537. }
  538. )
  539. })
  540. it('should return a not connected user if there is no object', function (done) {
  541. this.rClient.hgetall.callsArgWith(1, null, null)
  542. return this.ConnectedUsersManager._getConnectedUser(
  543. this.project_id,
  544. this.client_id,
  545. (err, result) => {
  546. if (err) return done(err)
  547. result.connected.should.equal(false)
  548. result.client_id.should.equal(this.client_id)
  549. return done()
  550. }
  551. )
  552. })
  553. return it('should return a not connected user if there is an empty object', function (done) {
  554. this.rClient.hgetall.callsArgWith(1, null, {})
  555. return this.ConnectedUsersManager._getConnectedUser(
  556. this.project_id,
  557. this.client_id,
  558. (err, result) => {
  559. if (err) return done(err)
  560. result.connected.should.equal(false)
  561. result.client_id.should.equal(this.client_id)
  562. return done()
  563. }
  564. )
  565. })
  566. })
  567. return describe('getConnectedUsers', function () {
  568. beforeEach(function () {
  569. this.users = ['1234', '5678', '9123', '8234']
  570. this.rClient.smembers.callsArgWith(1, null, this.users)
  571. this.ConnectedUsersManager._getConnectedUser = sinon.stub()
  572. this.ConnectedUsersManager._getConnectedUser
  573. .withArgs(this.project_id, this.users[0])
  574. .callsArgWith(2, null, {
  575. connected: true,
  576. client_age: 2,
  577. client_id: this.users[0],
  578. })
  579. this.ConnectedUsersManager._getConnectedUser
  580. .withArgs(this.project_id, this.users[1])
  581. .callsArgWith(2, null, {
  582. connected: false,
  583. client_age: 1,
  584. client_id: this.users[1],
  585. })
  586. this.ConnectedUsersManager._getConnectedUser
  587. .withArgs(this.project_id, this.users[2])
  588. .callsArgWith(2, null, {
  589. connected: true,
  590. client_age: 3,
  591. client_id: this.users[2],
  592. })
  593. return this.ConnectedUsersManager._getConnectedUser
  594. .withArgs(this.project_id, this.users[3])
  595. .callsArgWith(2, null, {
  596. connected: true,
  597. client_age: 11,
  598. client_id: this.users[3],
  599. })
  600. }) // connected but old
  601. return it('should only return the users in the list which are still in redis and recently updated', function (done) {
  602. return this.ConnectedUsersManager.getConnectedUsers(
  603. this.project_id,
  604. (err, users) => {
  605. if (err) return done(err)
  606. users.length.should.equal(2)
  607. users[0].should.deep.equal({
  608. client_id: this.users[0],
  609. client_age: 2,
  610. connected: true,
  611. })
  612. users[1].should.deep.equal({
  613. client_id: this.users[2],
  614. client_age: 3,
  615. connected: true,
  616. })
  617. return done()
  618. }
  619. )
  620. })
  621. })
  622. })