UpdatesManagerTests.coffee 27 KB

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