UpdatesManagerTests.js 40 KB

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