UpdatesManagerTests.js 40 KB

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