UpdatesManagerTests.js 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-return-assign,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS101: Remove unnecessary use of Array.from
  12. * DS102: Remove unnecessary code created because of implicit returns
  13. * DS207: Consider shorter variations of null checks
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. const sinon = require('sinon')
  17. const { expect } = require('chai')
  18. const { ObjectId } = require('mongodb')
  19. const modulePath = '../../../../app/js/UpdatesManager.js'
  20. const SandboxedModule = require('sandboxed-module')
  21. describe('UpdatesManager', function () {
  22. beforeEach(function () {
  23. this.UpdatesManager = SandboxedModule.require(modulePath, {
  24. singleOnly: true,
  25. requires: {
  26. './UpdateCompressor': (this.UpdateCompressor = {}),
  27. './MongoManager': (this.MongoManager = {}),
  28. './PackManager': (this.PackManager = {}),
  29. './RedisManager': (this.RedisManager = {}),
  30. './LockManager': (this.LockManager = {}),
  31. './WebApiManager': (this.WebApiManager = {}),
  32. './UpdateTrimmer': (this.UpdateTrimmer = {}),
  33. './DocArchiveManager': (this.DocArchiveManager = {}),
  34. '@overleaf/settings': {
  35. redis: {
  36. lock: {
  37. key_schema: {
  38. historyLock({ doc_id }) {
  39. return `HistoryLock:${doc_id}`
  40. },
  41. },
  42. },
  43. },
  44. },
  45. },
  46. })
  47. this.doc_id = 'doc-id-123'
  48. this.project_id = 'project-id-123'
  49. this.callback = sinon.stub()
  50. return (this.temporary = 'temp-mock')
  51. })
  52. describe('compressAndSaveRawUpdates', function () {
  53. describe('when there are no raw ops', function () {
  54. beforeEach(function () {
  55. this.MongoManager.peekLastCompressedUpdate = sinon.stub()
  56. return this.UpdatesManager.compressAndSaveRawUpdates(
  57. this.project_id,
  58. this.doc_id,
  59. [],
  60. this.temporary,
  61. this.callback
  62. )
  63. })
  64. it('should not need to access the database', function () {
  65. return this.MongoManager.peekLastCompressedUpdate.called.should.equal(
  66. false
  67. )
  68. })
  69. return it('should call the callback', function () {
  70. return this.callback.called.should.equal(true)
  71. })
  72. })
  73. describe('when there is no compressed history to begin with', function () {
  74. beforeEach(function () {
  75. this.rawUpdates = [
  76. { v: 12, op: 'mock-op-12' },
  77. { v: 13, op: 'mock-op-13' },
  78. ]
  79. this.compressedUpdates = [{ v: 13, op: 'compressed-op-12' }]
  80. this.MongoManager.peekLastCompressedUpdate = sinon
  81. .stub()
  82. .callsArgWith(1, null, null)
  83. this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5)
  84. this.UpdateCompressor.compressRawUpdates = sinon
  85. .stub()
  86. .returns(this.compressedUpdates)
  87. return this.UpdatesManager.compressAndSaveRawUpdates(
  88. this.project_id,
  89. this.doc_id,
  90. this.rawUpdates,
  91. this.temporary,
  92. this.callback
  93. )
  94. })
  95. it('should look at the last compressed op', function () {
  96. return this.MongoManager.peekLastCompressedUpdate
  97. .calledWith(this.doc_id)
  98. .should.equal(true)
  99. })
  100. it('should save the compressed ops as a pack', function () {
  101. return this.PackManager.insertCompressedUpdates
  102. .calledWith(
  103. this.project_id,
  104. this.doc_id,
  105. null,
  106. this.compressedUpdates,
  107. this.temporary
  108. )
  109. .should.equal(true)
  110. })
  111. return it('should call the callback', function () {
  112. return this.callback.called.should.equal(true)
  113. })
  114. })
  115. describe('when the raw ops need appending to existing history', function () {
  116. beforeEach(function () {
  117. this.lastCompressedUpdate = { v: 11, op: 'compressed-op-11' }
  118. this.compressedUpdates = [
  119. { v: 12, op: 'compressed-op-11+12' },
  120. { v: 13, op: 'compressed-op-12' },
  121. ]
  122. this.MongoManager.peekLastCompressedUpdate = sinon
  123. .stub()
  124. .callsArgWith(
  125. 1,
  126. null,
  127. this.lastCompressedUpdate,
  128. this.lastCompressedUpdate.v
  129. )
  130. this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5)
  131. return (this.UpdateCompressor.compressRawUpdates = sinon
  132. .stub()
  133. .returns(this.compressedUpdates))
  134. })
  135. describe('when the raw ops start where the existing history ends', function () {
  136. beforeEach(function () {
  137. this.rawUpdates = [
  138. { v: 12, op: 'mock-op-12' },
  139. { v: 13, op: 'mock-op-13' },
  140. ]
  141. return this.UpdatesManager.compressAndSaveRawUpdates(
  142. this.project_id,
  143. this.doc_id,
  144. this.rawUpdates,
  145. this.temporary,
  146. this.callback
  147. )
  148. })
  149. it('should look at the last compressed op', function () {
  150. return this.MongoManager.peekLastCompressedUpdate
  151. .calledWith(this.doc_id)
  152. .should.equal(true)
  153. })
  154. it('should compress the raw ops', function () {
  155. return this.UpdateCompressor.compressRawUpdates
  156. .calledWith(null, this.rawUpdates)
  157. .should.equal(true)
  158. })
  159. it('should save the new compressed ops into a pack', function () {
  160. return this.PackManager.insertCompressedUpdates
  161. .calledWith(
  162. this.project_id,
  163. this.doc_id,
  164. this.lastCompressedUpdate,
  165. this.compressedUpdates,
  166. this.temporary
  167. )
  168. .should.equal(true)
  169. })
  170. return it('should call the callback', function () {
  171. return this.callback.called.should.equal(true)
  172. })
  173. })
  174. describe('when the raw ops start where the existing history ends and the history is in a pack', function () {
  175. beforeEach(function () {
  176. this.lastCompressedUpdate = {
  177. pack: [{ v: 11, op: 'compressed-op-11' }],
  178. v: 11,
  179. }
  180. this.rawUpdates = [
  181. { v: 12, op: 'mock-op-12' },
  182. { v: 13, op: 'mock-op-13' },
  183. ]
  184. this.MongoManager.peekLastCompressedUpdate = sinon
  185. .stub()
  186. .callsArgWith(
  187. 1,
  188. null,
  189. this.lastCompressedUpdate,
  190. this.lastCompressedUpdate.v
  191. )
  192. return this.UpdatesManager.compressAndSaveRawUpdates(
  193. this.project_id,
  194. this.doc_id,
  195. this.rawUpdates,
  196. this.temporary,
  197. this.callback
  198. )
  199. })
  200. it('should look at the last compressed op', function () {
  201. return this.MongoManager.peekLastCompressedUpdate
  202. .calledWith(this.doc_id)
  203. .should.equal(true)
  204. })
  205. it('should compress the raw ops', function () {
  206. return this.UpdateCompressor.compressRawUpdates
  207. .calledWith(null, this.rawUpdates)
  208. .should.equal(true)
  209. })
  210. it('should save the new compressed ops into a pack', function () {
  211. return this.PackManager.insertCompressedUpdates
  212. .calledWith(
  213. this.project_id,
  214. this.doc_id,
  215. this.lastCompressedUpdate,
  216. this.compressedUpdates,
  217. this.temporary
  218. )
  219. .should.equal(true)
  220. })
  221. return it('should call the callback', function () {
  222. return this.callback.called.should.equal(true)
  223. })
  224. })
  225. describe('when some raw ops are passed that have already been compressed', function () {
  226. beforeEach(function () {
  227. this.rawUpdates = [
  228. { v: 10, op: 'mock-op-10' },
  229. { v: 11, op: 'mock-op-11' },
  230. { v: 12, op: 'mock-op-12' },
  231. { v: 13, op: 'mock-op-13' },
  232. ]
  233. return this.UpdatesManager.compressAndSaveRawUpdates(
  234. this.project_id,
  235. this.doc_id,
  236. this.rawUpdates,
  237. this.temporary,
  238. this.callback
  239. )
  240. })
  241. return it('should only compress the more recent raw ops', function () {
  242. return this.UpdateCompressor.compressRawUpdates
  243. .calledWith(null, this.rawUpdates.slice(-2))
  244. .should.equal(true)
  245. })
  246. })
  247. describe('when the raw ops do not follow from the last compressed op version', function () {
  248. beforeEach(function () {
  249. this.rawUpdates = [{ v: 13, op: 'mock-op-13' }]
  250. return this.UpdatesManager.compressAndSaveRawUpdates(
  251. this.project_id,
  252. this.doc_id,
  253. this.rawUpdates,
  254. this.temporary,
  255. this.callback
  256. )
  257. })
  258. it('should call the callback with an error', function () {
  259. return this.callback
  260. .calledWith(
  261. sinon.match.has(
  262. 'message',
  263. 'Tried to apply raw op at version 13 to last compressed update with version 11 from unknown time'
  264. )
  265. )
  266. .should.equal(true)
  267. })
  268. return it('should not insert any update into mongo', function () {
  269. return this.PackManager.insertCompressedUpdates.called.should.equal(
  270. false
  271. )
  272. })
  273. })
  274. return describe('when the raw ops are out of order', function () {
  275. beforeEach(function () {
  276. this.rawUpdates = [
  277. { v: 13, op: 'mock-op-13' },
  278. { v: 12, op: 'mock-op-12' },
  279. ]
  280. return this.UpdatesManager.compressAndSaveRawUpdates(
  281. this.project_id,
  282. this.doc_id,
  283. this.rawUpdates,
  284. this.temporary,
  285. this.callback
  286. )
  287. })
  288. it('should call the callback with an error', function () {
  289. return this.callback
  290. .calledWith(sinon.match.has('message'))
  291. .should.equal(true)
  292. })
  293. return it('should not insert any update into mongo', function () {
  294. return this.PackManager.insertCompressedUpdates.called.should.equal(
  295. false
  296. )
  297. })
  298. })
  299. })
  300. return describe('when the raw ops need appending to existing history which is in S3', function () {
  301. beforeEach(function () {
  302. this.lastCompressedUpdate = null
  303. this.lastVersion = 11
  304. this.compressedUpdates = [{ v: 13, op: 'compressed-op-12' }]
  305. this.MongoManager.peekLastCompressedUpdate = sinon
  306. .stub()
  307. .callsArgWith(1, null, null, this.lastVersion)
  308. this.PackManager.insertCompressedUpdates = sinon.stub().callsArg(5)
  309. return (this.UpdateCompressor.compressRawUpdates = sinon
  310. .stub()
  311. .returns(this.compressedUpdates))
  312. })
  313. return describe('when the raw ops start where the existing history ends', function () {
  314. beforeEach(function () {
  315. this.rawUpdates = [
  316. { v: 12, op: 'mock-op-12' },
  317. { v: 13, op: 'mock-op-13' },
  318. ]
  319. return this.UpdatesManager.compressAndSaveRawUpdates(
  320. this.project_id,
  321. this.doc_id,
  322. this.rawUpdates,
  323. this.temporary,
  324. this.callback
  325. )
  326. })
  327. it('should try to look at the last compressed op', function () {
  328. return this.MongoManager.peekLastCompressedUpdate
  329. .calledWith(this.doc_id)
  330. .should.equal(true)
  331. })
  332. it('should compress the last compressed op and the raw ops', function () {
  333. return this.UpdateCompressor.compressRawUpdates
  334. .calledWith(this.lastCompressedUpdate, this.rawUpdates)
  335. .should.equal(true)
  336. })
  337. it('should save the compressed ops', function () {
  338. return this.PackManager.insertCompressedUpdates
  339. .calledWith(
  340. this.project_id,
  341. this.doc_id,
  342. null,
  343. this.compressedUpdates,
  344. this.temporary
  345. )
  346. .should.equal(true)
  347. })
  348. return it('should call the callback', function () {
  349. return this.callback.called.should.equal(true)
  350. })
  351. })
  352. })
  353. })
  354. describe('processUncompressedUpdates', function () {
  355. beforeEach(function () {
  356. this.UpdatesManager.compressAndSaveRawUpdates = sinon
  357. .stub()
  358. .callsArgWith(4)
  359. this.RedisManager.deleteAppliedDocUpdates = sinon.stub().callsArg(3)
  360. this.MongoManager.backportProjectId = sinon.stub().callsArg(2)
  361. return (this.UpdateTrimmer.shouldTrimUpdates = sinon
  362. .stub()
  363. .callsArgWith(1, null, (this.temporary = 'temp mock')))
  364. })
  365. describe('when there is fewer than one batch to send', function () {
  366. beforeEach(function () {
  367. this.updates = ['mock-update']
  368. this.RedisManager.getOldestDocUpdates = sinon
  369. .stub()
  370. .callsArgWith(2, null, this.updates)
  371. this.RedisManager.expandDocUpdates = sinon
  372. .stub()
  373. .callsArgWith(1, null, this.updates)
  374. return this.UpdatesManager.processUncompressedUpdates(
  375. this.project_id,
  376. this.doc_id,
  377. this.temporary,
  378. this.callback
  379. )
  380. })
  381. it('should get the oldest updates', function () {
  382. return this.RedisManager.getOldestDocUpdates
  383. .calledWith(this.doc_id, this.UpdatesManager.REDIS_READ_BATCH_SIZE)
  384. .should.equal(true)
  385. })
  386. it('should compress and save the updates', function () {
  387. return this.UpdatesManager.compressAndSaveRawUpdates
  388. .calledWith(
  389. this.project_id,
  390. this.doc_id,
  391. this.updates,
  392. this.temporary
  393. )
  394. .should.equal(true)
  395. })
  396. it('should delete the batch of uncompressed updates that was just processed', function () {
  397. return this.RedisManager.deleteAppliedDocUpdates
  398. .calledWith(this.project_id, this.doc_id, this.updates)
  399. .should.equal(true)
  400. })
  401. return it('should call the callback', function () {
  402. return this.callback.called.should.equal(true)
  403. })
  404. })
  405. return describe('when there are multiple batches to send', function () {
  406. beforeEach(function (done) {
  407. this.UpdatesManager.REDIS_READ_BATCH_SIZE = 2
  408. this.updates = [
  409. 'mock-update-0',
  410. 'mock-update-1',
  411. 'mock-update-2',
  412. 'mock-update-3',
  413. 'mock-update-4',
  414. ]
  415. this.redisArray = this.updates.slice()
  416. this.RedisManager.getOldestDocUpdates = (
  417. doc_id,
  418. batchSize,
  419. callback
  420. ) => {
  421. if (callback == null) {
  422. callback = function (error, updates) {}
  423. }
  424. const updates = this.redisArray.slice(0, batchSize)
  425. this.redisArray = this.redisArray.slice(batchSize)
  426. return callback(null, updates)
  427. }
  428. sinon.spy(this.RedisManager, 'getOldestDocUpdates')
  429. this.RedisManager.expandDocUpdates = (jsonUpdates, callback) => {
  430. return callback(null, jsonUpdates)
  431. }
  432. sinon.spy(this.RedisManager, 'expandDocUpdates')
  433. return this.UpdatesManager.processUncompressedUpdates(
  434. this.project_id,
  435. this.doc_id,
  436. this.temporary,
  437. (...args) => {
  438. this.callback(...Array.from(args || []))
  439. return done()
  440. }
  441. )
  442. })
  443. it('should get the oldest updates in three batches ', function () {
  444. return this.RedisManager.getOldestDocUpdates.callCount.should.equal(3)
  445. })
  446. it('should compress and save the updates in batches', function () {
  447. this.UpdatesManager.compressAndSaveRawUpdates
  448. .calledWith(
  449. this.project_id,
  450. this.doc_id,
  451. this.updates.slice(0, 2),
  452. this.temporary
  453. )
  454. .should.equal(true)
  455. this.UpdatesManager.compressAndSaveRawUpdates
  456. .calledWith(
  457. this.project_id,
  458. this.doc_id,
  459. this.updates.slice(2, 4),
  460. this.temporary
  461. )
  462. .should.equal(true)
  463. return this.UpdatesManager.compressAndSaveRawUpdates
  464. .calledWith(
  465. this.project_id,
  466. this.doc_id,
  467. this.updates.slice(4, 5),
  468. this.temporary
  469. )
  470. .should.equal(true)
  471. })
  472. it('should delete the batches of uncompressed updates', function () {
  473. return this.RedisManager.deleteAppliedDocUpdates.callCount.should.equal(
  474. 3
  475. )
  476. })
  477. return it('should call the callback', function () {
  478. return this.callback.called.should.equal(true)
  479. })
  480. })
  481. })
  482. describe('processCompressedUpdatesWithLock', function () {
  483. beforeEach(function () {
  484. this.UpdateTrimmer.shouldTrimUpdates = sinon
  485. .stub()
  486. .callsArgWith(1, null, (this.temporary = 'temp mock'))
  487. this.MongoManager.backportProjectId = sinon.stub().callsArg(2)
  488. this.UpdatesManager._processUncompressedUpdates = sinon.stub().callsArg(3)
  489. this.LockManager.runWithLock = sinon.stub().callsArg(2)
  490. return this.UpdatesManager.processUncompressedUpdatesWithLock(
  491. this.project_id,
  492. this.doc_id,
  493. this.callback
  494. )
  495. })
  496. it('should check if the updates are temporary', function () {
  497. return this.UpdateTrimmer.shouldTrimUpdates
  498. .calledWith(this.project_id)
  499. .should.equal(true)
  500. })
  501. it('should backport the project id', function () {
  502. return this.MongoManager.backportProjectId
  503. .calledWith(this.project_id, this.doc_id)
  504. .should.equal(true)
  505. })
  506. it('should run processUncompressedUpdates with the lock', function () {
  507. return this.LockManager.runWithLock
  508. .calledWith(`HistoryLock:${this.doc_id}`)
  509. .should.equal(true)
  510. })
  511. return it('should call the callback', function () {
  512. return this.callback.called.should.equal(true)
  513. })
  514. })
  515. describe('getDocUpdates', function () {
  516. beforeEach(function () {
  517. this.updates = ['mock-updates']
  518. this.options = { to: 'mock-to', limit: 'mock-limit' }
  519. this.PackManager.getOpsByVersionRange = sinon
  520. .stub()
  521. .callsArgWith(4, null, this.updates)
  522. this.UpdatesManager.processUncompressedUpdatesWithLock = sinon
  523. .stub()
  524. .callsArg(2)
  525. return this.UpdatesManager.getDocUpdates(
  526. this.project_id,
  527. this.doc_id,
  528. this.options,
  529. this.callback
  530. )
  531. })
  532. it('should process outstanding updates', function () {
  533. return this.UpdatesManager.processUncompressedUpdatesWithLock
  534. .calledWith(this.project_id, this.doc_id)
  535. .should.equal(true)
  536. })
  537. it('should get the updates from the database', function () {
  538. return this.PackManager.getOpsByVersionRange
  539. .calledWith(
  540. this.project_id,
  541. this.doc_id,
  542. this.options.from,
  543. this.options.to
  544. )
  545. .should.equal(true)
  546. })
  547. return it('should return the updates', function () {
  548. return this.callback.calledWith(null, this.updates).should.equal(true)
  549. })
  550. })
  551. describe('getDocUpdatesWithUserInfo', function () {
  552. beforeEach(function () {
  553. this.updates = ['mock-updates']
  554. this.options = { to: 'mock-to', limit: 'mock-limit' }
  555. this.updatesWithUserInfo = ['updates-with-user-info']
  556. this.UpdatesManager.getDocUpdates = sinon
  557. .stub()
  558. .callsArgWith(3, null, this.updates)
  559. this.UpdatesManager.fillUserInfo = sinon
  560. .stub()
  561. .callsArgWith(1, null, this.updatesWithUserInfo)
  562. return this.UpdatesManager.getDocUpdatesWithUserInfo(
  563. this.project_id,
  564. this.doc_id,
  565. this.options,
  566. this.callback
  567. )
  568. })
  569. it('should get the updates', function () {
  570. return this.UpdatesManager.getDocUpdates
  571. .calledWith(this.project_id, this.doc_id, this.options)
  572. .should.equal(true)
  573. })
  574. it('should file the updates with the user info', function () {
  575. return this.UpdatesManager.fillUserInfo
  576. .calledWith(this.updates)
  577. .should.equal(true)
  578. })
  579. return it('should return the updates with the filled details', function () {
  580. return this.callback
  581. .calledWith(null, this.updatesWithUserInfo)
  582. .should.equal(true)
  583. })
  584. })
  585. describe('processUncompressedUpdatesForProject', function () {
  586. beforeEach(function (done) {
  587. this.doc_ids = ['mock-id-1', 'mock-id-2']
  588. this.UpdateTrimmer.shouldTrimUpdates = sinon
  589. .stub()
  590. .callsArgWith(1, null, (this.temporary = 'temp mock'))
  591. this.MongoManager.backportProjectId = sinon.stub().callsArg(2)
  592. this.UpdatesManager._processUncompressedUpdatesForDocWithLock = sinon
  593. .stub()
  594. .callsArg(3)
  595. this.RedisManager.getDocIdsWithHistoryOps = sinon
  596. .stub()
  597. .callsArgWith(1, null, this.doc_ids)
  598. return this.UpdatesManager.processUncompressedUpdatesForProject(
  599. this.project_id,
  600. () => {
  601. this.callback()
  602. return done()
  603. }
  604. )
  605. })
  606. it('should get all the docs with history ops', function () {
  607. return this.RedisManager.getDocIdsWithHistoryOps
  608. .calledWith(this.project_id)
  609. .should.equal(true)
  610. })
  611. it('should process the doc ops for the each doc_id', function () {
  612. return Array.from(this.doc_ids).map(doc_id =>
  613. this.UpdatesManager._processUncompressedUpdatesForDocWithLock
  614. .calledWith(this.project_id, doc_id, this.temporary)
  615. .should.equal(true)
  616. )
  617. })
  618. return it('should call the callback', function () {
  619. return this.callback.called.should.equal(true)
  620. })
  621. })
  622. describe('getSummarizedProjectUpdates', function () {
  623. beforeEach(function () {
  624. this.updates = [
  625. {
  626. doc_id: 123,
  627. v: 456,
  628. op: 'mock-updates',
  629. meta: { user_id: 123, start_ts: 1233, end_ts: 1234 },
  630. },
  631. ]
  632. this.options = { before: 'mock-before', limit: 'mock-limit' }
  633. this.summarizedUpdates = [
  634. {
  635. meta: { user_ids: [123], start_ts: 1233, end_ts: 1234 },
  636. docs: { 123: { fromV: 456, toV: 456 } },
  637. },
  638. ]
  639. this.updatesWithUserInfo = ['updates-with-user-info']
  640. this.done_state = false
  641. this.iterator = {
  642. next: cb => {
  643. this.done_state = true
  644. return cb(null, this.updates)
  645. },
  646. done: () => {
  647. return this.done_state
  648. },
  649. }
  650. this.PackManager.makeProjectIterator = sinon
  651. .stub()
  652. .callsArgWith(2, null, this.iterator)
  653. this.UpdatesManager.processUncompressedUpdatesForProject = sinon
  654. .stub()
  655. .callsArg(1)
  656. this.UpdatesManager.fillSummarizedUserInfo = sinon
  657. .stub()
  658. .callsArgWith(1, null, this.updatesWithUserInfo)
  659. return this.UpdatesManager.getSummarizedProjectUpdates(
  660. this.project_id,
  661. this.options,
  662. this.callback
  663. )
  664. })
  665. it('should process any outstanding updates', function () {
  666. return this.UpdatesManager.processUncompressedUpdatesForProject
  667. .calledWith(this.project_id)
  668. .should.equal(true)
  669. })
  670. it('should get the updates', function () {
  671. return this.PackManager.makeProjectIterator
  672. .calledWith(this.project_id, this.options.before)
  673. .should.equal(true)
  674. })
  675. it('should fill the updates with the user info', function () {
  676. return this.UpdatesManager.fillSummarizedUserInfo
  677. .calledWith(this.summarizedUpdates)
  678. .should.equal(true)
  679. })
  680. return it('should return the updates with the filled details', function () {
  681. return this.callback
  682. .calledWith(null, this.updatesWithUserInfo)
  683. .should.equal(true)
  684. })
  685. })
  686. // describe "_extendBatchOfSummarizedUpdates", ->
  687. // beforeEach ->
  688. // @before = Date.now()
  689. // @min_count = 2
  690. // @existingSummarizedUpdates = ["summarized-updates-3"]
  691. // @summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"]
  692. // describe "when there are updates to get", ->
  693. // beforeEach ->
  694. // @updates = [
  695. // {op: "mock-op-1", meta: end_ts: @before - 10},
  696. // {op: "mock-op-1", meta: end_ts: @nextBeforeTimestamp = @before - 20}
  697. // ]
  698. // @existingSummarizedUpdates = ["summarized-updates-3"]
  699. // @summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"]
  700. // @UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates)
  701. // @UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates)
  702. // @UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback
  703. // it "should get the updates", ->
  704. // @UpdatesManager.getProjectUpdatesWithUserInfo
  705. // .calledWith(@project_id, { before: @before, limit: 3 * @min_count })
  706. // .should.equal true
  707. // it "should summarize the updates", ->
  708. // @UpdatesManager._summarizeUpdates
  709. // .calledWith(@updates, @existingSummarizedUpdates)
  710. // .should.equal true
  711. // it "should call the callback with the summarized updates and the next before timestamp", ->
  712. // @callback.calledWith(null, @summarizedUpdates, @nextBeforeTimestamp).should.equal true
  713. // describe "when there are no more updates", ->
  714. // beforeEach ->
  715. // @updates = []
  716. // @UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates)
  717. // @UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates)
  718. // @UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback
  719. // it "should call the callback with the summarized updates and null for nextBeforeTimestamp", ->
  720. // @callback.calledWith(null, @summarizedUpdates, null).should.equal true
  721. // describe "getSummarizedProjectUpdates", ->
  722. // describe "when one batch of updates is enough to meet the limit", ->
  723. // beforeEach ->
  724. // @before = Date.now()
  725. // @min_count = 2
  726. // @updates = ["summarized-updates-3", "summarized-updates-2"]
  727. // @nextBeforeTimestamp = @before - 100
  728. // @UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, @nextBeforeTimestamp)
  729. // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
  730. // it "should get the batch of summarized updates", ->
  731. // @UpdatesManager._extendBatchOfSummarizedUpdates
  732. // .calledWith(@project_id, [], @before, @min_count)
  733. // .should.equal true
  734. // it "should call the callback with the updates", ->
  735. // @callback.calledWith(null, @updates, @nextBeforeTimestamp).should.equal true
  736. // describe "when multiple batches are needed to meet the limit", ->
  737. // beforeEach ->
  738. // @before = Date.now()
  739. // @min_count = 4
  740. // @firstBatch = [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }]
  741. // @nextBeforeTimestamp = @before - 100
  742. // @secondBatch = [{ toV: 4, fromV: 4 }, { toV: 3, fromV: 3 }]
  743. // @nextNextBeforeTimestamp = @before - 200
  744. // @UpdatesManager._extendBatchOfSummarizedUpdates = (project_id, existingUpdates, before, desiredLength, callback) =>
  745. // if existingUpdates.length == 0
  746. // callback null, @firstBatch, @nextBeforeTimestamp
  747. // else
  748. // callback null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp
  749. // sinon.spy @UpdatesManager, "_extendBatchOfSummarizedUpdates"
  750. // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
  751. // it "should get the first batch of summarized updates", ->
  752. // @UpdatesManager._extendBatchOfSummarizedUpdates
  753. // .calledWith(@project_id, [], @before, @min_count)
  754. // .should.equal true
  755. // it "should get the second batch of summarized updates", ->
  756. // @UpdatesManager._extendBatchOfSummarizedUpdates
  757. // .calledWith(@project_id, @firstBatch, @nextBeforeTimestamp, @min_count)
  758. // .should.equal true
  759. // it "should call the callback with all the updates", ->
  760. // @callback.calledWith(null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp).should.equal true
  761. // describe "when the end of the database is hit", ->
  762. // beforeEach ->
  763. // @before = Date.now()
  764. // @min_count = 4
  765. // @updates = [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }]
  766. // @UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, null)
  767. // @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
  768. // it "should get the batch of summarized updates", ->
  769. // @UpdatesManager._extendBatchOfSummarizedUpdates
  770. // .calledWith(@project_id, [], @before, @min_count)
  771. // .should.equal true
  772. // it "should call the callback with the updates", ->
  773. // @callback.calledWith(null, @updates, null).should.equal true
  774. describe('fillUserInfo', function () {
  775. describe('with valid users', function () {
  776. beforeEach(function (done) {
  777. this.user_id_1 = ObjectId().toString()
  778. this.user_id_2 = ObjectId().toString()
  779. this.updates = [
  780. {
  781. meta: {
  782. user_id: this.user_id_1,
  783. },
  784. op: 'mock-op-1',
  785. },
  786. {
  787. meta: {
  788. user_id: this.user_id_1,
  789. },
  790. op: 'mock-op-2',
  791. },
  792. {
  793. meta: {
  794. user_id: this.user_id_2,
  795. },
  796. op: 'mock-op-3',
  797. },
  798. ]
  799. this.user_info = {}
  800. this.user_info[this.user_id_1] = { email: 'user1@sharelatex.com' }
  801. this.user_info[this.user_id_2] = { email: 'user2@sharelatex.com' }
  802. this.WebApiManager.getUserInfo = (user_id, callback) => {
  803. if (callback == null) {
  804. callback = function (error, userInfo) {}
  805. }
  806. return callback(null, this.user_info[user_id])
  807. }
  808. sinon.spy(this.WebApiManager, 'getUserInfo')
  809. return this.UpdatesManager.fillUserInfo(
  810. this.updates,
  811. (error, results) => {
  812. this.results = results
  813. return done()
  814. }
  815. )
  816. })
  817. it('should only call getUserInfo once for each user_id', function () {
  818. this.WebApiManager.getUserInfo.calledTwice.should.equal(true)
  819. this.WebApiManager.getUserInfo
  820. .calledWith(this.user_id_1)
  821. .should.equal(true)
  822. return this.WebApiManager.getUserInfo
  823. .calledWith(this.user_id_2)
  824. .should.equal(true)
  825. })
  826. return it('should return the updates with the user info filled', function () {
  827. return expect(this.results).to.deep.equal([
  828. {
  829. meta: {
  830. user: {
  831. email: 'user1@sharelatex.com',
  832. },
  833. },
  834. op: 'mock-op-1',
  835. },
  836. {
  837. meta: {
  838. user: {
  839. email: 'user1@sharelatex.com',
  840. },
  841. },
  842. op: 'mock-op-2',
  843. },
  844. {
  845. meta: {
  846. user: {
  847. email: 'user2@sharelatex.com',
  848. },
  849. },
  850. op: 'mock-op-3',
  851. },
  852. ])
  853. })
  854. })
  855. return describe('with invalid user ids', function () {
  856. beforeEach(function (done) {
  857. this.updates = [
  858. {
  859. meta: {
  860. user_id: null,
  861. },
  862. op: 'mock-op-1',
  863. },
  864. {
  865. meta: {
  866. user_id: 'anonymous-user',
  867. },
  868. op: 'mock-op-2',
  869. },
  870. ]
  871. this.WebApiManager.getUserInfo = (user_id, callback) => {
  872. if (callback == null) {
  873. callback = function (error, userInfo) {}
  874. }
  875. return callback(null, this.user_info[user_id])
  876. }
  877. sinon.spy(this.WebApiManager, 'getUserInfo')
  878. return this.UpdatesManager.fillUserInfo(
  879. this.updates,
  880. (error, results) => {
  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. })