ApplyingUpdatesToADocTests.coffee 10 KB

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