ApplyingUpdatesToADocTests.coffee 12 KB

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