UpdatesManagerTests.coffee 28 KB

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