ApplyingUpdatesToADocTests.coffee 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. sinon = require "sinon"
  2. chai = require("chai")
  3. chai.should()
  4. async = require "async"
  5. rclient = require("redis").createClient()
  6. {db, ObjectId} = require "../../../app/js/mongojs"
  7. MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
  8. MockWebApi = require "./helpers/MockWebApi"
  9. DocUpdaterClient = require "./helpers/DocUpdaterClient"
  10. describe "Applying updates to a doc", ->
  11. before ->
  12. @lines = ["one", "two", "three"]
  13. @version = 42
  14. @update =
  15. doc: @doc_id
  16. op: [{
  17. i: "one and a half\n"
  18. p: 4
  19. }]
  20. v: @version
  21. @result = ["one", "one and a half", "two", "three"]
  22. describe "when the document is not loaded", ->
  23. before (done) ->
  24. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  25. sinon.spy MockWebApi, "getDocument"
  26. MockWebApi.insertDoc @project_id, @doc_id, lines: @lines
  27. db.docOps.insert {
  28. doc_id: ObjectId(@doc_id)
  29. version: @version
  30. }, (error) =>
  31. throw error if error?
  32. DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
  33. throw error if error?
  34. setTimeout done, 200
  35. after ->
  36. MockWebApi.getDocument.restore()
  37. it "should load the document from the web API", ->
  38. MockWebApi.getDocument
  39. .calledWith(@project_id, @doc_id)
  40. .should.equal true
  41. it "should update the doc", (done) ->
  42. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  43. doc.lines.should.deep.equal @result
  44. done()
  45. it "should push the applied updates to the track changes api", (done) ->
  46. rclient.lrange "UncompressedHistoryOps:#{@doc_id}", 0, -1, (error, updates) =>
  47. throw error if error?
  48. JSON.parse(updates[0]).op.should.deep.equal @update.op
  49. rclient.sismember "DocsWithHistoryOps:#{@project_id}", @doc_id, (error, result) =>
  50. throw error if error?
  51. result.should.equal 1
  52. done()
  53. describe "when the document is loaded", ->
  54. before (done) ->
  55. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  56. MockWebApi.insertDoc @project_id, @doc_id, lines: @lines
  57. db.docOps.insert doc_id: ObjectId(@doc_id), version: @version, (error) =>
  58. throw error if error?
  59. DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
  60. throw error if error?
  61. sinon.spy MockWebApi, "getDocument"
  62. DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
  63. throw error if error?
  64. setTimeout done, 200
  65. after ->
  66. MockWebApi.getDocument.restore()
  67. it "should not need to call the web api", ->
  68. MockWebApi.getDocument.called.should.equal false
  69. it "should update the doc", (done) ->
  70. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  71. doc.lines.should.deep.equal @result
  72. done()
  73. it "should push the applied updates to the track changes api", (done) ->
  74. rclient.lrange "UncompressedHistoryOps:#{@doc_id}", 0, -1, (error, updates) =>
  75. JSON.parse(updates[0]).op.should.deep.equal @update.op
  76. rclient.sismember "DocsWithHistoryOps:#{@project_id}", @doc_id, (error, result) =>
  77. result.should.equal 1
  78. done()
  79. describe "when the document has been deleted", ->
  80. describe "when the ops come in a single linear order", ->
  81. before (done) ->
  82. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  83. lines = ["", "", ""]
  84. MockWebApi.insertDoc @project_id, @doc_id, lines: lines
  85. db.docOps.insert doc_id: ObjectId(@doc_id), version: 0, (error) =>
  86. throw error if error?
  87. @updates = [
  88. { doc_id: @doc_id, v: 0, op: [i: "h", p: 0 ] }
  89. { doc_id: @doc_id, v: 1, op: [i: "e", p: 1 ] }
  90. { doc_id: @doc_id, v: 2, op: [i: "l", p: 2 ] }
  91. { doc_id: @doc_id, v: 3, op: [i: "l", p: 3 ] }
  92. { doc_id: @doc_id, v: 4, op: [i: "o", p: 4 ] }
  93. { doc_id: @doc_id, v: 5, op: [i: " ", p: 5 ] }
  94. { doc_id: @doc_id, v: 6, op: [i: "w", p: 6 ] }
  95. { doc_id: @doc_id, v: 7, op: [i: "o", p: 7 ] }
  96. { doc_id: @doc_id, v: 8, op: [i: "r", p: 8 ] }
  97. { doc_id: @doc_id, v: 9, op: [i: "l", p: 9 ] }
  98. { doc_id: @doc_id, v: 10, op: [i: "d", p: 10] }
  99. ]
  100. @my_result = ["hello world", "", ""]
  101. done()
  102. it "should be able to continue applying updates when the project has been deleted", (done) ->
  103. actions = []
  104. for update in @updates.slice(0,6)
  105. do (update) =>
  106. actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
  107. actions.push (callback) => DocUpdaterClient.deleteDoc @project_id, @doc_id, callback
  108. for update in @updates.slice(6)
  109. do (update) =>
  110. actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
  111. async.series actions, (error) =>
  112. throw error if error?
  113. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  114. doc.lines.should.deep.equal @my_result
  115. done()
  116. it "should push the applied updates to the track changes api", (done) ->
  117. rclient.lrange "UncompressedHistoryOps:#{@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. rclient.sismember "DocsWithHistoryOps:#{@project_id}", @doc_id, (error, result) =>
  122. result.should.equal 1
  123. done()
  124. describe "when older ops come in after the delete", ->
  125. before (done) ->
  126. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  127. lines = ["", "", ""]
  128. MockWebApi.insertDoc @project_id, @doc_id, lines: lines
  129. db.docOps.insert doc_id: ObjectId(@doc_id), version: 0, (error) =>
  130. throw error if error?
  131. @updates = [
  132. { doc_id: @doc_id, v: 0, op: [i: "h", p: 0 ] }
  133. { doc_id: @doc_id, v: 1, op: [i: "e", p: 1 ] }
  134. { doc_id: @doc_id, v: 2, op: [i: "l", p: 2 ] }
  135. { doc_id: @doc_id, v: 3, op: [i: "l", p: 3 ] }
  136. { doc_id: @doc_id, v: 4, op: [i: "o", p: 4 ] }
  137. { doc_id: @doc_id, v: 0, op: [i: "world", p: 1 ] }
  138. ]
  139. @my_result = ["hello", "world", ""]
  140. done()
  141. it "should be able to continue applying updates when the project has been deleted", (done) ->
  142. actions = []
  143. for update in @updates.slice(0,5)
  144. do (update) =>
  145. actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
  146. actions.push (callback) => DocUpdaterClient.deleteDoc @project_id, @doc_id, callback
  147. for update in @updates.slice(5)
  148. do (update) =>
  149. actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
  150. async.series actions, (error) =>
  151. throw error if error?
  152. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  153. doc.lines.should.deep.equal @my_result
  154. done()
  155. describe "with a broken update", ->
  156. before (done) ->
  157. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  158. MockWebApi.insertDoc @project_id, @doc_id, lines: @lines
  159. db.docOps.insert doc_id: ObjectId(@doc_id), version: @version, (error) =>
  160. throw error if error?
  161. DocUpdaterClient.sendUpdate @project_id, @doc_id, @undefined, (error) ->
  162. throw error if error?
  163. setTimeout done, 200
  164. it "should not update the doc", (done) ->
  165. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  166. doc.lines.should.deep.equal @lines
  167. done()
  168. describe "with enough updates to flush to the track changes api", ->
  169. before (done) ->
  170. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  171. updates = []
  172. for v in [0..99] # Should flush after 50 ops
  173. updates.push
  174. doc_id: @doc_id,
  175. op: [i: v.toString(), p: 0]
  176. v: v
  177. sinon.spy MockTrackChangesApi, "flushDoc"
  178. MockWebApi.insertDoc @project_id, @doc_id, lines: @lines
  179. db.docOps.insert doc_id: ObjectId(@doc_id), version: 0, (error) =>
  180. throw error if error?
  181. DocUpdaterClient.sendUpdates @project_id, @doc_id, updates, (error) =>
  182. throw error if error?
  183. setTimeout done, 1000
  184. after ->
  185. MockTrackChangesApi.flushDoc.restore()
  186. it "should flush the doc twice", ->
  187. MockTrackChangesApi.flushDoc.calledTwice.should.equal true
  188. describe "when there is no version in Mongo", ->
  189. before (done) ->
  190. [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
  191. MockWebApi.insertDoc @project_id, @doc_id, {
  192. lines: @lines
  193. }
  194. update =
  195. doc: @doc_id
  196. op: @update.op
  197. v: 0
  198. DocUpdaterClient.sendUpdate @project_id, @doc_id, update, (error) ->
  199. throw error if error?
  200. setTimeout done, 200
  201. it "should update the doc (using version = 0)", (done) ->
  202. DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
  203. doc.lines.should.deep.equal @result
  204. done()