UpdatesManagerTests.coffee 21 KB

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