DocumentManagerTests.coffee 18 KB

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