UpdatesManagerTests.coffee 27 KB

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