UpdatesManagerTests.coffee 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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. @callback = sinon.stub()
  18. describe "compressAndSaveRawUpdates", ->
  19. describe "when there are no raw ops", ->
  20. beforeEach ->
  21. @MongoManager.popLastCompressedUpdate = sinon.stub()
  22. @MongoManager.insertCompressedUpdates = sinon.stub()
  23. @UpdatesManager.compressAndSaveRawUpdates @doc_id, [], @callback
  24. it "should not need to access the database", ->
  25. @MongoManager.popLastCompressedUpdate.called.should.equal false
  26. @MongoManager.insertCompressedUpdates.called.should.equal false
  27. it "should call the callback", ->
  28. @callback.called.should.equal true
  29. describe "when there is no compressed history to begin with", ->
  30. beforeEach ->
  31. @rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]
  32. @compressedUpdates = { v: 13, op: "compressed-op-12" }
  33. @MongoManager.popLastCompressedUpdate = sinon.stub().callsArgWith(1, null, null)
  34. @MongoManager.insertCompressedUpdates = sinon.stub().callsArg(2)
  35. @UpdateCompressor.compressRawUpdates = sinon.stub().returns(@compressedUpdates)
  36. @UpdatesManager.compressAndSaveRawUpdates @doc_id, @rawUpdates, @callback
  37. it "should try to pop the last compressed op", ->
  38. @MongoManager.popLastCompressedUpdate
  39. .calledWith(@doc_id)
  40. .should.equal true
  41. it "should compress the raw ops", ->
  42. @UpdateCompressor.compressRawUpdates
  43. .calledWith(null, @rawUpdates)
  44. .should.equal true
  45. it "should save the compressed ops", ->
  46. @MongoManager.insertCompressedUpdates
  47. .calledWith(@doc_id, @compressedUpdates)
  48. .should.equal true
  49. it "should call the callback", ->
  50. @callback.called.should.equal true
  51. describe "when the raw ops need appending to existing history", ->
  52. beforeEach ->
  53. @lastCompressedUpdate = { v: 11, op: "compressed-op-11" }
  54. @compressedUpdates = { v: 13, op: "compressed-op-12" }
  55. @MongoManager.popLastCompressedUpdate = sinon.stub().callsArgWith(1, null, @lastCompressedUpdate)
  56. @MongoManager.insertCompressedUpdates = sinon.stub().callsArg(2)
  57. @UpdateCompressor.compressRawUpdates = sinon.stub().returns(@compressedUpdates)
  58. describe "when the raw ops start where the existing history ends", ->
  59. beforeEach ->
  60. @rawUpdates = [{ v: 12, op: "mock-op-12" }, { v: 13, op: "mock-op-13" }]
  61. @UpdatesManager.compressAndSaveRawUpdates @doc_id, @rawUpdates, @callback
  62. it "should try to pop the last compressed op", ->
  63. @MongoManager.popLastCompressedUpdate
  64. .calledWith(@doc_id)
  65. .should.equal true
  66. it "should compress the last compressed op and the raw ops", ->
  67. @UpdateCompressor.compressRawUpdates
  68. .calledWith(@lastCompressedUpdate, @rawUpdates)
  69. .should.equal true
  70. it "should save the compressed ops", ->
  71. @MongoManager.insertCompressedUpdates
  72. .calledWith(@doc_id, @compressedUpdates)
  73. .should.equal true
  74. it "should call the callback", ->
  75. @callback.called.should.equal true
  76. describe "when some raw ops are passed that have already been compressed", ->
  77. beforeEach ->
  78. @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" }]
  79. @UpdatesManager.compressAndSaveRawUpdates @doc_id, @rawUpdates, @callback
  80. it "should only compress the more recent raw ops", ->
  81. @UpdateCompressor.compressRawUpdates
  82. .calledWith(@lastCompressedUpdate, @rawUpdates.slice(-2))
  83. .should.equal true
  84. describe "when the raw ops do not follow from the last compressed op version", ->
  85. beforeEach ->
  86. @rawUpdates = [{ v: 13, op: "mock-op-13" }]
  87. @UpdatesManager.compressAndSaveRawUpdates @doc_id, @rawUpdates, @callback
  88. it "should call the callback with an error", ->
  89. @callback
  90. .calledWith(new Error("Tried to apply raw op at version 13 to last compressed update with version 11"))
  91. .should.equal true
  92. it "should put the popped update back into mongo", ->
  93. @MongoManager.insertCompressedUpdates.calledOnce.should.equal true
  94. @MongoManager.insertCompressedUpdates
  95. .calledWith(@doc_id, [@lastCompressedUpdate])
  96. .should.equal true
  97. describe "processUncompressedUpdates", ->
  98. describe "when there is fewer than one batch to send", ->
  99. beforeEach ->
  100. @updates = ["mock-update"]
  101. @RedisManager.getOldestRawUpdates = sinon.stub().callsArgWith(2, null, @updates)
  102. @UpdatesManager.compressAndSaveRawUpdates = sinon.stub().callsArgWith(2)
  103. @RedisManager.deleteOldestRawUpdates = sinon.stub().callsArg(2)
  104. @UpdatesManager.processUncompressedUpdates @doc_id, @callback
  105. it "should get the oldest updates", ->
  106. @RedisManager.getOldestRawUpdates
  107. .calledWith(@doc_id, @UpdatesManager.REDIS_READ_BATCH_SIZE)
  108. .should.equal true
  109. it "should compress and save the updates", ->
  110. @UpdatesManager.compressAndSaveRawUpdates
  111. .calledWith(@doc_id, @updates)
  112. .should.equal true
  113. it "should delete the batch of uncompressed updates that was just processed", ->
  114. @RedisManager.deleteOldestRawUpdates
  115. .calledWith(@doc_id, @updates.length)
  116. .should.equal true
  117. it "should call the callback", ->
  118. @callback.called.should.equal true
  119. describe "when there are multiple batches to send", ->
  120. beforeEach (done) ->
  121. @UpdatesManager.REDIS_READ_BATCH_SIZE = 2
  122. @updates = ["mock-update-0", "mock-update-1", "mock-update-2", "mock-update-3", "mock-update-4"]
  123. @redisArray = @updates.slice()
  124. @RedisManager.getOldestRawUpdates = (doc_id, batchSize, callback = (error, updates) ->) =>
  125. updates = @redisArray.slice(0, batchSize)
  126. @redisArray = @redisArray.slice(batchSize)
  127. callback null, updates
  128. sinon.spy @RedisManager, "getOldestRawUpdates"
  129. @UpdatesManager.compressAndSaveRawUpdates = sinon.stub().callsArgWith(2)
  130. @RedisManager.deleteOldestRawUpdates = sinon.stub().callsArg(2)
  131. @UpdatesManager.processUncompressedUpdates @doc_id, (args...) =>
  132. @callback(args...)
  133. done()
  134. it "should get the oldest updates in three batches ", ->
  135. @RedisManager.getOldestRawUpdates.callCount.should.equal 3
  136. it "should compress and save the updates in batches", ->
  137. @UpdatesManager.compressAndSaveRawUpdates
  138. .calledWith(@doc_id, @updates.slice(0,2))
  139. .should.equal true
  140. @UpdatesManager.compressAndSaveRawUpdates
  141. .calledWith(@doc_id, @updates.slice(2,4))
  142. .should.equal true
  143. @UpdatesManager.compressAndSaveRawUpdates
  144. .calledWith(@doc_id, @updates.slice(4,5))
  145. .should.equal true
  146. it "should delete the batches of uncompressed updates", ->
  147. @RedisManager.deleteOldestRawUpdates.callCount.should.equal 3
  148. it "should call the callback", ->
  149. @callback.called.should.equal true
  150. describe "processCompressedUpdatesWithLock", ->
  151. beforeEach ->
  152. @UpdatesManager.processUncompressedUpdates = sinon.stub().callsArg(2)
  153. @LockManager.runWithLock = sinon.stub().callsArg(2)
  154. @UpdatesManager.processUncompressedUpdatesWithLock @doc_id, @callback
  155. it "should run processUncompressedUpdates with the lock", ->
  156. @LockManager.runWithLock
  157. .calledWith(
  158. "HistoryLock:#{@doc_id}"
  159. )
  160. .should.equal true
  161. it "should call the callback", ->
  162. @callback.called.should.equal true
  163. describe "getUpdates", ->
  164. beforeEach ->
  165. @updates = ["mock-updates"]
  166. @options = { to: "mock-to", limit: "mock-limit" }
  167. @MongoManager.getUpdates = sinon.stub().callsArgWith(2, null, @updates)
  168. @UpdatesManager.processUncompressedUpdatesWithLock = sinon.stub().callsArg(1)
  169. @UpdatesManager.getUpdates @doc_id, @options, @callback
  170. it "should process outstanding updates", ->
  171. @UpdatesManager.processUncompressedUpdatesWithLock
  172. .calledWith(@doc_id)
  173. .should.equal true
  174. it "should get the updates from the database", ->
  175. @MongoManager.getUpdates
  176. .calledWith(@doc_id, @options)
  177. .should.equal true
  178. it "should return the updates", ->
  179. @callback
  180. .calledWith(null, @updates)
  181. .should.equal true
  182. describe "getUpdatesWithUserInfo", ->
  183. beforeEach ->
  184. @updates = ["mock-updates"]
  185. @options = { to: "mock-to", limit: "mock-limit" }
  186. @updatesWithUserInfo = ["updates-with-user-info"]
  187. @UpdatesManager.getUpdates = sinon.stub().callsArgWith(2, null, @updates)
  188. @UpdatesManager.fillUserInfo = sinon.stub().callsArgWith(1, null, @updatesWithUserInfo)
  189. @UpdatesManager.getUpdatesWithUserInfo @doc_id, @options, @callback
  190. it "should get the updates", ->
  191. @UpdatesManager.getUpdates
  192. .calledWith(@doc_id, @options)
  193. .should.equal true
  194. it "should file the updates with the user info", ->
  195. @UpdatesManager.fillUserInfo
  196. .calledWith(@updates)
  197. .should.equal true
  198. it "shoudl return the updates with the filled details", ->
  199. @callback.calledWith(null, @updatesWithUserInfo).should.equal true
  200. describe "fillUserInfo", ->
  201. beforeEach (done) ->
  202. @user_id_1 = "user-id-1"
  203. @user_id_2 = "user-id-2"
  204. @updates = [{
  205. meta:
  206. user_id: @user_id_1
  207. op: "mock-op-1"
  208. }, {
  209. meta:
  210. user_id: @user_id_1
  211. op: "mock-op-2"
  212. }, {
  213. meta:
  214. user_id: @user_id_2
  215. op: "mock-op-3"
  216. }]
  217. @user_info =
  218. "user-id-1": {
  219. email: "user1@sharelatex.com"
  220. }
  221. "user-id-2": {
  222. email: "user2@sharelatex.com"
  223. }
  224. @WebApiManager.getUserInfo = (user_id, callback = (error, userInfo) ->) =>
  225. callback null, @user_info[user_id]
  226. sinon.spy @WebApiManager, "getUserInfo"
  227. @UpdatesManager.fillUserInfo @updates, (error, @results) =>
  228. done()
  229. it "should only call getUserInfo once for each user_id", ->
  230. @WebApiManager.getUserInfo.calledTwice.should.equal true
  231. @WebApiManager.getUserInfo
  232. .calledWith(@user_id_1)
  233. .should.equal true
  234. @WebApiManager.getUserInfo
  235. .calledWith(@user_id_2)
  236. .should.equal true
  237. it "should return the updates with the user info filled", ->
  238. expect(@results).to.deep.equal [{
  239. meta:
  240. user:
  241. email: "user1@sharelatex.com"
  242. op: "mock-op-1"
  243. }, {
  244. meta:
  245. user:
  246. email: "user1@sharelatex.com"
  247. op: "mock-op-2"
  248. }, {
  249. meta:
  250. user:
  251. email: "user2@sharelatex.com"
  252. op: "mock-op-3"
  253. }]