ApplyingUpdatesToADocTests.coffee 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. sinon = require "sinon"
  2. chai = require("chai")
  3. chai.should()
  4. expect = chai.expect
  5. async = require "async"
  6. Settings = require('settings-sharelatex')
  7. rclient_history = require("redis-sharelatex").createClient(Settings.redis.history)
  8. rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
  9. Keys = Settings.redis.documentupdater.key_schema
  10. HistoryKeys = Settings.redis.history.key_schema
  11. ProjectHistoryKeys = Settings.redis.project_history.key_schema
  12. MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
  13. MockWebApi = require "./helpers/MockWebApi"
  14. DocUpdaterClient = require "./helpers/DocUpdaterClient"
  15. DocUpdaterApp = require "./helpers/DocUpdaterApp"
  16. describe "Applying updates to a doc", ->
  17. before (done) ->
  18. @lines = ["one", "two", "three"]
  19. @version = 42
  20. @update =
  21. doc: @doc_id
  22. op: [{
  23. i: "one and a half\n"
  24. p: 4
  25. }]
  26. v: @version
  27. @result = ["one", "one and a half", "two", "three"]
  28. DocUpdaterApp.ensureRunning(done)
  29. describe "when the document is not loaded", ->
  30. before (done) ->
  31. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  32. sinon.spy MockWebApi, "getDocument"
  33. @startTime = Date.now()
  34. MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
  35. DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
  36. throw error if error?
  37. setTimeout done, 200
  38. after ->
  39. MockWebApi.getDocument.restore()
  40. it "should load the document from the web API", ->
  41. MockWebApi.getDocument
  42. .calledWith(@project_id, @doc_id)
  43. .should.equal true
  44. it "should update the doc", (done) ->
  45. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  46. doc.lines.should.deep.equal @result
  47. done()
  48. it "should push the applied updates to the track changes api", (done) ->
  49. rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
  50. throw error if error?
  51. JSON.parse(updates[0]).op.should.deep.equal @update.op
  52. rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
  53. throw error if error?
  54. result.should.equal 1
  55. done()
  56. it "should push the applied updates to the project history changes api", (done) ->
  57. rclient_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
  58. throw error if error?
  59. JSON.parse(updates[0]).op.should.deep.equal @update.op
  60. done()
  61. it "should set the first op timestamp", (done) ->
  62. rclient_history.get ProjectHistoryKeys.projectHistoryFirstOpTimestamp({@project_id}), (error, result) =>
  63. throw error if error?
  64. result.should.be.within(@startTime, Date.now())
  65. @firstOpTimestamp = result
  66. done()
  67. describe "when sending another update", ->
  68. before (done) ->
  69. @second_update = Object.create(@update)
  70. @second_update.v = @version + 1
  71. DocUpdaterClient.sendUpdate @project_id, @doc_id, @second_update, (error) ->
  72. throw error if error?
  73. setTimeout done, 200
  74. it "should not change the first op timestamp", (done) ->
  75. rclient_history.get ProjectHistoryKeys.projectHistoryFirstOpTimestamp({@project_id}), (error, result) =>
  76. throw error if error?
  77. result.should.equal @firstOpTimestamp
  78. done()
  79. describe "when the document is loaded", ->
  80. before (done) ->
  81. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  82. MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
  83. DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
  84. throw error if error?
  85. sinon.spy MockWebApi, "getDocument"
  86. DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
  87. throw error if error?
  88. setTimeout done, 200
  89. after ->
  90. MockWebApi.getDocument.restore()
  91. it "should not need to call the web api", ->
  92. MockWebApi.getDocument.called.should.equal false
  93. it "should update the doc", (done) ->
  94. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  95. doc.lines.should.deep.equal @result
  96. done()
  97. it "should push the applied updates to the track changes api", (done) ->
  98. rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
  99. JSON.parse(updates[0]).op.should.deep.equal @update.op
  100. rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
  101. result.should.equal 1
  102. done()
  103. it "should push the applied updates to the project history changes api", (done) ->
  104. rclient_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
  105. JSON.parse(updates[0]).op.should.deep.equal @update.op
  106. done()
  107. describe "when the document has been deleted", ->
  108. describe "when the ops come in a single linear order", ->
  109. before (done) ->
  110. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  111. lines = ["", "", ""]
  112. MockWebApi.insertDoc @project_id, @doc_id, {lines: lines, version: 0}
  113. @updates = [
  114. { doc_id: @doc_id, v: 0, op: [i: "h", p: 0 ] }
  115. { doc_id: @doc_id, v: 1, op: [i: "e", p: 1 ] }
  116. { doc_id: @doc_id, v: 2, op: [i: "l", p: 2 ] }
  117. { doc_id: @doc_id, v: 3, op: [i: "l", p: 3 ] }
  118. { doc_id: @doc_id, v: 4, op: [i: "o", p: 4 ] }
  119. { doc_id: @doc_id, v: 5, op: [i: " ", p: 5 ] }
  120. { doc_id: @doc_id, v: 6, op: [i: "w", p: 6 ] }
  121. { doc_id: @doc_id, v: 7, op: [i: "o", p: 7 ] }
  122. { doc_id: @doc_id, v: 8, op: [i: "r", p: 8 ] }
  123. { doc_id: @doc_id, v: 9, op: [i: "l", p: 9 ] }
  124. { doc_id: @doc_id, v: 10, op: [i: "d", p: 10] }
  125. ]
  126. @my_result = ["hello world", "", ""]
  127. done()
  128. it "should be able to continue applying updates when the project has been deleted", (done) ->
  129. actions = []
  130. for update in @updates.slice(0,6)
  131. do (update) =>
  132. actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
  133. actions.push (callback) => DocUpdaterClient.deleteDoc @project_id, @doc_id, callback
  134. for update in @updates.slice(6)
  135. do (update) =>
  136. actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
  137. async.series actions, (error) =>
  138. throw error if error?
  139. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  140. doc.lines.should.deep.equal @my_result
  141. done()
  142. it "should push the applied updates to the track changes api", (done) ->
  143. rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
  144. updates = (JSON.parse(u) for u in updates)
  145. for appliedUpdate, i in @updates
  146. appliedUpdate.op.should.deep.equal updates[i].op
  147. rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
  148. result.should.equal 1
  149. done()
  150. it "should store the doc ops in the correct order", (done) ->
  151. rclient_du.lrange Keys.docOps({doc_id: @doc_id}), 0, -1, (error, updates) =>
  152. updates = (JSON.parse(u) for u in updates)
  153. for appliedUpdate, i in @updates
  154. appliedUpdate.op.should.deep.equal updates[i].op
  155. done()
  156. describe "when older ops come in after the delete", ->
  157. before (done) ->
  158. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  159. lines = ["", "", ""]
  160. MockWebApi.insertDoc @project_id, @doc_id, {lines: lines, version: 0}
  161. @updates = [
  162. { doc_id: @doc_id, v: 0, op: [i: "h", p: 0 ] }
  163. { doc_id: @doc_id, v: 1, op: [i: "e", p: 1 ] }
  164. { doc_id: @doc_id, v: 2, op: [i: "l", p: 2 ] }
  165. { doc_id: @doc_id, v: 3, op: [i: "l", p: 3 ] }
  166. { doc_id: @doc_id, v: 4, op: [i: "o", p: 4 ] }
  167. { doc_id: @doc_id, v: 0, op: [i: "world", p: 1 ] }
  168. ]
  169. @my_result = ["hello", "world", ""]
  170. done()
  171. it "should be able to continue applying updates when the project has been deleted", (done) ->
  172. actions = []
  173. for update in @updates.slice(0,5)
  174. do (update) =>
  175. actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
  176. actions.push (callback) => DocUpdaterClient.deleteDoc @project_id, @doc_id, callback
  177. for update in @updates.slice(5)
  178. do (update) =>
  179. actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
  180. async.series actions, (error) =>
  181. throw error if error?
  182. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  183. doc.lines.should.deep.equal @my_result
  184. done()
  185. describe "with a broken update", ->
  186. before (done) ->
  187. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  188. @broken_update = { doc_id: @doc_id, v: @version, op: [d: "not the correct content", p: 0 ] }
  189. MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
  190. DocUpdaterClient.subscribeToAppliedOps @messageCallback = sinon.stub()
  191. DocUpdaterClient.sendUpdate @project_id, @doc_id, @broken_update, (error) ->
  192. throw error if error?
  193. setTimeout done, 200
  194. it "should not update the doc", (done) ->
  195. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  196. doc.lines.should.deep.equal @lines
  197. done()
  198. it "should send a message with an error", ->
  199. @messageCallback.called.should.equal true
  200. [channel, message] = @messageCallback.args[0]
  201. channel.should.equal "applied-ops"
  202. JSON.parse(message).should.deep.include {
  203. project_id: @project_id,
  204. doc_id: @doc_id,
  205. error:'Delete component does not match'
  206. }
  207. describe "with enough updates to flush to the track changes api", ->
  208. before (done) ->
  209. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  210. updates = []
  211. for v in [0..199] # Should flush after 100 ops
  212. updates.push
  213. doc_id: @doc_id,
  214. op: [i: v.toString(), p: 0]
  215. v: v
  216. sinon.spy MockTrackChangesApi, "flushDoc"
  217. MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: 0}
  218. # Send updates in chunks to causes multiple flushes
  219. actions = []
  220. for i in [0..19]
  221. do (i) =>
  222. actions.push (cb) =>
  223. DocUpdaterClient.sendUpdates @project_id, @doc_id, updates.slice(i*10, (i+1)*10), cb
  224. async.series actions, (error) =>
  225. throw error if error?
  226. setTimeout done, 2000
  227. after ->
  228. MockTrackChangesApi.flushDoc.restore()
  229. it "should flush the doc twice", ->
  230. MockTrackChangesApi.flushDoc.calledTwice.should.equal true
  231. describe "when there is no version in Mongo", ->
  232. before (done) ->
  233. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  234. MockWebApi.insertDoc @project_id, @doc_id, {
  235. lines: @lines
  236. }
  237. update =
  238. doc: @doc_id
  239. op: @update.op
  240. v: 0
  241. DocUpdaterClient.sendUpdate @project_id, @doc_id, update, (error) ->
  242. throw error if error?
  243. setTimeout done, 200
  244. it "should update the doc (using version = 0)", (done) ->
  245. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  246. doc.lines.should.deep.equal @result
  247. done()
  248. describe "when the sending duplicate ops", ->
  249. before (done) ->
  250. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  251. MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
  252. DocUpdaterClient.subscribeToAppliedOps @messageCallback = sinon.stub()
  253. # One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
  254. DocUpdaterClient.sendUpdate @project_id, @doc_id, {
  255. doc: @doc_id
  256. op: [{
  257. i: "one and a half\n"
  258. p: 4
  259. }]
  260. v: @version
  261. meta:
  262. source: "ikHceq3yfAdQYzBo4-xZ"
  263. }, (error) =>
  264. throw error if error?
  265. setTimeout () =>
  266. DocUpdaterClient.sendUpdate @project_id, @doc_id, {
  267. doc: @doc_id
  268. op: [{
  269. i: "one and a half\n"
  270. p: 4
  271. }]
  272. v: @version
  273. dupIfSource: ["ikHceq3yfAdQYzBo4-xZ"]
  274. meta:
  275. source: "ikHceq3yfAdQYzBo4-xZ"
  276. }, (error) =>
  277. throw error if error?
  278. setTimeout done, 200
  279. , 200
  280. it "should update the doc", (done) ->
  281. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  282. doc.lines.should.deep.equal @result
  283. done()
  284. it "should return a message about duplicate ops", ->
  285. @messageCallback.calledTwice.should.equal true
  286. @messageCallback.args[0][0].should.equal "applied-ops"
  287. expect(JSON.parse(@messageCallback.args[0][1]).op.dup).to.be.undefined
  288. @messageCallback.args[1][0].should.equal "applied-ops"
  289. expect(JSON.parse(@messageCallback.args[1][1]).op.dup).to.equal true