DocumentManagerTests.coffee 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. sinon = require('sinon')
  2. chai = require('chai')
  3. should = chai.should()
  4. modulePath = "../../../../app/js/DocumentManager.js"
  5. SandboxedModule = require('sandboxed-module')
  6. Errors = require "../../../../app/js/Errors"
  7. tk = require "timekeeper"
  8. describe "DocumentManager", ->
  9. beforeEach ->
  10. @DocumentManager = SandboxedModule.require modulePath, requires:
  11. "./RedisManager": @RedisManager = {}
  12. "./PersistenceManager": @PersistenceManager = {}
  13. "./HistoryManager": @HistoryManager = {}
  14. "logger-sharelatex": @logger = {log: sinon.stub()}
  15. "./DocOpsManager": @DocOpsManager = {}
  16. "./Metrics": @Metrics =
  17. Timer: class Timer
  18. done: sinon.stub()
  19. "./RealTimeRedisManager": @RealTimeRedisManager = {}
  20. "./DiffCodec": @DiffCodec = {}
  21. "./UpdateManager": @UpdateManager = {}
  22. "./RangesManager": @RangesManager = {}
  23. @project_id = "project-id-123"
  24. @doc_id = "doc-id-123"
  25. @user_id = 1234
  26. @callback = sinon.stub()
  27. @lines = ["one", "two", "three"]
  28. @version = 42
  29. @ranges = { comments: "mock", entries: "mock" }
  30. @pathname = '/a/b/c.tex'
  31. @unflushedTime = Date.now()
  32. describe "flushAndDeleteDoc", ->
  33. describe "successfully", ->
  34. beforeEach ->
  35. @RedisManager.removeDocFromMemory = sinon.stub().callsArg(2)
  36. @DocumentManager.flushDocIfLoaded = sinon.stub().callsArgWith(2)
  37. @HistoryManager.flushChangesAsync = sinon.stub()
  38. @DocumentManager.flushAndDeleteDoc @project_id, @doc_id, @callback
  39. it "should flush the doc", ->
  40. @DocumentManager.flushDocIfLoaded
  41. .calledWith(@project_id, @doc_id)
  42. .should.equal true
  43. it "should remove the doc from redis", ->
  44. @RedisManager.removeDocFromMemory
  45. .calledWith(@project_id, @doc_id)
  46. .should.equal true
  47. it "should call the callback without error", ->
  48. @callback.calledWith(null).should.equal true
  49. it "should time the execution", ->
  50. @Metrics.Timer::done.called.should.equal true
  51. it "should flush to the history api", ->
  52. @HistoryManager.flushChangesAsync
  53. .calledWith(@project_id, @doc_id)
  54. .should.equal true
  55. describe "flushDocIfLoaded", ->
  56. describe "when the doc is in Redis", ->
  57. beforeEach ->
  58. @RedisManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges)
  59. @RedisManager.clearUnflushedTime = sinon.stub().callsArgWith(1, null)
  60. @PersistenceManager.setDoc = sinon.stub().yields()
  61. @DocumentManager.flushDocIfLoaded @project_id, @doc_id, @callback
  62. it "should get the doc from redis", ->
  63. @RedisManager.getDoc
  64. .calledWith(@project_id, @doc_id)
  65. .should.equal true
  66. it "should write the doc lines to the persistence layer", ->
  67. @PersistenceManager.setDoc
  68. .calledWith(@project_id, @doc_id, @lines, @version, @ranges)
  69. .should.equal true
  70. it "should call the callback without error", ->
  71. @callback.calledWith(null).should.equal true
  72. it "should time the execution", ->
  73. @Metrics.Timer::done.called.should.equal true
  74. describe "when the document is not in Redis", ->
  75. beforeEach ->
  76. @RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null, null)
  77. @PersistenceManager.setDoc = sinon.stub().yields()
  78. @DocOpsManager.flushDocOpsToMongo = sinon.stub().callsArgWith(2)
  79. @DocumentManager.flushDocIfLoaded @project_id, @doc_id, @callback
  80. it "should get the doc from redis", ->
  81. @RedisManager.getDoc
  82. .calledWith(@project_id, @doc_id)
  83. .should.equal true
  84. it "should not write anything to the persistence layer", ->
  85. @PersistenceManager.setDoc.called.should.equal false
  86. @DocOpsManager.flushDocOpsToMongo.called.should.equal false
  87. it "should call the callback without error", ->
  88. @callback.calledWith(null).should.equal true
  89. it "should time the execution", ->
  90. @Metrics.Timer::done.called.should.equal true
  91. describe "getDocAndRecentOps", ->
  92. describe "with a previous version specified", ->
  93. beforeEach ->
  94. @DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname)
  95. @RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
  96. @DocumentManager.getDocAndRecentOps @project_id, @doc_id, @fromVersion, @callback
  97. it "should get the doc", ->
  98. @DocumentManager.getDoc
  99. .calledWith(@project_id, @doc_id)
  100. .should.equal true
  101. it "should get the doc ops", ->
  102. @RedisManager.getPreviousDocOps
  103. .calledWith(@doc_id, @fromVersion, @version)
  104. .should.equal true
  105. it "should call the callback with the doc info", ->
  106. @callback.calledWith(null, @lines, @version, @ops, @ranges, @pathname).should.equal true
  107. it "should time the execution", ->
  108. @Metrics.Timer::done.called.should.equal true
  109. describe "with no previous version specified", ->
  110. beforeEach ->
  111. @DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname)
  112. @RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
  113. @DocumentManager.getDocAndRecentOps @project_id, @doc_id, -1, @callback
  114. it "should get the doc", ->
  115. @DocumentManager.getDoc
  116. .calledWith(@project_id, @doc_id)
  117. .should.equal true
  118. it "should not need to get the doc ops", ->
  119. @RedisManager.getPreviousDocOps.called.should.equal false
  120. it "should call the callback with the doc info", ->
  121. @callback.calledWith(null, @lines, @version, [], @ranges, @pathname).should.equal true
  122. it "should time the execution", ->
  123. @Metrics.Timer::done.called.should.equal true
  124. describe "getDoc", ->
  125. describe "when the doc exists in Redis", ->
  126. beforeEach ->
  127. @RedisManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, @unflushedTime)
  128. @DocumentManager.getDoc @project_id, @doc_id, @callback
  129. it "should get the doc from Redis", ->
  130. @RedisManager.getDoc
  131. .calledWith(@project_id, @doc_id)
  132. .should.equal true
  133. it "should call the callback with the doc info", ->
  134. @callback.calledWith(null, @lines, @version, @ranges, @pathname, @unflushedTime, true).should.equal true
  135. it "should time the execution", ->
  136. @Metrics.Timer::done.called.should.equal true
  137. describe "when the doc does not exist in Redis", ->
  138. beforeEach ->
  139. @RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null, null, null, null)
  140. @PersistenceManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname)
  141. @RedisManager.putDocInMemory = sinon.stub().yields()
  142. @DocumentManager.getDoc @project_id, @doc_id, @callback
  143. it "should try to get the doc from Redis", ->
  144. @RedisManager.getDoc
  145. .calledWith(@project_id, @doc_id)
  146. .should.equal true
  147. it "should get the doc from the PersistenceManager", ->
  148. @PersistenceManager.getDoc
  149. .calledWith(@project_id, @doc_id)
  150. .should.equal true
  151. it "should set the doc in Redis", ->
  152. @RedisManager.putDocInMemory
  153. .calledWith(@project_id, @doc_id, @lines, @version, @ranges, @pathname)
  154. .should.equal true
  155. it "should call the callback with the doc info", ->
  156. @callback.calledWith(null, @lines, @version, @ranges, @pathname, null, false).should.equal true
  157. it "should time the execution", ->
  158. @Metrics.Timer::done.called.should.equal true
  159. describe "setDoc", ->
  160. describe "with plain tex lines", ->
  161. beforeEach ->
  162. @beforeLines = ["before", "lines"]
  163. @afterLines = ["after", "lines"]
  164. @ops = [{ i: "foo", p: 4 }, { d: "bar", p: 42 }]
  165. @DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @beforeLines, @version, @ranges, @pathname, @unflushedTime, true)
  166. @DiffCodec.diffAsShareJsOp = sinon.stub().callsArgWith(2, null, @ops)
  167. @UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null)
  168. @DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2)
  169. @DocumentManager.flushAndDeleteDoc = sinon.stub().callsArg(2)
  170. describe "when already loaded", ->
  171. beforeEach ->
  172. @DocumentManager.setDoc @project_id, @doc_id, @afterLines, @source, @user_id, false, @callback
  173. it "should get the current doc lines", ->
  174. @DocumentManager.getDoc
  175. .calledWith(@project_id, @doc_id)
  176. .should.equal true
  177. it "should return a diff of the old and new lines", ->
  178. @DiffCodec.diffAsShareJsOp
  179. .calledWith(@beforeLines, @afterLines)
  180. .should.equal true
  181. it "should apply the diff as a ShareJS op", ->
  182. @UpdateManager.applyUpdate
  183. .calledWith(
  184. @project_id,
  185. @doc_id,
  186. {
  187. doc: @doc_id,
  188. v: @version,
  189. op: @ops,
  190. meta: {
  191. type: "external"
  192. source: @source
  193. user_id: @user_id
  194. }
  195. }
  196. )
  197. .should.equal true
  198. it "should flush the doc to Mongo", ->
  199. @DocumentManager.flushDocIfLoaded
  200. .calledWith(@project_id, @doc_id)
  201. .should.equal true
  202. it "should call the callback", ->
  203. @callback.calledWith(null).should.equal true
  204. it "should time the execution", ->
  205. @Metrics.Timer::done.called.should.equal true
  206. describe "when not already loaded", ->
  207. beforeEach ->
  208. @DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @beforeLines, @version, @pathname, null, false)
  209. @DocumentManager.setDoc @project_id, @doc_id, @afterLines, @source, @user_id, false, @callback
  210. it "should flush and delete the doc from the doc updater", ->
  211. @DocumentManager.flushAndDeleteDoc
  212. .calledWith(@project_id, @doc_id)
  213. .should.equal true
  214. describe "without new lines", ->
  215. beforeEach ->
  216. @DocumentManager.setDoc @project_id, @doc_id, null, @source, @user_id, false, @callback
  217. it "should return the callback with an error", ->
  218. @callback.calledWith(new Error("No lines were passed to setDoc"))
  219. it "should not try to get the doc lines", ->
  220. @DocumentManager.getDoc.called.should.equal false
  221. describe "with the undoing flag", ->
  222. beforeEach ->
  223. # Copy ops so we don't interfere with other tests
  224. @ops = [{ i: "foo", p: 4 }, { d: "bar", p: 42 }]
  225. @DiffCodec.diffAsShareJsOp = sinon.stub().callsArgWith(2, null, @ops)
  226. @DocumentManager.setDoc @project_id, @doc_id, @afterLines, @source, @user_id, true, @callback
  227. it "should set the undo flag on each op", ->
  228. for op in @ops
  229. op.u.should.equal true
  230. describe "acceptChanges", ->
  231. beforeEach ->
  232. @change_id = "mock-change-id"
  233. @change_ids = [ "mock-change-id-1", "mock-change-id-2", "mock-change-id-3", "mock-change-id-4" ]
  234. @version = 34
  235. @lines = ["original", "lines"]
  236. @ranges = { entries: "mock", comments: "mock" }
  237. @updated_ranges = { entries: "updated", comments: "updated" }
  238. @DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges)
  239. @RangesManager.acceptChanges = sinon.stub().yields(null, @updated_ranges)
  240. @RedisManager.updateDocument = sinon.stub().yields()
  241. describe "successfully with a single change", ->
  242. beforeEach ->
  243. @DocumentManager.acceptChanges @project_id, @doc_id, [ @change_id ], @callback
  244. it "should get the document's current ranges", ->
  245. @DocumentManager.getDoc
  246. .calledWith(@project_id, @doc_id)
  247. .should.equal true
  248. it "should apply the accept change to the ranges", ->
  249. @RangesManager.acceptChanges
  250. .calledWith([ @change_id ], @ranges)
  251. .should.equal true
  252. it "should save the updated ranges", ->
  253. @RedisManager.updateDocument
  254. .calledWith(@project_id, @doc_id, @lines, @version, [], @updated_ranges)
  255. .should.equal true
  256. it "should call the callback", ->
  257. @callback.called.should.equal true
  258. describe "successfully with multiple changes", ->
  259. beforeEach ->
  260. @DocumentManager.acceptChanges @project_id, @doc_id, @change_ids, @callback
  261. it "should apply the accept change to the ranges", ->
  262. @RangesManager.acceptChanges
  263. .calledWith(@change_ids, @ranges)
  264. .should.equal true
  265. describe "when the doc is not found", ->
  266. beforeEach ->
  267. @DocumentManager.getDoc = sinon.stub().yields(null, null, null, null)
  268. @DocumentManager.acceptChanges @project_id, @doc_id, [ @change_id ], @callback
  269. it "should not save anything", ->
  270. @RedisManager.updateDocument.called.should.equal false
  271. it "should call the callback with a not found error", ->
  272. error = new Errors.NotFoundError("document not found: #{@doc_id}")
  273. @callback.calledWith(error).should.equal true
  274. describe "deleteComment", ->
  275. beforeEach ->
  276. @comment_id = "mock-comment-id"
  277. @version = 34
  278. @lines = ["original", "lines"]
  279. @ranges = { comments: ["one", "two", "three"] }
  280. @updated_ranges = { comments: ["one", "three"] }
  281. @DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges)
  282. @RangesManager.deleteComment = sinon.stub().yields(null, @updated_ranges)
  283. @RedisManager.updateDocument = sinon.stub().yields()
  284. describe "successfully", ->
  285. beforeEach ->
  286. @DocumentManager.deleteComment @project_id, @doc_id, @comment_id, @callback
  287. it "should get the document's current ranges", ->
  288. @DocumentManager.getDoc
  289. .calledWith(@project_id, @doc_id)
  290. .should.equal true
  291. it "should delete the comment from the ranges", ->
  292. @RangesManager.deleteComment
  293. .calledWith(@comment_id, @ranges)
  294. .should.equal true
  295. it "should save the updated ranges", ->
  296. @RedisManager.updateDocument
  297. .calledWith(@project_id, @doc_id, @lines, @version, [], @updated_ranges)
  298. .should.equal true
  299. it "should call the callback", ->
  300. @callback.called.should.equal true
  301. describe "when the doc is not found", ->
  302. beforeEach ->
  303. @DocumentManager.getDoc = sinon.stub().yields(null, null, null, null)
  304. @DocumentManager.acceptChanges @project_id, @doc_id, [ @comment_id ], @callback
  305. it "should not save anything", ->
  306. @RedisManager.updateDocument.called.should.equal false
  307. it "should call the callback with a not found error", ->
  308. error = new Errors.NotFoundError("document not found: #{@doc_id}")
  309. @callback.calledWith(error).should.equal true
  310. describe "getDocAndFlushIfOld", ->
  311. beforeEach ->
  312. tk.freeze(new Date())
  313. @DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2)
  314. afterEach ->
  315. tk.reset()
  316. describe "when the doc is in Redis", ->
  317. describe "and has changes to be flushed", ->
  318. beforeEach ->
  319. @DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, Date.now() - 1e9, true)
  320. @DocumentManager.getDocAndFlushIfOld @project_id, @doc_id, @callback
  321. it "should get the doc", ->
  322. @DocumentManager.getDoc
  323. .calledWith(@project_id, @doc_id)
  324. .should.equal true
  325. it "should flush the doc", ->
  326. @DocumentManager.flushDocIfLoaded
  327. .calledWith(@project_id, @doc_id)
  328. .should.equal true
  329. it "should call the callback with the lines and versions", ->
  330. @callback.calledWith(null, @lines, @version).should.equal true
  331. describe "and has only changes that don't need to be flushed", ->
  332. beforeEach ->
  333. @DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, Date.now() - 100, true)
  334. @DocumentManager.getDocAndFlushIfOld @project_id, @doc_id, @callback
  335. it "should get the doc", ->
  336. @DocumentManager.getDoc
  337. .calledWith(@project_id, @doc_id)
  338. .should.equal true
  339. it "should not flush the doc", ->
  340. @DocumentManager.flushDocIfLoaded
  341. .called.should.equal false
  342. it "should call the callback with the lines and versions", ->
  343. @callback.calledWith(null, @lines, @version).should.equal true
  344. describe "when the doc is not in Redis", ->
  345. beforeEach ->
  346. @DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, null, false)
  347. @DocumentManager.getDocAndFlushIfOld @project_id, @doc_id, @callback
  348. it "should get the doc", ->
  349. @DocumentManager.getDoc
  350. .calledWith(@project_id, @doc_id)
  351. .should.equal true
  352. it "should not flush the doc", ->
  353. @DocumentManager.flushDocIfLoaded
  354. .called.should.equal false
  355. it "should call the callback with the lines and versions", ->
  356. @callback.calledWith(null, @lines, @version).should.equal true
  357. describe "renameDoc", ->
  358. beforeEach ->
  359. @update = 'some-update'
  360. @RedisManager.renameDoc = sinon.stub().yields()
  361. describe "successfully", ->
  362. beforeEach ->
  363. @DocumentManager.renameDoc @project_id, @doc_id, @user_id, @update, @callback
  364. it "should rename the document", ->
  365. @RedisManager.renameDoc
  366. .calledWith(@project_id, @doc_id, @user_id, @update)
  367. .should.equal true
  368. it "should call the callback", ->
  369. @callback.called.should.equal true