DocumentManagerTests.coffee 18 KB

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