UpdatesManagerTests.coffee 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  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. describe "when there is fewer than one batch to send", ->
  100. beforeEach ->
  101. @updates = ["mock-update"]
  102. @RedisManager.getOldestRawUpdates = sinon.stub().callsArgWith(2, null, @updates)
  103. @UpdatesManager.compressAndSaveRawUpdates = sinon.stub().callsArgWith(3)
  104. @RedisManager.deleteOldestRawUpdates = sinon.stub().callsArg(2)
  105. @UpdatesManager.processUncompressedUpdates @project_id, @doc_id, @callback
  106. it "should get the oldest updates", ->
  107. @RedisManager.getOldestRawUpdates
  108. .calledWith(@doc_id, @UpdatesManager.REDIS_READ_BATCH_SIZE)
  109. .should.equal true
  110. it "should compress and save the updates", ->
  111. @UpdatesManager.compressAndSaveRawUpdates
  112. .calledWith(@project_id, @doc_id, @updates)
  113. .should.equal true
  114. it "should delete the batch of uncompressed updates that was just processed", ->
  115. @RedisManager.deleteOldestRawUpdates
  116. .calledWith(@doc_id, @updates.length)
  117. .should.equal true
  118. it "should call the callback", ->
  119. @callback.called.should.equal true
  120. describe "when there are multiple batches to send", ->
  121. beforeEach (done) ->
  122. @UpdatesManager.REDIS_READ_BATCH_SIZE = 2
  123. @updates = ["mock-update-0", "mock-update-1", "mock-update-2", "mock-update-3", "mock-update-4"]
  124. @redisArray = @updates.slice()
  125. @RedisManager.getOldestRawUpdates = (doc_id, batchSize, callback = (error, updates) ->) =>
  126. updates = @redisArray.slice(0, batchSize)
  127. @redisArray = @redisArray.slice(batchSize)
  128. callback null, updates
  129. sinon.spy @RedisManager, "getOldestRawUpdates"
  130. @UpdatesManager.compressAndSaveRawUpdates = sinon.stub().callsArgWith(3)
  131. @RedisManager.deleteOldestRawUpdates = sinon.stub().callsArg(2)
  132. @UpdatesManager.processUncompressedUpdates @project_id, @doc_id, (args...) =>
  133. @callback(args...)
  134. done()
  135. it "should get the oldest updates in three batches ", ->
  136. @RedisManager.getOldestRawUpdates.callCount.should.equal 3
  137. it "should compress and save the updates in batches", ->
  138. @UpdatesManager.compressAndSaveRawUpdates
  139. .calledWith(@project_id, @doc_id, @updates.slice(0,2))
  140. .should.equal true
  141. @UpdatesManager.compressAndSaveRawUpdates
  142. .calledWith(@project_id, @doc_id, @updates.slice(2,4))
  143. .should.equal true
  144. @UpdatesManager.compressAndSaveRawUpdates
  145. .calledWith(@project_id, @doc_id, @updates.slice(4,5))
  146. .should.equal true
  147. it "should delete the batches of uncompressed updates", ->
  148. @RedisManager.deleteOldestRawUpdates.callCount.should.equal 3
  149. it "should call the callback", ->
  150. @callback.called.should.equal true
  151. describe "processCompressedUpdatesWithLock", ->
  152. beforeEach ->
  153. @UpdatesManager.processUncompressedUpdates = sinon.stub().callsArg(2)
  154. @LockManager.runWithLock = sinon.stub().callsArg(2)
  155. @UpdatesManager.processUncompressedUpdatesWithLock @project_id, @doc_id, @callback
  156. it "should run processUncompressedUpdates with the lock", ->
  157. @LockManager.runWithLock
  158. .calledWith(
  159. "HistoryLock:#{@doc_id}"
  160. )
  161. .should.equal true
  162. it "should call the callback", ->
  163. @callback.called.should.equal true
  164. describe "getDocUpdates", ->
  165. beforeEach ->
  166. @updates = ["mock-updates"]
  167. @options = { to: "mock-to", limit: "mock-limit" }
  168. @MongoManager.getDocUpdates = sinon.stub().callsArgWith(2, null, @updates)
  169. @UpdatesManager.processUncompressedUpdatesWithLock = sinon.stub().callsArg(2)
  170. @UpdatesManager.getDocUpdates @project_id, @doc_id, @options, @callback
  171. it "should process outstanding updates", ->
  172. @UpdatesManager.processUncompressedUpdatesWithLock
  173. .calledWith(@project_id, @doc_id)
  174. .should.equal true
  175. it "should get the updates from the database", ->
  176. @MongoManager.getDocUpdates
  177. .calledWith(@doc_id, @options)
  178. .should.equal true
  179. it "should return the updates", ->
  180. @callback
  181. .calledWith(null, @updates)
  182. .should.equal true
  183. describe "getDocUpdatesWithUserInfo", ->
  184. beforeEach ->
  185. @updates = ["mock-updates"]
  186. @options = { to: "mock-to", limit: "mock-limit" }
  187. @updatesWithUserInfo = ["updates-with-user-info"]
  188. @UpdatesManager.getDocUpdates = sinon.stub().callsArgWith(3, null, @updates)
  189. @UpdatesManager.fillUserInfo = sinon.stub().callsArgWith(1, null, @updatesWithUserInfo)
  190. @UpdatesManager.getDocUpdatesWithUserInfo @project_id, @doc_id, @options, @callback
  191. it "should get the updates", ->
  192. @UpdatesManager.getDocUpdates
  193. .calledWith(@project_id, @doc_id, @options)
  194. .should.equal true
  195. it "should file the updates with the user info", ->
  196. @UpdatesManager.fillUserInfo
  197. .calledWith(@updates)
  198. .should.equal true
  199. it "should return the updates with the filled details", ->
  200. @callback.calledWith(null, @updatesWithUserInfo).should.equal true
  201. describe "getProjectUpdates", ->
  202. beforeEach ->
  203. @updates = ["mock-updates"]
  204. @options = { before: "mock-before", limit: "mock-limit" }
  205. @MongoManager.getProjectUpdates = sinon.stub().callsArgWith(2, null, @updates)
  206. @UpdatesManager.getProjectUpdates @project_id, @options, @callback
  207. it "should get the updates from the database", ->
  208. @MongoManager.getProjectUpdates
  209. .calledWith(@project_id, @options)
  210. .should.equal true
  211. it "should return the updates", ->
  212. @callback
  213. .calledWith(null, @updates)
  214. .should.equal true
  215. describe "getProjectUpdatesWithUserInfo", ->
  216. beforeEach ->
  217. @updates = ["mock-updates"]
  218. @options = { before: "mock-before", limit: "mock-limit" }
  219. @updatesWithUserInfo = ["updates-with-user-info"]
  220. @UpdatesManager.getProjectUpdates = sinon.stub().callsArgWith(2, null, @updates)
  221. @UpdatesManager.fillUserInfo = sinon.stub().callsArgWith(1, null, @updatesWithUserInfo)
  222. @UpdatesManager.getProjectUpdatesWithUserInfo @project_id, @options, @callback
  223. it "should get the updates", ->
  224. @UpdatesManager.getProjectUpdates
  225. .calledWith(@project_id, @options)
  226. .should.equal true
  227. it "should file the updates with the user info", ->
  228. @UpdatesManager.fillUserInfo
  229. .calledWith(@updates)
  230. .should.equal true
  231. it "should return the updates with the filled details", ->
  232. @callback.calledWith(null, @updatesWithUserInfo).should.equal true
  233. describe "_extendBatchOfSummarizedUpdates", ->
  234. beforeEach ->
  235. @before = Date.now()
  236. @min_count = 2
  237. @existingSummarizedUpdates = ["summarized-updates-3"]
  238. @summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"]
  239. describe "when there are updates to get", ->
  240. beforeEach ->
  241. @updates = [
  242. {op: "mock-op-1", meta: end_ts: @before - 10},
  243. {op: "mock-op-1", meta: end_ts: @nextBeforeTimestamp = @before - 20}
  244. ]
  245. @existingSummarizedUpdates = ["summarized-updates-3"]
  246. @summarizedUpdates = ["summarized-updates-3", "summarized-update-2", "summarized-update-1"]
  247. @UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates)
  248. @UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates)
  249. @UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback
  250. it "should get the updates", ->
  251. @UpdatesManager.getProjectUpdatesWithUserInfo
  252. .calledWith(@project_id, { before: @before, limit: 3 * @min_count })
  253. .should.equal true
  254. it "should summarize the updates", ->
  255. @UpdatesManager._summarizeUpdates
  256. .calledWith(@updates, @existingSummarizedUpdates)
  257. .should.equal true
  258. it "should call the callback with the summarized updates and the next before timestamp", ->
  259. @callback.calledWith(null, @summarizedUpdates, @nextBeforeTimestamp).should.equal true
  260. describe "when there are no more updates", ->
  261. beforeEach ->
  262. @updates = []
  263. @UpdatesManager._summarizeUpdates = sinon.stub().returns(@summarizedUpdates)
  264. @UpdatesManager.getProjectUpdatesWithUserInfo = sinon.stub().callsArgWith(2, null, @updates)
  265. @UpdatesManager._extendBatchOfSummarizedUpdates @project_id, @existingSummarizedUpdates, @before, @min_count, @callback
  266. it "should call the callback with the summarized updates and null for nextBeforeTimestamp", ->
  267. @callback.calledWith(null, @summarizedUpdates, null).should.equal true
  268. describe "getSummarizedProjectUpdates", ->
  269. describe "when one batch of updates is enough to meet the limit", ->
  270. beforeEach ->
  271. @before = Date.now()
  272. @min_count = 2
  273. @updates = ["summarized-updates-3", "summarized-updates-2"]
  274. @nextBeforeTimestamp = @before - 100
  275. @UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, @nextBeforeTimestamp)
  276. @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
  277. it "should get the batch of summarized updates", ->
  278. @UpdatesManager._extendBatchOfSummarizedUpdates
  279. .calledWith(@project_id, [], @before, @min_count)
  280. .should.equal true
  281. it "should call the callback with the updates", ->
  282. @callback.calledWith(null, @updates, @nextBeforeTimestamp).should.equal true
  283. describe "when multiple batches are needed to meet the limit", ->
  284. beforeEach ->
  285. @before = Date.now()
  286. @min_count = 4
  287. @firstBatch = [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }]
  288. @nextBeforeTimestamp = @before - 100
  289. @secondBatch = [{ toV: 4, fromV: 4 }, { toV: 3, fromV: 3 }]
  290. @nextNextBeforeTimestamp = @before - 200
  291. @UpdatesManager._extendBatchOfSummarizedUpdates = (project_id, existingUpdates, before, desiredLength, callback) =>
  292. if existingUpdates.length == 0
  293. callback null, @firstBatch, @nextBeforeTimestamp
  294. else
  295. callback null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp
  296. sinon.spy @UpdatesManager, "_extendBatchOfSummarizedUpdates"
  297. @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
  298. it "should get the first batch of summarized updates", ->
  299. @UpdatesManager._extendBatchOfSummarizedUpdates
  300. .calledWith(@project_id, [], @before, @min_count)
  301. .should.equal true
  302. it "should get the second batch of summarized updates", ->
  303. @UpdatesManager._extendBatchOfSummarizedUpdates
  304. .calledWith(@project_id, @firstBatch, @nextBeforeTimestamp, @min_count)
  305. .should.equal true
  306. it "should call the callback with all the updates", ->
  307. @callback.calledWith(null, @firstBatch.concat(@secondBatch), @nextNextBeforeTimestamp).should.equal true
  308. describe "when the end of the database is hit", ->
  309. beforeEach ->
  310. @before = Date.now()
  311. @min_count = 4
  312. @updates = [{ toV: 6, fromV: 6 }, { toV: 5, fromV: 5 }]
  313. @UpdatesManager._extendBatchOfSummarizedUpdates = sinon.stub().callsArgWith(4, null, @updates, null)
  314. @UpdatesManager.getSummarizedProjectUpdates @project_id, { before: @before, min_count: @min_count }, @callback
  315. it "should get the batch of summarized updates", ->
  316. @UpdatesManager._extendBatchOfSummarizedUpdates
  317. .calledWith(@project_id, [], @before, @min_count)
  318. .should.equal true
  319. it "should call the callback with the updates", ->
  320. @callback.calledWith(null, @updates, null).should.equal true
  321. describe "fillUserInfo", ->
  322. describe "with valid users", ->
  323. beforeEach (done) ->
  324. {ObjectId} = require "mongojs"
  325. @user_id_1 = ObjectId().toString()
  326. @user_id_2 = ObjectId().toString()
  327. @updates = [{
  328. meta:
  329. user_id: @user_id_1
  330. op: "mock-op-1"
  331. }, {
  332. meta:
  333. user_id: @user_id_1
  334. op: "mock-op-2"
  335. }, {
  336. meta:
  337. user_id: @user_id_2
  338. op: "mock-op-3"
  339. }]
  340. @user_info = {}
  341. @user_info[@user_id_1] = email: "user1@sharelatex.com"
  342. @user_info[@user_id_2] = email: "user2@sharelatex.com"
  343. @WebApiManager.getUserInfo = (user_id, callback = (error, userInfo) ->) =>
  344. callback null, @user_info[user_id]
  345. sinon.spy @WebApiManager, "getUserInfo"
  346. @UpdatesManager.fillUserInfo @updates, (error, @results) =>
  347. done()
  348. it "should only call getUserInfo once for each user_id", ->
  349. @WebApiManager.getUserInfo.calledTwice.should.equal true
  350. @WebApiManager.getUserInfo
  351. .calledWith(@user_id_1)
  352. .should.equal true
  353. @WebApiManager.getUserInfo
  354. .calledWith(@user_id_2)
  355. .should.equal true
  356. it "should return the updates with the user info filled", ->
  357. expect(@results).to.deep.equal [{
  358. meta:
  359. user:
  360. email: "user1@sharelatex.com"
  361. op: "mock-op-1"
  362. }, {
  363. meta:
  364. user:
  365. email: "user1@sharelatex.com"
  366. op: "mock-op-2"
  367. }, {
  368. meta:
  369. user:
  370. email: "user2@sharelatex.com"
  371. op: "mock-op-3"
  372. }]
  373. describe "with invalid user ids", ->
  374. beforeEach (done) ->
  375. @updates = [{
  376. meta:
  377. user_id: null
  378. op: "mock-op-1"
  379. }, {
  380. meta:
  381. user_id: "anonymous-user"
  382. op: "mock-op-2"
  383. }]
  384. @WebApiManager.getUserInfo = (user_id, callback = (error, userInfo) ->) =>
  385. callback null, @user_info[user_id]
  386. sinon.spy @WebApiManager, "getUserInfo"
  387. @UpdatesManager.fillUserInfo @updates, (error, @results) =>
  388. done()
  389. it "should not call getUserInfo", ->
  390. @WebApiManager.getUserInfo.called.should.equal false
  391. it "should return the updates without the user info filled", ->
  392. expect(@results).to.deep.equal [{
  393. meta: {}
  394. op: "mock-op-1"
  395. }, {
  396. meta: {}
  397. op: "mock-op-2"
  398. }]
  399. describe "_summarizeUpdates", ->
  400. beforeEach ->
  401. @now = Date.now()
  402. @user_1 = { id: "mock-user-1" }
  403. @user_2 = { id: "mock-user-2" }
  404. it "should concat updates that are close in time", ->
  405. result = @UpdatesManager._summarizeUpdates [{
  406. doc_id: "doc-id-1"
  407. meta:
  408. user: @user_1
  409. start_ts: @now + 20
  410. end_ts: @now + 30
  411. v: 5
  412. }, {
  413. doc_id: "doc-id-1"
  414. meta:
  415. user: @user_2
  416. start_ts: @now
  417. end_ts: @now + 10
  418. v: 4
  419. }]
  420. expect(result).to.deep.equal [{
  421. docs:
  422. "doc-id-1":
  423. fromV: 4
  424. toV: 5
  425. meta:
  426. users: [@user_1, @user_2]
  427. start_ts: @now
  428. end_ts: @now + 30
  429. }]
  430. it "should leave updates that are far apart in time", ->
  431. oneDay = 1000 * 60 * 60 * 24
  432. result = @UpdatesManager._summarizeUpdates [{
  433. doc_id: "doc-id-1"
  434. meta:
  435. user: @user_2
  436. start_ts: @now + oneDay
  437. end_ts: @now + oneDay + 10
  438. v: 5
  439. }, {
  440. doc_id: "doc-id-1"
  441. meta:
  442. user: @user_1
  443. start_ts: @now
  444. end_ts: @now + 10
  445. v: 4
  446. }]
  447. expect(result).to.deep.equal [{
  448. docs:
  449. "doc-id-1":
  450. fromV: 5
  451. toV: 5
  452. meta:
  453. users: [@user_2]
  454. start_ts: @now + oneDay
  455. end_ts: @now + oneDay + 10
  456. }, {
  457. docs:
  458. "doc-id-1":
  459. fromV: 4
  460. toV: 4
  461. meta:
  462. users: [@user_1]
  463. start_ts: @now
  464. end_ts: @now + 10
  465. }]
  466. it "should concat onto existing summarized updates", ->
  467. result = @UpdatesManager._summarizeUpdates [{
  468. doc_id: "doc-id-2"
  469. meta:
  470. user: @user_1
  471. start_ts: @now + 20
  472. end_ts: @now + 30
  473. v: 5
  474. }, {
  475. doc_id: "doc-id-2"
  476. meta:
  477. user: @user_2
  478. start_ts: @now
  479. end_ts: @now + 10
  480. v: 4
  481. }], [{
  482. docs:
  483. "doc-id-1":
  484. fromV: 6
  485. toV: 8
  486. meta:
  487. users: [@user_1]
  488. start_ts: @now + 40
  489. end_ts: @now + 50
  490. }]
  491. expect(result).to.deep.equal [{
  492. docs:
  493. "doc-id-1":
  494. toV: 8
  495. fromV: 6
  496. "doc-id-2":
  497. toV: 5
  498. fromV: 4
  499. meta:
  500. users: [@user_1, @user_2]
  501. start_ts: @now
  502. end_ts: @now + 50
  503. }]