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