UpdatesManagerTests.coffee 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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. "logger-sharelatex": { log: sinon.stub(), error: sinon.stub() }
  15. @doc_id = "doc-id-123"
  16. @callback = sinon.stub()
  17. describe "compressAndSaveRawUpdates", ->
  18. describe "when there are no raw ops", ->
  19. beforeEach ->
  20. @MongoManager.popLastCompressedUpdate = sinon.stub()
  21. @MongoManager.insertCompressedUpdates = sinon.stub()
  22. @UpdatesManager.compressAndSaveRawUpdates @doc_id, [], @callback
  23. it "should not need to access the database", ->
  24. @MongoManager.popLastCompressedUpdate.called.should.equal false
  25. @MongoManager.insertCompressedUpdates.called.should.equal false
  26. it "should call the callback", ->
  27. @callback.called.should.equal true
  28. describe "when there is no compressed history to begin with", ->
  29. beforeEach ->
  30. @rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]
  31. @compressedUpdates = { v: 13, op: "compressed-op-12" }
  32. @MongoManager.popLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null)
  33. @MongoManager.insertCompressedUpdates = sinon.stub().callsArg(2)
  34. @UpdateCompressor.compressRawUpdates = sinon.stub().returns(@compressedUpdates)
  35. @UpdatesManager.compressAndSaveRawUpdates @doc_id, @rawUpdates, @callback
  36. it "should try to pop the last compressed op", ->
  37. @MongoManager.popLastCompressedUpdate
  38. .calledWith(@doc_id)
  39. .should.equal true
  40. it "should compress the raw ops", ->
  41. @UpdateCompressor.compressRawUpdates
  42. .calledWith(null, @rawUpdates)
  43. .should.equal true
  44. it "should save the compressed ops", ->
  45. @MongoManager.insertCompressedUpdates
  46. .calledWith(@doc_id, @compressedUpdates)
  47. .should.equal true
  48. it "should call the callback", ->
  49. @callback.called.should.equal true
  50. describe "when the raw ops need appending to existing history", ->
  51. beforeEach ->
  52. @lastCompressedUpdate = { v: 11, op: "compressed-op-11" }
  53. @compressedUpdates = { v: 13, op: "compressed-op-12" }
  54. @MongoManager.popLastCompressedUpdate = sinon.stub().callsArgWith(1, null, @lastCompressedUpdate)
  55. @MongoManager.insertCompressedUpdates = sinon.stub().callsArg(2)
  56. @UpdateCompressor.compressRawUpdates = sinon.stub().returns(@compressedUpdates)
  57. describe "when the raw ops start where the existing history ends", ->
  58. beforeEach ->
  59. @rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]
  60. @UpdatesManager.compressAndSaveRawUpdates @doc_id, @rawUpdates, @callback
  61. it "should try to pop the last compressed op", ->
  62. @MongoManager.popLastCompressedUpdate
  63. .calledWith(@doc_id)
  64. .should.equal true
  65. it "should compress the last compressed op and the raw ops", ->
  66. @UpdateCompressor.compressRawUpdates
  67. .calledWith(@lastCompressedUpdate, @rawUpdates)
  68. .should.equal true
  69. it "should save the compressed ops", ->
  70. @MongoManager.insertCompressedUpdates
  71. .calledWith(@doc_id, @compressedUpdates)
  72. .should.equal true
  73. it "should call the callback", ->
  74. @callback.called.should.equal true
  75. describe "when some raw ops are passed that have already been compressed", ->
  76. beforeEach ->
  77. @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" }]
  78. @UpdatesManager.compressAndSaveRawUpdates @doc_id, @rawUpdates, @callback
  79. it "should only compress the more recent raw ops", ->
  80. @UpdateCompressor.compressRawUpdates
  81. .calledWith(@lastCompressedUpdate, @rawUpdates.slice(-2))
  82. .should.equal true
  83. describe "when the raw ops do not follow from the last compressed op version", ->
  84. beforeEach ->
  85. @rawUpdates = [{ v: 13, op: "mock-op-13" }]
  86. @UpdatesManager.compressAndSaveRawUpdates @doc_id, @rawUpdates, @callback
  87. it "should call the callback with an error", ->
  88. @callback
  89. .calledWith(new Error("Tried to apply raw op at version 13 to last compressed update with version 11"))
  90. .should.equal true
  91. it "should put the popped update back into mongo", ->
  92. @MongoManager.insertCompressedUpdates.calledOnce.should.equal true
  93. @MongoManager.insertCompressedUpdates
  94. .calledWith(@doc_id, [@lastCompressedUpdate])
  95. .should.equal true
  96. describe "processUncompressedUpdates", ->
  97. describe "when there is fewer than one batch to send", ->
  98. beforeEach ->
  99. @updates = ["mock-update"]
  100. @RedisManager.getOldestRawUpdates = sinon.stub().callsArgWith(2, null, @updates)
  101. @UpdatesManager.compressAndSaveRawUpdates = sinon.stub().callsArgWith(2)
  102. @RedisManager.deleteOldestRawUpdates = sinon.stub().callsArg(2)
  103. @UpdatesManager.processUncompressedUpdates @doc_id, @callback
  104. it "should get the oldest updates", ->
  105. @RedisManager.getOldestRawUpdates
  106. .calledWith(@doc_id, @UpdatesManager.REDIS_READ_BATCH_SIZE)
  107. .should.equal true
  108. it "should compress and save the updates", ->
  109. @UpdatesManager.compressAndSaveRawUpdates
  110. .calledWith(@doc_id, @updates)
  111. .should.equal true
  112. it "should delete the batch of uncompressed updates that was just processed", ->
  113. @RedisManager.deleteOldestRawUpdates
  114. .calledWith(@doc_id, @updates.length)
  115. .should.equal true
  116. it "should call the callback", ->
  117. @callback.called.should.equal true
  118. describe "when there are multiple batches to send", ->
  119. beforeEach (done) ->
  120. @UpdatesManager.REDIS_READ_BATCH_SIZE = 2
  121. @updates = ["mock-update-0", "mock-update-1", "mock-update-2", "mock-update-3", "mock-update-4"]
  122. @redisArray = @updates.slice()
  123. @RedisManager.getOldestRawUpdates = (doc_id, batchSize, callback = (error, updates) ->) =>
  124. updates = @redisArray.slice(0, batchSize)
  125. @redisArray = @redisArray.slice(batchSize)
  126. callback null, updates
  127. sinon.spy @RedisManager, "getOldestRawUpdates"
  128. @UpdatesManager.compressAndSaveRawUpdates = sinon.stub().callsArgWith(2)
  129. @RedisManager.deleteOldestRawUpdates = sinon.stub().callsArg(2)
  130. @UpdatesManager.processUncompressedUpdates @doc_id, (args...) =>
  131. @callback(args...)
  132. done()
  133. it "should get the oldest updates in three batches ", ->
  134. @RedisManager.getOldestRawUpdates.callCount.should.equal 3
  135. it "should compress and save the updates in batches", ->
  136. @UpdatesManager.compressAndSaveRawUpdates
  137. .calledWith(@doc_id, @updates.slice(0,2))
  138. .should.equal true
  139. @UpdatesManager.compressAndSaveRawUpdates
  140. .calledWith(@doc_id, @updates.slice(2,4))
  141. .should.equal true
  142. @UpdatesManager.compressAndSaveRawUpdates
  143. .calledWith(@doc_id, @updates.slice(4,5))
  144. .should.equal true
  145. it "should delete the batches of uncompressed updates", ->
  146. @RedisManager.deleteOldestRawUpdates.callCount.should.equal 3
  147. it "should call the callback", ->
  148. @callback.called.should.equal true
  149. describe "processCompressedUpdatesWithLock", ->
  150. beforeEach ->
  151. @UpdatesManager.processUncompressedUpdates = sinon.stub().callsArg(2)
  152. @LockManager.runWithLock = sinon.stub().callsArg(2)
  153. @UpdatesManager.processUncompressedUpdatesWithLock @doc_id, @callback
  154. it "should run processUncompressedUpdates with the lock", ->
  155. @LockManager.runWithLock
  156. .calledWith(
  157. "HistoryLock:#{@doc_id}"
  158. )
  159. .should.equal true
  160. it "should call the callback", ->
  161. @callback.called.should.equal true
  162. describe "getUpdates", ->
  163. beforeEach ->
  164. @updates = ["mock-updates"]
  165. @options = { to: "mock-to", limit: "mock-limit" }
  166. @MongoManager.getUpdates = sinon.stub().callsArgWith(2, null, @updates)
  167. @UpdatesManager.processUncompressedUpdatesWithLock = sinon.stub().callsArg(1)
  168. @UpdatesManager.getUpdates @doc_id, @options, @callback
  169. it "should process outstanding updates", ->
  170. @UpdatesManager.processUncompressedUpdatesWithLock
  171. .calledWith(@doc_id)
  172. .should.equal true
  173. it "should get the updates from the database", ->
  174. @MongoManager.getUpdates
  175. .calledWith(@doc_id, @options)
  176. .should.equal true
  177. it "should return the updates", ->
  178. @callback
  179. .calledWith(null, @updates)
  180. .should.equal true