DocumentManagerTests.coffee 19 KB

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