UpdatesManagerTests.coffee 21 KB

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