DocOpsManagerTests.coffee 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. sinon = require('sinon')
  2. chai = require('chai')
  3. should = chai.should()
  4. modulePath = "../../../../app/js/DocOpsManager.js"
  5. SandboxedModule = require('sandboxed-module')
  6. ObjectId = require("../../../../app/js/mongojs").ObjectId
  7. describe "DocOpsManager", ->
  8. beforeEach ->
  9. @doc_id = ObjectId().toString()
  10. @project_id = "project-id"
  11. @callback = sinon.stub()
  12. @DocOpsManager = SandboxedModule.require modulePath, requires:
  13. "./RedisManager": @RedisManager = {}
  14. "./mongojs":
  15. db: @db = { docOps: {} }
  16. ObjectId: ObjectId
  17. "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
  18. "./Metrics": @Metrics =
  19. Timer: class Timer
  20. done: sinon.stub()
  21. describe "flushDocOpsToMongo", ->
  22. describe "when versions are consistent", ->
  23. beforeEach ->
  24. @mongo_version = 40
  25. @redis_version = 42
  26. @ops = [ "mock-op-1", "mock-op-2" ]
  27. @DocOpsManager.getDocVersionInMongo = sinon.stub().callsArgWith(1, null, @mongo_version)
  28. @RedisManager.getDocVersion = sinon.stub().callsArgWith(1, null, @redis_version)
  29. @RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
  30. @DocOpsManager._appendDocOpsInMongo = sinon.stub().callsArg(3)
  31. @DocOpsManager.flushDocOpsToMongo @project_id, @doc_id, @callback
  32. it "should get the version from Mongo", ->
  33. @DocOpsManager.getDocVersionInMongo
  34. .calledWith(@doc_id)
  35. .should.equal true
  36. it "should get the version from REdis", ->
  37. @RedisManager.getDocVersion
  38. .calledWith(@doc_id)
  39. .should.equal true
  40. it "should get all doc ops since the version in Mongo", ->
  41. @RedisManager.getPreviousDocOps
  42. .calledWith(@doc_id, @mongo_version, -1)
  43. .should.equal true
  44. it "should update Mongo with the new ops", ->
  45. @DocOpsManager._appendDocOpsInMongo
  46. .calledWith(@doc_id, @ops, @redis_version)
  47. .should.equal true
  48. it "should call the callback", ->
  49. @callback.called.should.equal true
  50. it "should time the execution", ->
  51. @Metrics.Timer::done.called.should.equal true
  52. describe "when the number of ops does not match the difference in versions", ->
  53. beforeEach ->
  54. @mongo_version = 40
  55. @redis_version = 45
  56. @ops = [ "mock-op-1", "mock-op-2" ]
  57. @DocOpsManager.getDocVersionInMongo = sinon.stub().callsArgWith(1, null, @mongo_version)
  58. @RedisManager.getDocVersion = sinon.stub().callsArgWith(1, null, @redis_version)
  59. @RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
  60. @DocOpsManager._appendDocOpsInMongo = sinon.stub().callsArg(3)
  61. @DocOpsManager.flushDocOpsToMongo @project_id, @doc_id, @callback
  62. it "should call the callback with an error", ->
  63. @callback.calledWith(new Error("inconsistet versions")).should.equal true
  64. it "should log an error", ->
  65. @logger.error
  66. .calledWith(doc_id: @doc_id, mongoVersion: @mongo_version, redisVersion: @redis_version, opsLength: @ops.length, "version difference does not match ops length")
  67. .should.equal true
  68. it "should not modify mongo", ->
  69. @DocOpsManager._appendDocOpsInMongo.called.should.equal false
  70. it "should time the execution", ->
  71. @Metrics.Timer::done.called.should.equal true
  72. describe "when redis version is behind mongo version", ->
  73. beforeEach ->
  74. @mongo_version = 40
  75. @redis_version = 30
  76. @DocOpsManager.getDocVersionInMongo = sinon.stub().callsArgWith(1, null, @mongo_version)
  77. @RedisManager.getDocVersion = sinon.stub().callsArgWith(1, null, @redis_version)
  78. @RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
  79. @DocOpsManager._appendDocOpsInMongo = sinon.stub().callsArg(3)
  80. @DocOpsManager.flushDocOpsToMongo @project_id, @doc_id, @callback
  81. it "should call the callback with an error", ->
  82. @callback.calledWith(new Error("inconsistet versions")).should.equal true
  83. it "should log an error", ->
  84. @logger.error
  85. .calledWith(doc_id: @doc_id, mongoVersion: @mongo_version, redisVersion: @redis_version, "mongo version is ahead of redis")
  86. .should.equal true
  87. it "should not modify mongo", ->
  88. @DocOpsManager._appendDocOpsInMongo.called.should.equal false
  89. it "should time the execution", ->
  90. @Metrics.Timer::done.called.should.equal true
  91. describe "getPreviousDocOps", ->
  92. beforeEach ->
  93. @ops = [ "mock-op-1", "mock-op-2" ]
  94. @start = 30
  95. @end = 32
  96. @RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
  97. @DocOpsManager._ensureOpsAreLoaded = sinon.stub().callsArg(3)
  98. @DocOpsManager.getPreviousDocOps @project_id, @doc_id, @start, @end, @callback
  99. it "should ensure the ops are loaded back far enough", ->
  100. @DocOpsManager._ensureOpsAreLoaded
  101. .calledWith(@project_id, @doc_id, @start)
  102. .should.equal true
  103. it "should get the previous doc ops", ->
  104. @RedisManager.getPreviousDocOps
  105. .calledWith(@doc_id, @start, @end)
  106. .should.equal true
  107. it "should call the callback with the ops", ->
  108. @callback.calledWith(null, @ops).should.equal true
  109. it "should time the execution", ->
  110. @Metrics.Timer::done.called.should.equal true
  111. describe "_ensureOpsAreLoaded", ->
  112. describe "when the ops are not loaded", ->
  113. beforeEach ->
  114. @redisVersion = 42
  115. @redisOpsLength = 10
  116. @backToVersion = 30
  117. @ops = [ "mock-op-1", "mock-op-2" ]
  118. @RedisManager.getDocVersion = sinon.stub().callsArgWith(1, null, @redisVersion)
  119. @RedisManager.getDocOpsLength = sinon.stub().callsArgWith(1, null, @redisOpsLength)
  120. @DocOpsManager._getDocOpsFromMongo = sinon.stub().callsArgWith(3, null, @ops)
  121. @RedisManager.prependDocOps = sinon.stub().callsArgWith(2, null)
  122. @DocOpsManager._ensureOpsAreLoaded @project_id, @doc_id, @backToVersion, @callback
  123. it "should get the doc version from redis", ->
  124. @RedisManager.getDocVersion
  125. .calledWith(@doc_id)
  126. .should.equal true
  127. it "should get the doc ops length in redis", ->
  128. @RedisManager.getDocOpsLength
  129. .calledWith(@doc_id)
  130. .should.equal true
  131. it "should get the doc ops that need loading from Mongo", ->
  132. @DocOpsManager._getDocOpsFromMongo
  133. .calledWith(@doc_id, @backToVersion, @redisVersion - @redisOpsLength)
  134. .should.equal true
  135. it "should prepend the retrieved ops to redis", ->
  136. @RedisManager.prependDocOps
  137. .calledWith(@doc_id, @ops)
  138. .should.equal true
  139. it "should call the callback", ->
  140. @callback.called.should.equal true
  141. describe "when the ops are loaded", ->
  142. beforeEach ->
  143. @redisVersion = 42
  144. @redisOpsLength = 10
  145. @backToVersion = 35
  146. @RedisManager.getDocVersion = sinon.stub().callsArgWith(1, null, @redisVersion)
  147. @RedisManager.getDocOpsLength = sinon.stub().callsArgWith(1, null, @redisOpsLength)
  148. @DocOpsManager._getDocOpsFromMongo = sinon.stub().callsArgWith(3, null, @ops)
  149. @RedisManager.prependDocOps = sinon.stub().callsArgWith(2, null)
  150. @DocOpsManager._ensureOpsAreLoaded @project_id, @doc_id, @backToVersion, @callback
  151. it "should not need to get the docs from Mongo or put any into redis", ->
  152. @DocOpsManager._getDocOpsFromMongo.called.should.equal false
  153. @RedisManager.prependDocOps.called.should.equal false
  154. it "should call the callback", ->
  155. @callback.called.should.equal true
  156. describe "getDocVersionInMongo", ->
  157. describe "when the doc exists", ->
  158. beforeEach ->
  159. @doc =
  160. version: @version = 42
  161. @db.docOps.find = sinon.stub().callsArgWith(2, null, [@doc])
  162. @DocOpsManager.getDocVersionInMongo @doc_id, @callback
  163. it "should look for the doc in the database", ->
  164. @db.docOps.find
  165. .calledWith({ doc_id: ObjectId(@doc_id) }, {version: 1})
  166. .should.equal true
  167. it "should call the callback with the version", ->
  168. @callback.calledWith(null, @version).should.equal true
  169. describe "when the doc doesn't exist", ->
  170. beforeEach ->
  171. @db.docOps.find = sinon.stub().callsArgWith(2, null, [])
  172. @DocOpsManager.getDocVersionInMongo @doc_id, @callback
  173. it "should call the callback with 0", ->
  174. @callback.calledWith(null, 0).should.equal true
  175. describe "_appendDocOpsInMongo", ->
  176. describe "with a small set of updates", ->
  177. beforeEach (done) ->
  178. @ops = [ "mock-op-1", "mock-op-2" ]
  179. @version = 42
  180. @db.docOps.update = sinon.stub().callsArg(3)
  181. @DocOpsManager._appendDocOpsInMongo @doc_id, @ops, @version, (error) =>
  182. @callback(error)
  183. done()
  184. it "should update the database", ->
  185. @db.docOps.update
  186. .calledWith({
  187. doc_id: ObjectId(@doc_id)
  188. }, {
  189. $push: docOps: { $each: @ops, $slice: -100 }
  190. $set: version: @version
  191. }, {
  192. upsert: true
  193. })
  194. .should.equal true
  195. it "should call the callbak", ->
  196. @callback.called.should.equal true
  197. describe "with a large set of updates", ->
  198. beforeEach (done) ->
  199. @ops = [ "mock-op-1", "mock-op-2", "mock-op-3", "mock-op-4", "mock-op-5" ]
  200. @version = 42
  201. @DocOpsManager.APPEND_OPS_BATCH_SIZE = 2
  202. @db.docOps.update = sinon.stub().callsArg(3)
  203. @DocOpsManager._appendDocOpsInMongo @doc_id, @ops, @version, (error) =>
  204. @callback(error)
  205. done()
  206. it "should update the database in batches", ->
  207. @db.docOps.update
  208. .calledWith({ doc_id: ObjectId(@doc_id) }, {
  209. $push: docOps: { $each: @ops.slice(0,2), $slice: -100 }
  210. $set: version: @version - 3
  211. }, { upsert: true })
  212. .should.equal true
  213. @db.docOps.update
  214. .calledWith({ doc_id: ObjectId(@doc_id) }, {
  215. $push: docOps: { $each: @ops.slice(2,4), $slice: -100 }
  216. $set: version: @version - 1
  217. }, { upsert: true })
  218. .should.equal true
  219. @db.docOps.update
  220. .calledWith({ doc_id: ObjectId(@doc_id) }, {
  221. $push: docOps: { $each: @ops.slice(4,5), $slice: -100 }
  222. $set: version: @version
  223. }, { upsert: true })
  224. .should.equal true
  225. it "should call the callbak", ->
  226. @callback.called.should.equal true
  227. describe "with no updates", ->
  228. beforeEach (done) ->
  229. @ops = []
  230. @version = 42
  231. @db.docOps.update = sinon.stub().callsArg(3)
  232. @DocOpsManager._appendDocOpsInMongo @doc_id, @ops, @version, (error) =>
  233. @callback(error)
  234. done()
  235. it "should not try to update the database", ->
  236. @db.docOps.update.called.should.equal false
  237. describe "_getDocsOpsFromMongo", ->
  238. beforeEach ->
  239. @version = 42
  240. @start = 32
  241. @limit = 5
  242. @doc =
  243. docOps: ["mock-ops"]
  244. @DocOpsManager.getDocVersionInMongo = sinon.stub().callsArgWith(1, null, @version)
  245. @db.docOps.find = sinon.stub().callsArgWith(2, null, [@doc])
  246. @DocOpsManager._getDocOpsFromMongo @doc_id, @start, @start + @limit, @callback
  247. it "should get the current version", ->
  248. @DocOpsManager.getDocVersionInMongo
  249. .calledWith(@doc_id)
  250. .should.equal true
  251. it "should get the doc ops", ->
  252. @db.docOps.find
  253. .calledWith({ doc_id: ObjectId(@doc_id) }, {
  254. docOps: $slice: [-(@version - @start), @limit]
  255. })
  256. .should.equal true
  257. it "should return the ops", ->
  258. @callback.calledWith(null, @doc.docOps).should.equal true
  259. describe "pushDocOp", ->
  260. beforeEach ->
  261. @op = "mock-op"
  262. @RedisManager.pushDocOp = sinon.stub().callsArgWith(2, null, @version = 42)
  263. @RedisManager.pushUncompressedHistoryOp = sinon.stub().callsArg(2)
  264. @DocOpsManager.pushDocOp @project_id, @doc_id, @op, @callback
  265. it "should push the op in to the docOps list", ->
  266. @RedisManager.pushDocOp
  267. .calledWith(@doc_id, @op)
  268. .should.equal true
  269. it "should push the op into the pushUncompressedHistoryOp", ->
  270. @RedisManager.pushUncompressedHistoryOp
  271. .calledWith(@doc_id, @op)
  272. .should.equal true
  273. it "should call the callback with the version", ->
  274. @callback.calledWith(null, @version).should.equal true