UpdatesManagerTests.js 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333
  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. * DS101: Remove unnecessary use of Array.from
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS207: Consider shorter variations of null checks
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. const sinon = require('sinon')
  15. const { expect } = require('chai')
  16. const { ObjectId } = require('mongodb')
  17. const modulePath = '../../../../app/js/UpdatesManager.js'
  18. const SandboxedModule = require('sandboxed-module')
  19. describe('UpdatesManager', function () {
  20. beforeEach(function () {
  21. this.UpdatesManager = SandboxedModule.require(modulePath, {
  22. singleOnly: true,
  23. requires: {
  24. './UpdateCompressor': (this.UpdateCompressor = {}),
  25. './MongoManager': (this.MongoManager = {}),
  26. './PackManager': (this.PackManager = {}),
  27. './RedisManager': (this.RedisManager = {}),
  28. './LockManager': (this.LockManager = {}),
  29. './WebApiManager': (this.WebApiManager = {}),
  30. './UpdateTrimmer': (this.UpdateTrimmer = {}),
  31. './DocArchiveManager': (this.DocArchiveManager = {}),
  32. '@overleaf/settings': {
  33. redis: {
  34. lock: {
  35. key_schema: {
  36. historyLock({ doc_id: docId }) {
  37. return `HistoryLock:${docId}`
  38. },
  39. },
  40. },
  41. },
  42. },
  43. },
  44. })
  45. this.doc_id = 'doc-id-123'
  46. this.project_id = 'project-id-123'
  47. this.callback = sinon.stub()
  48. return (this.temporary = 'temp-mock')
  49. })
  50. describe('compressAndSaveRawUpdates', function () {
  51. describe('when there are no raw ops', function () {
  52. beforeEach(function () {
  53. this.MongoManager.peekLastCompressedUpdate = sinon.stub()
  54. return this.UpdatesManager.compressAndSaveRawUpdates(
  55. this.project_id,
  56. this.doc_id,
  57. [],
  58. this.temporary,
  59. this.callback
  60. )
  61. })
  62. it('should not need to access the database', function () {
  63. return this.MongoManager.peekLastCompressedUpdate.called.should.equal(
  64. false
  65. )
  66. })
  67. return it('should call the callback', function () {
  68. return this.callback.called.should.equal(true)
  69. })
  70. })
  71. describe('when there is no compressed history to begin with', function () {
  72. beforeEach(function () {
  73. this.rawUpdates = [
  74. { v: 12, op: 'mock-op-12' },
  75. { v: 13, op: 'mock-op-13' },
  76. ]
  77. this.compressedUpdates = [{ v: 13, op: 'compressed-op-12' }]
  78. this.MongoManager.peekLastCompressedUpdate = sinon
  79. .stub()
  80. .callsArgWith(1, null, null)
  81. this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5)
  82. this.UpdateCompressor.compressRawUpdates = sinon
  83. .stub()
  84. .returns(this.compressedUpdates)
  85. return this.UpdatesManager.compressAndSaveRawUpdates(
  86. this.project_id,
  87. this.doc_id,
  88. this.rawUpdates,
  89. this.temporary,
  90. this.callback
  91. )
  92. })
  93. it('should look at the last compressed op', function () {
  94. return this.MongoManager.peekLastCompressedUpdate
  95. .calledWith(this.doc_id)
  96. .should.equal(true)
  97. })
  98. it('should save the compressed ops as a pack', function () {
  99. return this.PackManager.insertCompressedUpdates
  100. .calledWith(
  101. this.project_id,
  102. this.doc_id,
  103. null,
  104. this.compressedUpdates,
  105. this.temporary
  106. )
  107. .should.equal(true)
  108. })
  109. return it('should call the callback', function () {
  110. return this.callback.called.should.equal(true)
  111. })
  112. })
  113. describe('when the raw ops need appending to existing history', function () {
  114. beforeEach(function () {
  115. this.lastCompressedUpdate = { v: 11, op: 'compressed-op-11' }
  116. this.compressedUpdates = [
  117. { v: 12, op: 'compressed-op-11+12' },
  118. { v: 13, op: 'compressed-op-12' },
  119. ]
  120. this.MongoManager.peekLastCompressedUpdate = sinon
  121. .stub()
  122. .callsArgWith(
  123. 1,
  124. null,
  125. this.lastCompressedUpdate,
  126. this.lastCompressedUpdate.v
  127. )
  128. this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5)
  129. return (this.UpdateCompressor.compressRawUpdates = sinon
  130. .stub()
  131. .returns(this.compressedUpdates))
  132. })
  133. describe('when the raw ops start where the existing history ends', function () {
  134. beforeEach(function () {
  135. this.rawUpdates = [
  136. { v: 12, op: 'mock-op-12' },
  137. { v: 13, op: 'mock-op-13' },
  138. ]
  139. return this.UpdatesManager.compressAndSaveRawUpdates(
  140. this.project_id,
  141. this.doc_id,
  142. this.rawUpdates,
  143. this.temporary,
  144. this.callback
  145. )
  146. })
  147. it('should look at the last compressed op', function () {
  148. return this.MongoManager.peekLastCompressedUpdate
  149. .calledWith(this.doc_id)
  150. .should.equal(true)
  151. })
  152. it('should compress the raw ops', function () {
  153. return this.UpdateCompressor.compressRawUpdates
  154. .calledWith(null, this.rawUpdates)
  155. .should.equal(true)
  156. })
  157. it('should save the new compressed ops into a pack', function () {
  158. return this.PackManager.insertCompressedUpdates
  159. .calledWith(
  160. this.project_id,
  161. this.doc_id,
  162. this.lastCompressedUpdate,
  163. this.compressedUpdates,
  164. this.temporary
  165. )
  166. .should.equal(true)
  167. })
  168. return it('should call the callback', function () {
  169. return this.callback.called.should.equal(true)
  170. })
  171. })
  172. describe('when the raw ops start where the existing history ends and the history is in a pack', function () {
  173. beforeEach(function () {
  174. this.lastCompressedUpdate = {
  175. pack: [{ v: 11, op: 'compressed-op-11' }],
  176. v: 11,
  177. }
  178. this.rawUpdates = [
  179. { v: 12, op: 'mock-op-12' },
  180. { v: 13, op: 'mock-op-13' },
  181. ]
  182. this.MongoManager.peekLastCompressedUpdate = sinon
  183. .stub()
  184. .callsArgWith(
  185. 1,
  186. null,
  187. this.lastCompressedUpdate,
  188. this.lastCompressedUpdate.v
  189. )
  190. return this.UpdatesManager.compressAndSaveRawUpdates(
  191. this.project_id,
  192. this.doc_id,
  193. this.rawUpdates,
  194. this.temporary,
  195. this.callback
  196. )
  197. })
  198. it('should look at the last compressed op', function () {
  199. return this.MongoManager.peekLastCompressedUpdate
  200. .calledWith(this.doc_id)
  201. .should.equal(true)
  202. })
  203. it('should compress the raw ops', function () {
  204. return this.UpdateCompressor.compressRawUpdates
  205. .calledWith(null, this.rawUpdates)
  206. .should.equal(true)
  207. })
  208. it('should save the new compressed ops into a pack', function () {
  209. return this.PackManager.insertCompressedUpdates
  210. .calledWith(
  211. this.project_id,
  212. this.doc_id,
  213. this.lastCompressedUpdate,
  214. this.compressedUpdates,
  215. this.temporary
  216. )
  217. .should.equal(true)
  218. })
  219. return it('should call the callback', function () {
  220. return this.callback.called.should.equal(true)
  221. })
  222. })
  223. describe('when some raw ops are passed that have already been compressed', function () {
  224. beforeEach(function () {
  225. this.rawUpdates = [
  226. { v: 10, op: 'mock-op-10' },
  227. { v: 11, op: 'mock-op-11' },
  228. { v: 12, op: 'mock-op-12' },
  229. { v: 13, op: 'mock-op-13' },
  230. ]
  231. return this.UpdatesManager.compressAndSaveRawUpdates(
  232. this.project_id,
  233. this.doc_id,
  234. this.rawUpdates,
  235. this.temporary,
  236. this.callback
  237. )
  238. })
  239. return it('should only compress the more recent raw ops', function () {
  240. return this.UpdateCompressor.compressRawUpdates
  241. .calledWith(null, this.rawUpdates.slice(-2))
  242. .should.equal(true)
  243. })
  244. })
  245. describe('when the raw ops do not follow from the last compressed op version', function () {
  246. beforeEach(function () {
  247. this.rawUpdates = [{ v: 13, op: 'mock-op-13' }]
  248. return this.UpdatesManager.compressAndSaveRawUpdates(
  249. this.project_id,
  250. this.doc_id,
  251. this.rawUpdates,
  252. this.temporary,
  253. this.callback
  254. )
  255. })
  256. it('should call the callback with an error', function () {
  257. return this.callback
  258. .calledWith(
  259. sinon.match.has(
  260. 'message',
  261. 'Tried to apply raw op at version 13 to last compressed update with version 11 from unknown time'
  262. )
  263. )
  264. .should.equal(true)
  265. })
  266. return it('should not insert any update into mongo', function () {
  267. return this.PackManager.insertCompressedUpdates.called.should.equal(
  268. false
  269. )
  270. })
  271. })
  272. return describe('when the raw ops are out of order', function () {
  273. beforeEach(function () {
  274. this.rawUpdates = [
  275. { v: 13, op: 'mock-op-13' },
  276. { v: 12, op: 'mock-op-12' },
  277. ]
  278. return this.UpdatesManager.compressAndSaveRawUpdates(
  279. this.project_id,
  280. this.doc_id,
  281. this.rawUpdates,
  282. this.temporary,
  283. this.callback
  284. )
  285. })
  286. it('should call the callback with an error', function () {
  287. return this.callback
  288. .calledWith(sinon.match.has('message'))
  289. .should.equal(true)
  290. })
  291. return it('should not insert any update into mongo', function () {
  292. return this.PackManager.insertCompressedUpdates.called.should.equal(
  293. false
  294. )
  295. })
  296. })
  297. })
  298. return describe('when the raw ops need appending to existing history which is in S3', function () {
  299. beforeEach(function () {
  300. this.lastCompressedUpdate = null
  301. this.lastVersion = 11
  302. this.compressedUpdates = [{ v: 13, op: 'compressed-op-12' }]
  303. this.MongoManager.peekLastCompressedUpdate = sinon
  304. .stub()
  305. .callsArgWith(1, null, null, this.lastVersion)
  306. this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5)
  307. return (this.UpdateCompressor.compressRawUpdates = sinon
  308. .stub()
  309. .returns(this.compressedUpdates))
  310. })
  311. return describe('when the raw ops start where the existing history ends', function () {
  312. beforeEach(function () {
  313. this.rawUpdates = [
  314. { v: 12, op: 'mock-op-12' },
  315. { v: 13, op: 'mock-op-13' },
  316. ]
  317. return this.UpdatesManager.compressAndSaveRawUpdates(
  318. this.project_id,
  319. this.doc_id,
  320. this.rawUpdates,
  321. this.temporary,
  322. this.callback
  323. )
  324. })
  325. it('should try to look at the last compressed op', function () {
  326. return this.MongoManager.peekLastCompressedUpdate
  327. .calledWith(this.doc_id)
  328. .should.equal(true)
  329. })
  330. it('should compress the last compressed op and the raw ops', function () {
  331. return this.UpdateCompressor.compressRawUpdates
  332. .calledWith(this.lastCompressedUpdate, this.rawUpdates)
  333. .should.equal(true)
  334. })
  335. it('should save the compressed ops', function () {
  336. return this.PackManager.insertCompressedUpdates
  337. .calledWith(
  338. this.project_id,
  339. this.doc_id,
  340. null,
  341. this.compressedUpdates,
  342. this.temporary
  343. )
  344. .should.equal(true)
  345. })
  346. return it('should call the callback', function () {
  347. return this.callback.called.should.equal(true)
  348. })
  349. })
  350. })
  351. })
  352. describe('processUncompressedUpdates', function () {
  353. beforeEach(function () {
  354. this.UpdatesManager.compressAndSaveRawUpdates = sinon
  355. .stub()
  356. .callsArgWith(4)
  357. this.RedisManager.deleteAppliedDocUpdates = sinon.stub().callsArg(3)
  358. this.MongoManager.backportProjectId = sinon.stub().callsArg(2)
  359. return (this.UpdateTrimmer.shouldTrimUpdates = sinon
  360. .stub()
  361. .callsArgWith(1, null, (this.temporary = 'temp mock')))
  362. })
  363. describe('when there is fewer than one batch to send', function () {
  364. beforeEach(function () {
  365. this.updates = ['mock-update']
  366. this.RedisManager.getOldestDocUpdates = sinon
  367. .stub()
  368. .callsArgWith(2, null, this.updates)
  369. this.RedisManager.expandDocUpdates = sinon
  370. .stub()
  371. .callsArgWith(1, null, this.updates)
  372. return this.UpdatesManager.processUncompressedUpdates(
  373. this.project_id,
  374. this.doc_id,
  375. this.temporary,
  376. this.callback
  377. )
  378. })
  379. it('should get the oldest updates', function () {
  380. return this.RedisManager.getOldestDocUpdates
  381. .calledWith(this.doc_id, this.UpdatesManager.REDIS_READ_BATCH_SIZE)
  382. .should.equal(true)
  383. })
  384. it('should compress and save the updates', function () {
  385. return this.UpdatesManager.compressAndSaveRawUpdates
  386. .calledWith(
  387. this.project_id,
  388. this.doc_id,
  389. this.updates,
  390. this.temporary
  391. )
  392. .should.equal(true)
  393. })
  394. it('should delete the batch of uncompressed updates that was just processed', function () {
  395. return this.RedisManager.deleteAppliedDocUpdates
  396. .calledWith(this.project_id, this.doc_id, this.updates)
  397. .should.equal(true)
  398. })
  399. return it('should call the callback', function () {
  400. return this.callback.called.should.equal(true)
  401. })
  402. })
  403. return describe('when there are multiple batches to send', function () {
  404. beforeEach(function (done) {
  405. this.UpdatesManager.REDIS_READ_BATCH_SIZE = 2
  406. this.updates = [
  407. 'mock-update-0',
  408. 'mock-update-1',
  409. 'mock-update-2',
  410. 'mock-update-3',
  411. 'mock-update-4',
  412. ]
  413. this.redisArray = this.updates.slice()
  414. this.RedisManager.getOldestDocUpdates = (
  415. docId,
  416. batchSize,
  417. callback
  418. ) => {
  419. if (callback == null) {
  420. callback = function () {}
  421. }
  422. const updates = this.redisArray.slice(0, batchSize)
  423. this.redisArray = this.redisArray.slice(batchSize)
  424. return callback(null, updates)
  425. }
  426. sinon.spy(this.RedisManager, 'getOldestDocUpdates')
  427. this.RedisManager.expandDocUpdates = (jsonUpdates, callback) => {
  428. return callback(null, jsonUpdates)
  429. }
  430. sinon.spy(this.RedisManager, 'expandDocUpdates')
  431. return this.UpdatesManager.processUncompressedUpdates(
  432. this.project_id,
  433. this.doc_id,
  434. this.temporary,
  435. (...args) => {
  436. this.callback(...Array.from(args || []))
  437. return done()
  438. }
  439. )
  440. })
  441. it('should get the oldest updates in three batches ', function () {
  442. return this.RedisManager.getOldestDocUpdates.callCount.should.equal(3)
  443. })
  444. it('should compress and save the updates in batches', function () {
  445. this.UpdatesManager.compressAndSaveRawUpdates
  446. .calledWith(
  447. this.project_id,
  448. this.doc_id,
  449. this.updates.slice(0, 2),
  450. this.temporary
  451. )
  452. .should.equal(true)
  453. this.UpdatesManager.compressAndSaveRawUpdates
  454. .calledWith(
  455. this.project_id,
  456. this.doc_id,
  457. this.updates.slice(2, 4),
  458. this.temporary
  459. )
  460. .should.equal(true)
  461. return this.UpdatesManager.compressAndSaveRawUpdates
  462. .calledWith(
  463. this.project_id,
  464. this.doc_id,
  465. this.updates.slice(4, 5),
  466. this.temporary
  467. )
  468. .should.equal(true)
  469. })
  470. it('should delete the batches of uncompressed updates', function () {
  471. return this.RedisManager.deleteAppliedDocUpdates.callCount.should.equal(
  472. 3
  473. )
  474. })
  475. return it('should call the callback', function () {
  476. return this.callback.called.should.equal(true)
  477. })
  478. })
  479. })
  480. describe('processCompressedUpdatesWithLock', function () {
  481. beforeEach(function () {
  482. this.UpdateTrimmer.shouldTrimUpdates = sinon
  483. .stub()
  484. .callsArgWith(1, null, (this.temporary = 'temp mock'))
  485. this.MongoManager.backportProjectId = sinon.stub().callsArg(2)
  486. this.UpdatesManager._processUncompressedUpdates = sinon.stub().callsArg(3)
  487. this.LockManager.runWithLock = sinon.stub().callsArg(2)
  488. return this.UpdatesManager.processUncompressedUpdatesWithLock(
  489. this.project_id,
  490. this.doc_id,
  491. this.callback
  492. )
  493. })
  494. it('should check if the updates are temporary', function () {
  495. return this.UpdateTrimmer.shouldTrimUpdates
  496. .calledWith(this.project_id)
  497. .should.equal(true)
  498. })
  499. it('should backport the project id', function () {
  500. return this.MongoManager.backportProjectId
  501. .calledWith(this.project_id, this.doc_id)
  502. .should.equal(true)
  503. })
  504. it('should run processUncompressedUpdates with the lock', function () {
  505. return this.LockManager.runWithLock
  506. .calledWith(`HistoryLock:${this.doc_id}`)
  507. .should.equal(true)
  508. })
  509. return it('should call the callback', function () {
  510. return this.callback.called.should.equal(true)
  511. })
  512. })
  513. describe('getDocUpdates', function () {
  514. beforeEach(function () {
  515. this.updates = ['mock-updates']
  516. this.options = { to: 'mock-to', limit: 'mock-limit' }
  517. this.PackManager.getOpsByVersionRange = sinon
  518. .stub()
  519. .callsArgWith(4, null, this.updates)
  520. this.UpdatesManager.processUncompressedUpdatesWithLock = sinon
  521. .stub()
  522. .callsArg(2)
  523. return this.UpdatesManager.getDocUpdates(
  524. this.project_id,
  525. this.doc_id,
  526. this.options,
  527. this.callback
  528. )
  529. })
  530. it('should process outstanding updates', function () {
  531. return this.UpdatesManager.processUncompressedUpdatesWithLock
  532. .calledWith(this.project_id, this.doc_id)
  533. .should.equal(true)
  534. })
  535. it('should get the updates from the database', function () {
  536. return this.PackManager.getOpsByVersionRange
  537. .calledWith(
  538. this.project_id,
  539. this.doc_id,
  540. this.options.from,
  541. this.options.to
  542. )
  543. .should.equal(true)
  544. })
  545. return it('should return the updates', function () {
  546. return this.callback.calledWith(null, this.updates).should.equal(true)
  547. })
  548. })
  549. describe('getDocUpdatesWithUserInfo', function () {
  550. beforeEach(function () {
  551. this.updates = ['mock-updates']
  552. this.options = { to: 'mock-to', limit: 'mock-limit' }
  553. this.updatesWithUserInfo = ['updates-with-user-info']
  554. this.UpdatesManager.getDocUpdates = sinon
  555. .stub()
  556. .callsArgWith(3, null, this.updates)
  557. this.UpdatesManager.fillUserInfo = sinon
  558. .stub()
  559. .callsArgWith(1, null, this.updatesWithUserInfo)
  560. return this.UpdatesManager.getDocUpdatesWithUserInfo(
  561. this.project_id,
  562. this.doc_id,
  563. this.options,
  564. this.callback
  565. )
  566. })
  567. it('should get the updates', function () {
  568. return this.UpdatesManager.getDocUpdates
  569. .calledWith(this.project_id, this.doc_id, this.options)
  570. .should.equal(true)
  571. })
  572. it('should file the updates with the user info', function () {
  573. return this.UpdatesManager.fillUserInfo
  574. .calledWith(this.updates)
  575. .should.equal(true)
  576. })
  577. return it('should return the updates with the filled details', function () {
  578. return this.callback
  579. .calledWith(null, this.updatesWithUserInfo)
  580. .should.equal(true)
  581. })
  582. })
  583. describe('processUncompressedUpdatesForProject', function () {
  584. beforeEach(function (done) {
  585. this.doc_ids = ['mock-id-1', 'mock-id-2']
  586. this.UpdateTrimmer.shouldTrimUpdates = sinon
  587. .stub()
  588. .callsArgWith(1, null, (this.temporary = 'temp mock'))
  589. this.MongoManager.backportProjectId = sinon.stub().callsArg(2)
  590. this.UpdatesManager._processUncompressedUpdatesForDocWithLock = sinon
  591. .stub()
  592. .callsArg(3)
  593. this.RedisManager.getDocIdsWithHistoryOps = sinon
  594. .stub()
  595. .callsArgWith(1, null, this.doc_ids)
  596. return this.UpdatesManager.processUncompressedUpdatesForProject(
  597. this.project_id,
  598. () => {
  599. this.callback()
  600. return done()
  601. }
  602. )
  603. })
  604. it('should get all the docs with history ops', function () {
  605. return this.RedisManager.getDocIdsWithHistoryOps
  606. .calledWith(this.project_id)
  607. .should.equal(true)
  608. })
  609. it('should process the doc ops for the each doc_id', function () {
  610. return Array.from(this.doc_ids).map(docId =>
  611. this.UpdatesManager._processUncompressedUpdatesForDocWithLock
  612. .calledWith(this.project_id, docId, this.temporary)
  613. .should.equal(true)
  614. )
  615. })
  616. return it('should call the callback', function () {
  617. return this.callback.called.should.equal(true)
  618. })
  619. })
  620. describe('getSummarizedProjectUpdates', function () {
  621. beforeEach(function () {
  622. this.updates = [
  623. {
  624. doc_id: 123,
  625. v: 456,
  626. op: 'mock-updates',
  627. meta: { user_id: 123, start_ts: 1233, end_ts: 1234 },
  628. },
  629. ]
  630. this.options = { before: 'mock-before', limit: 'mock-limit' }
  631. this.summarizedUpdates = [
  632. {
  633. meta: { user_ids: [123], start_ts: 1233, end_ts: 1234 },
  634. docs: { 123: { fromV: 456, toV: 456 } },
  635. },
  636. ]
  637. this.updatesWithUserInfo = ['updates-with-user-info']
  638. this.done_state = false
  639. this.iterator = {
  640. next: cb => {
  641. this.done_state = true
  642. return cb(null, this.updates)
  643. },
  644. done: () => {
  645. return this.done_state
  646. },
  647. }
  648. this.PackManager.makeProjectIterator = sinon
  649. .stub()
  650. .callsArgWith(2, null, this.iterator)
  651. this.UpdatesManager.processUncompressedUpdatesForProject = sinon
  652. .stub()
  653. .callsArg(1)
  654. this.UpdatesManager.fillSummarizedUserInfo = sinon
  655. .stub()
  656. .callsArgWith(1, null, this.updatesWithUserInfo)
  657. return this.UpdatesManager.getSummarizedProjectUpdates(
  658. this.project_id,
  659. this.options,
  660. this.callback
  661. )
  662. })
  663. it('should process any outstanding updates', function () {
  664. return this.UpdatesManager.processUncompressedUpdatesForProject
  665. .calledWith(this.project_id)
  666. .should.equal(true)
  667. })
  668. it('should get the updates', function () {
  669. return this.PackManager.makeProjectIterator
  670. .calledWith(this.project_id, this.options.before)
  671. .should.equal(true)
  672. })
  673. it('should fill the updates with the user info', function () {
  674. return this.UpdatesManager.fillSummarizedUserInfo
  675. .calledWith(this.summarizedUpdates)
  676. .should.equal(true)
  677. })
  678. return it('should return the updates with the filled details', function () {
  679. return this.callback
  680. .calledWith(null, this.updatesWithUserInfo)
  681. .should.equal(true)
  682. })
  683. })
  684. // describe "_extendBatchOfSummarizedUpdates", ->
  685. // beforeEach ->
  686. // @before = Date.now()
  687. // @min_count = 2
  688. // @existingSummarizedUpdates = ["summarized-updates-3"]
  689. // @summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"]
  690. // describe "when there are updates to get", ->
  691. // beforeEach ->
  692. // @updates = [
  693. // {op: "mock-op-1", meta: end_ts: @before - 10},
  694. // {op: "mock-op-1", meta: end_ts: @nextBeforeTimestamp = @before - 20}
  695. // ]
  696. // @existingSummarizedUpdates = ["summarized-updates-3"]
  697. // @summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"]
  698. // @UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates)
  699. // @UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates)
  700. // @UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback
  701. // it "should get the updates", ->
  702. // @UpdatesManager.getProjectUpdatesWithUserInfo
  703. // .calledWith(@project_id, { before: @before, limit: 3 * @min_count })
  704. // .should.equal true
  705. // it "should summarize the updates", ->
  706. // @UpdatesManager._summarizeUpdates
  707. // .calledWith(@updates, @existingSummarizedUpdates)
  708. // .should.equal true
  709. // it "should call the callback with the summarized updates and the next before timestamp", ->
  710. // @callback.calledWith(null, @summarizedUpdates, @nextBeforeTimestamp).should.equal true
  711. // describe "when there are no more updates", ->
  712. // beforeEach ->
  713. // @updates = []
  714. // @UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates)
  715. // @UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates)
  716. // @UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback
  717. // it "should call the callback with the summarized updates and null for nextBeforeTimestamp", ->
  718. // @callback.calledWith(null, @summarizedUpdates, null).should.equal true
  719. // describe "getSummarizedProjectUpdates", ->
  720. // describe "when one batch of updates is enough to meet the limit", ->
  721. // beforeEach ->
  722. // @before = Date.now()
  723. // @min_count = 2
  724. // @updates = ["summarized-updates-3", "summarized-updates-2"]
  725. // @nextBeforeTimestamp = @before - 100
  726. // @UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, @nextBeforeTimestamp)
  727. // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
  728. // it "should get the batch of summarized updates", ->
  729. // @UpdatesManager._extendBatchOfSummarizedUpdates
  730. // .calledWith(@project_id, [], @before, @min_count)
  731. // .should.equal true
  732. // it "should call the callback with the updates", ->
  733. // @callback.calledWith(null, @updates, @nextBeforeTimestamp).should.equal true
  734. // describe "when multiple batches are needed to meet the limit", ->
  735. // beforeEach ->
  736. // @before = Date.now()
  737. // @min_count = 4
  738. // @firstBatch = [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }]
  739. // @nextBeforeTimestamp = @before - 100
  740. // @secondBatch = [{ toV: 4, fromV: 4 }, { toV: 3, fromV: 3 }]
  741. // @nextNextBeforeTimestamp = @before - 200
  742. // @UpdatesManager._extendBatchOfSummarizedUpdates = (project_id, existingUpdates, before, desiredLength, callback) =>
  743. // if existingUpdates.length == 0
  744. // callback null, @firstBatch, @nextBeforeTimestamp
  745. // else
  746. // callback null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp
  747. // sinon.spy @UpdatesManager, "_extendBatchOfSummarizedUpdates"
  748. // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
  749. // it "should get the first batch of summarized updates", ->
  750. // @UpdatesManager._extendBatchOfSummarizedUpdates
  751. // .calledWith(@project_id, [], @before, @min_count)
  752. // .should.equal true
  753. // it "should get the second batch of summarized updates", ->
  754. // @UpdatesManager._extendBatchOfSummarizedUpdates
  755. // .calledWith(@project_id, @firstBatch, @nextBeforeTimestamp, @min_count)
  756. // .should.equal true
  757. // it "should call the callback with all the updates", ->
  758. // @callback.calledWith(null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp).should.equal true
  759. // describe "when the end of the database is hit", ->
  760. // beforeEach ->
  761. // @before = Date.now()
  762. // @min_count = 4
  763. // @updates = [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }]
  764. // @UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, null)
  765. // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
  766. // it "should get the batch of summarized updates", ->
  767. // @UpdatesManager._extendBatchOfSummarizedUpdates
  768. // .calledWith(@project_id, [], @before, @min_count)
  769. // .should.equal true
  770. // it "should call the callback with the updates", ->
  771. // @callback.calledWith(null, @updates, null).should.equal true
  772. describe('fillUserInfo', function () {
  773. describe('with valid users', function () {
  774. beforeEach(function (done) {
  775. this.user_id_1 = ObjectId().toString()
  776. this.user_id_2 = ObjectId().toString()
  777. this.updates = [
  778. {
  779. meta: {
  780. user_id: this.user_id_1,
  781. },
  782. op: 'mock-op-1',
  783. },
  784. {
  785. meta: {
  786. user_id: this.user_id_1,
  787. },
  788. op: 'mock-op-2',
  789. },
  790. {
  791. meta: {
  792. user_id: this.user_id_2,
  793. },
  794. op: 'mock-op-3',
  795. },
  796. ]
  797. this.user_info = {}
  798. this.user_info[this.user_id_1] = { email: 'user1@sharelatex.com' }
  799. this.user_info[this.user_id_2] = { email: 'user2@sharelatex.com' }
  800. this.WebApiManager.getUserInfo = (userId, callback) => {
  801. if (callback == null) {
  802. callback = function () {}
  803. }
  804. return callback(null, this.user_info[userId])
  805. }
  806. sinon.spy(this.WebApiManager, 'getUserInfo')
  807. return this.UpdatesManager.fillUserInfo(
  808. this.updates,
  809. (error, results) => {
  810. if (error) return done(error)
  811. this.results = results
  812. return done()
  813. }
  814. )
  815. })
  816. it('should only call getUserInfo once for each user_id', function () {
  817. this.WebApiManager.getUserInfo.calledTwice.should.equal(true)
  818. this.WebApiManager.getUserInfo
  819. .calledWith(this.user_id_1)
  820. .should.equal(true)
  821. return this.WebApiManager.getUserInfo
  822. .calledWith(this.user_id_2)
  823. .should.equal(true)
  824. })
  825. return it('should return the updates with the user info filled', function () {
  826. return expect(this.results).to.deep.equal([
  827. {
  828. meta: {
  829. user: {
  830. email: 'user1@sharelatex.com',
  831. },
  832. },
  833. op: 'mock-op-1',
  834. },
  835. {
  836. meta: {
  837. user: {
  838. email: 'user1@sharelatex.com',
  839. },
  840. },
  841. op: 'mock-op-2',
  842. },
  843. {
  844. meta: {
  845. user: {
  846. email: 'user2@sharelatex.com',
  847. },
  848. },
  849. op: 'mock-op-3',
  850. },
  851. ])
  852. })
  853. })
  854. return describe('with invalid user ids', function () {
  855. beforeEach(function (done) {
  856. this.updates = [
  857. {
  858. meta: {
  859. user_id: null,
  860. },
  861. op: 'mock-op-1',
  862. },
  863. {
  864. meta: {
  865. user_id: 'anonymous-user',
  866. },
  867. op: 'mock-op-2',
  868. },
  869. ]
  870. this.WebApiManager.getUserInfo = (userId, callback) => {
  871. if (callback == null) {
  872. callback = function () {}
  873. }
  874. return callback(null, this.user_info[userId])
  875. }
  876. sinon.spy(this.WebApiManager, 'getUserInfo')
  877. return this.UpdatesManager.fillUserInfo(
  878. this.updates,
  879. (error, results) => {
  880. if (error) return done(error)
  881. this.results = results
  882. return done()
  883. }
  884. )
  885. })
  886. it('should not call getUserInfo', function () {
  887. return this.WebApiManager.getUserInfo.called.should.equal(false)
  888. })
  889. return it('should return the updates without the user info filled', function () {
  890. return expect(this.results).to.deep.equal([
  891. {
  892. meta: {},
  893. op: 'mock-op-1',
  894. },
  895. {
  896. meta: {},
  897. op: 'mock-op-2',
  898. },
  899. ])
  900. })
  901. })
  902. })
  903. return describe('_summarizeUpdates', function () {
  904. beforeEach(function () {
  905. this.now = Date.now()
  906. this.user_1 = { id: 'mock-user-1' }
  907. return (this.user_2 = { id: 'mock-user-2' })
  908. })
  909. it('should concat updates that are close in time', function () {
  910. const result = this.UpdatesManager._summarizeUpdates([
  911. {
  912. doc_id: 'doc-id-1',
  913. meta: {
  914. user_id: this.user_1.id,
  915. start_ts: this.now + 20,
  916. end_ts: this.now + 30,
  917. },
  918. v: 5,
  919. },
  920. {
  921. doc_id: 'doc-id-1',
  922. meta: {
  923. user_id: this.user_2.id,
  924. start_ts: this.now,
  925. end_ts: this.now + 10,
  926. },
  927. v: 4,
  928. },
  929. ])
  930. return expect(result).to.deep.equal([
  931. {
  932. docs: {
  933. 'doc-id-1': {
  934. fromV: 4,
  935. toV: 5,
  936. },
  937. },
  938. meta: {
  939. user_ids: [this.user_1.id, this.user_2.id],
  940. start_ts: this.now,
  941. end_ts: this.now + 30,
  942. },
  943. },
  944. ])
  945. })
  946. it('should leave updates that are far apart in time', function () {
  947. const oneDay = 1000 * 60 * 60 * 24
  948. const result = this.UpdatesManager._summarizeUpdates([
  949. {
  950. doc_id: 'doc-id-1',
  951. meta: {
  952. user_id: this.user_2.id,
  953. start_ts: this.now + oneDay,
  954. end_ts: this.now + oneDay + 10,
  955. },
  956. v: 5,
  957. },
  958. {
  959. doc_id: 'doc-id-1',
  960. meta: {
  961. user_id: this.user_1.id,
  962. start_ts: this.now,
  963. end_ts: this.now + 10,
  964. },
  965. v: 4,
  966. },
  967. ])
  968. return expect(result).to.deep.equal([
  969. {
  970. docs: {
  971. 'doc-id-1': {
  972. fromV: 5,
  973. toV: 5,
  974. },
  975. },
  976. meta: {
  977. user_ids: [this.user_2.id],
  978. start_ts: this.now + oneDay,
  979. end_ts: this.now + oneDay + 10,
  980. },
  981. },
  982. {
  983. docs: {
  984. 'doc-id-1': {
  985. fromV: 4,
  986. toV: 4,
  987. },
  988. },
  989. meta: {
  990. user_ids: [this.user_1.id],
  991. start_ts: this.now,
  992. end_ts: this.now + 10,
  993. },
  994. },
  995. ])
  996. })
  997. it('should concat onto existing summarized updates', function () {
  998. const result = this.UpdatesManager._summarizeUpdates(
  999. [
  1000. {
  1001. doc_id: 'doc-id-2',
  1002. meta: {
  1003. user_id: this.user_1.id,
  1004. start_ts: this.now + 20,
  1005. end_ts: this.now + 30,
  1006. },
  1007. v: 5,
  1008. },
  1009. {
  1010. doc_id: 'doc-id-2',
  1011. meta: {
  1012. user_id: this.user_2.id,
  1013. start_ts: this.now,
  1014. end_ts: this.now + 10,
  1015. },
  1016. v: 4,
  1017. },
  1018. ],
  1019. [
  1020. {
  1021. docs: {
  1022. 'doc-id-1': {
  1023. fromV: 6,
  1024. toV: 8,
  1025. },
  1026. },
  1027. meta: {
  1028. user_ids: [this.user_1.id],
  1029. start_ts: this.now + 40,
  1030. end_ts: this.now + 50,
  1031. },
  1032. },
  1033. ]
  1034. )
  1035. return expect(result).to.deep.equal([
  1036. {
  1037. docs: {
  1038. 'doc-id-1': {
  1039. toV: 8,
  1040. fromV: 6,
  1041. },
  1042. 'doc-id-2': {
  1043. toV: 5,
  1044. fromV: 4,
  1045. },
  1046. },
  1047. meta: {
  1048. user_ids: [this.user_1.id, this.user_2.id],
  1049. start_ts: this.now,
  1050. end_ts: this.now + 50,
  1051. },
  1052. },
  1053. ])
  1054. })
  1055. it('should include null user values', function () {
  1056. const result = this.UpdatesManager._summarizeUpdates([
  1057. {
  1058. doc_id: 'doc-id-1',
  1059. meta: {
  1060. user_id: this.user_1.id,
  1061. start_ts: this.now + 20,
  1062. end_ts: this.now + 30,
  1063. },
  1064. v: 5,
  1065. },
  1066. {
  1067. doc_id: 'doc-id-1',
  1068. meta: {
  1069. user_id: null,
  1070. start_ts: this.now,
  1071. end_ts: this.now + 10,
  1072. },
  1073. v: 4,
  1074. },
  1075. ])
  1076. return expect(result).to.deep.equal([
  1077. {
  1078. docs: {
  1079. 'doc-id-1': {
  1080. fromV: 4,
  1081. toV: 5,
  1082. },
  1083. },
  1084. meta: {
  1085. user_ids: [this.user_1.id, null],
  1086. start_ts: this.now,
  1087. end_ts: this.now + 30,
  1088. },
  1089. },
  1090. ])
  1091. })
  1092. it('should include null user values, when the null is earlier in the updates list', function () {
  1093. const result = this.UpdatesManager._summarizeUpdates([
  1094. {
  1095. doc_id: 'doc-id-1',
  1096. meta: {
  1097. user_id: null,
  1098. start_ts: this.now,
  1099. end_ts: this.now + 10,
  1100. },
  1101. v: 4,
  1102. },
  1103. {
  1104. doc_id: 'doc-id-1',
  1105. meta: {
  1106. user_id: this.user_1.id,
  1107. start_ts: this.now + 20,
  1108. end_ts: this.now + 30,
  1109. },
  1110. v: 5,
  1111. },
  1112. ])
  1113. return expect(result).to.deep.equal([
  1114. {
  1115. docs: {
  1116. 'doc-id-1': {
  1117. fromV: 4,
  1118. toV: 5,
  1119. },
  1120. },
  1121. meta: {
  1122. user_ids: [null, this.user_1.id],
  1123. start_ts: this.now,
  1124. end_ts: this.now + 30,
  1125. },
  1126. },
  1127. ])
  1128. })
  1129. it('should roll several null user values into one', function () {
  1130. const result = this.UpdatesManager._summarizeUpdates([
  1131. {
  1132. doc_id: 'doc-id-1',
  1133. meta: {
  1134. user_id: this.user_1.id,
  1135. start_ts: this.now + 20,
  1136. end_ts: this.now + 30,
  1137. },
  1138. v: 5,
  1139. },
  1140. {
  1141. doc_id: 'doc-id-1',
  1142. meta: {
  1143. user_id: null,
  1144. start_ts: this.now,
  1145. end_ts: this.now + 10,
  1146. },
  1147. v: 4,
  1148. },
  1149. {
  1150. doc_id: 'doc-id-1',
  1151. meta: {
  1152. user_id: null,
  1153. start_ts: this.now + 2,
  1154. end_ts: this.now + 4,
  1155. },
  1156. v: 4,
  1157. },
  1158. ])
  1159. return expect(result).to.deep.equal([
  1160. {
  1161. docs: {
  1162. 'doc-id-1': {
  1163. fromV: 4,
  1164. toV: 5,
  1165. },
  1166. },
  1167. meta: {
  1168. user_ids: [this.user_1.id, null],
  1169. start_ts: this.now,
  1170. end_ts: this.now + 30,
  1171. },
  1172. },
  1173. ])
  1174. })
  1175. return it('should split updates before a big delete', function () {
  1176. const result = this.UpdatesManager._summarizeUpdates([
  1177. {
  1178. doc_id: 'doc-id-1',
  1179. op: [{ d: 'this is a long long long long long delete', p: 34 }],
  1180. meta: {
  1181. user_id: this.user_1.id,
  1182. start_ts: this.now + 20,
  1183. end_ts: this.now + 30,
  1184. },
  1185. v: 5,
  1186. },
  1187. {
  1188. doc_id: 'doc-id-1',
  1189. meta: {
  1190. user_id: this.user_2.id,
  1191. start_ts: this.now,
  1192. end_ts: this.now + 10,
  1193. },
  1194. v: 4,
  1195. },
  1196. ])
  1197. return expect(result).to.deep.equal([
  1198. {
  1199. docs: {
  1200. 'doc-id-1': {
  1201. fromV: 5,
  1202. toV: 5,
  1203. },
  1204. },
  1205. meta: {
  1206. user_ids: [this.user_1.id],
  1207. start_ts: this.now + 20,
  1208. end_ts: this.now + 30,
  1209. },
  1210. },
  1211. {
  1212. docs: {
  1213. 'doc-id-1': {
  1214. fromV: 4,
  1215. toV: 4,
  1216. },
  1217. },
  1218. meta: {
  1219. user_ids: [this.user_2.id],
  1220. start_ts: this.now,
  1221. end_ts: this.now + 10,
  1222. },
  1223. },
  1224. ])
  1225. })
  1226. })
  1227. })