UpdatesManagerTests.coffee 25 KB

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