HttpControllerTests.coffee 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. sinon = require('sinon')
  2. chai = require('chai')
  3. should = chai.should()
  4. modulePath = "../../../../app/js/HttpController.js"
  5. SandboxedModule = require('sandboxed-module')
  6. Errors = require "../../../../app/js/Errors.js"
  7. describe "HttpController", ->
  8. beforeEach ->
  9. @HttpController = SandboxedModule.require modulePath, requires:
  10. "./DocumentManager": @DocumentManager = {}
  11. "./HistoryManager": @HistoryManager =
  12. flushProjectChangesAsync: sinon.stub()
  13. "./ProjectManager": @ProjectManager = {}
  14. "logger-sharelatex" : @logger = { log: sinon.stub() }
  15. "./ProjectFlusher": {flushAllProjects:->}
  16. "./DeleteQueueManager": @DeleteQueueManager = {}
  17. "./Metrics": @Metrics = {}
  18. "./Errors" : Errors
  19. @Metrics.Timer = class Timer
  20. done: sinon.stub()
  21. @project_id = "project-id-123"
  22. @doc_id = "doc-id-123"
  23. @next = sinon.stub()
  24. @res =
  25. send: sinon.stub()
  26. json: sinon.stub()
  27. describe "getDoc", ->
  28. beforeEach ->
  29. @lines = ["one", "two", "three"]
  30. @ops = ["mock-op-1", "mock-op-2"]
  31. @version = 42
  32. @fromVersion = 42
  33. @ranges = { changes: "mock", comments: "mock" }
  34. @pathname = '/a/b/c'
  35. @req =
  36. params:
  37. project_id: @project_id
  38. doc_id: @doc_id
  39. describe "when the document exists and no recent ops are requested", ->
  40. beforeEach ->
  41. @DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, null, @lines, @version, [], @ranges, @pathname)
  42. @HttpController.getDoc(@req, @res, @next)
  43. it "should get the doc", ->
  44. @DocumentManager.getDocAndRecentOpsWithLock
  45. .calledWith(@project_id, @doc_id, -1)
  46. .should.equal true
  47. it "should return the doc as JSON", ->
  48. @res.json
  49. .calledWith({
  50. id: @doc_id
  51. lines: @lines
  52. version: @version
  53. ops: []
  54. ranges: @ranges
  55. pathname: @pathname
  56. })
  57. .should.equal true
  58. it "should log the request", ->
  59. @logger.log
  60. .calledWith(doc_id: @doc_id, project_id: @project_id, "getting doc via http")
  61. .should.equal true
  62. it "should time the request", ->
  63. @Metrics.Timer::done.called.should.equal true
  64. describe "when recent ops are requested", ->
  65. beforeEach ->
  66. @DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, null, @lines, @version, @ops, @ranges, @pathname)
  67. @req.query = fromVersion: "#{@fromVersion}"
  68. @HttpController.getDoc(@req, @res, @next)
  69. it "should get the doc", ->
  70. @DocumentManager.getDocAndRecentOpsWithLock
  71. .calledWith(@project_id, @doc_id, @fromVersion)
  72. .should.equal true
  73. it "should return the doc as JSON", ->
  74. @res.json
  75. .calledWith({
  76. id: @doc_id
  77. lines: @lines
  78. version: @version
  79. ops: @ops
  80. ranges: @ranges
  81. pathname: @pathname
  82. })
  83. .should.equal true
  84. it "should log the request", ->
  85. @logger.log
  86. .calledWith(doc_id: @doc_id, project_id: @project_id, "getting doc via http")
  87. .should.equal true
  88. it "should time the request", ->
  89. @Metrics.Timer::done.called.should.equal true
  90. describe "when the document does not exist", ->
  91. beforeEach ->
  92. @DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, null, null, null)
  93. @HttpController.getDoc(@req, @res, @next)
  94. it "should call next with NotFoundError", ->
  95. @next
  96. .calledWith(new Errors.NotFoundError("not found"))
  97. .should.equal true
  98. describe "when an errors occurs", ->
  99. beforeEach ->
  100. @DocumentManager.getDocAndRecentOpsWithLock = sinon.stub().callsArgWith(3, new Error("oops"), null, null)
  101. @HttpController.getDoc(@req, @res, @next)
  102. it "should call next with the error", ->
  103. @next
  104. .calledWith(new Error("oops"))
  105. .should.equal true
  106. describe "setDoc", ->
  107. beforeEach ->
  108. @lines = ["one", "two", "three"]
  109. @source = "dropbox"
  110. @user_id = "user-id-123"
  111. @req =
  112. headers: {}
  113. params:
  114. project_id: @project_id
  115. doc_id: @doc_id
  116. body:
  117. lines: @lines
  118. source: @source
  119. user_id: @user_id
  120. undoing: @undoing = true
  121. describe "successfully", ->
  122. beforeEach ->
  123. @DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6)
  124. @HttpController.setDoc(@req, @res, @next)
  125. it "should set the doc", ->
  126. @DocumentManager.setDocWithLock
  127. .calledWith(@project_id, @doc_id, @lines, @source, @user_id, @undoing)
  128. .should.equal true
  129. it "should return a successful No Content response", ->
  130. @res.send
  131. .calledWith(204)
  132. .should.equal true
  133. it "should log the request", ->
  134. @logger.log
  135. .calledWith(doc_id: @doc_id, project_id: @project_id, lines: @lines, source: @source, user_id: @user_id, undoing: @undoing, "setting doc via http")
  136. .should.equal true
  137. it "should time the request", ->
  138. @Metrics.Timer::done.called.should.equal true
  139. describe "when an errors occurs", ->
  140. beforeEach ->
  141. @DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6, new Error("oops"))
  142. @HttpController.setDoc(@req, @res, @next)
  143. it "should call next with the error", ->
  144. @next
  145. .calledWith(new Error("oops"))
  146. .should.equal true
  147. describe "when the payload is too large", ->
  148. beforeEach ->
  149. lines = []
  150. for _ in [0..200000]
  151. lines.push "test test test"
  152. @req.body.lines = lines
  153. @DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6)
  154. @HttpController.setDoc(@req, @res, @next)
  155. it 'should send back a 406 response', ->
  156. @res.send.calledWith(406).should.equal true
  157. it 'should not call setDocWithLock', ->
  158. @DocumentManager.setDocWithLock.callCount.should.equal 0
  159. describe "flushProject", ->
  160. beforeEach ->
  161. @req =
  162. params:
  163. project_id: @project_id
  164. describe "successfully", ->
  165. beforeEach ->
  166. @ProjectManager.flushProjectWithLocks = sinon.stub().callsArgWith(1)
  167. @HttpController.flushProject(@req, @res, @next)
  168. it "should flush the project", ->
  169. @ProjectManager.flushProjectWithLocks
  170. .calledWith(@project_id)
  171. .should.equal true
  172. it "should return a successful No Content response", ->
  173. @res.send
  174. .calledWith(204)
  175. .should.equal true
  176. it "should log the request", ->
  177. @logger.log
  178. .calledWith(project_id: @project_id, "flushing project via http")
  179. .should.equal true
  180. it "should time the request", ->
  181. @Metrics.Timer::done.called.should.equal true
  182. describe "when an errors occurs", ->
  183. beforeEach ->
  184. @ProjectManager.flushProjectWithLocks = sinon.stub().callsArgWith(1, new Error("oops"))
  185. @HttpController.flushProject(@req, @res, @next)
  186. it "should call next with the error", ->
  187. @next
  188. .calledWith(new Error("oops"))
  189. .should.equal true
  190. describe "flushDocIfLoaded", ->
  191. beforeEach ->
  192. @lines = ["one", "two", "three"]
  193. @version = 42
  194. @req =
  195. params:
  196. project_id: @project_id
  197. doc_id: @doc_id
  198. describe "successfully", ->
  199. beforeEach ->
  200. @DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArgWith(2)
  201. @HttpController.flushDocIfLoaded(@req, @res, @next)
  202. it "should flush the doc", ->
  203. @DocumentManager.flushDocIfLoadedWithLock
  204. .calledWith(@project_id, @doc_id)
  205. .should.equal true
  206. it "should return a successful No Content response", ->
  207. @res.send
  208. .calledWith(204)
  209. .should.equal true
  210. it "should log the request", ->
  211. @logger.log
  212. .calledWith(doc_id: @doc_id, project_id: @project_id, "flushing doc via http")
  213. .should.equal true
  214. it "should time the request", ->
  215. @Metrics.Timer::done.called.should.equal true
  216. describe "when an errors occurs", ->
  217. beforeEach ->
  218. @DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArgWith(2, new Error("oops"))
  219. @HttpController.flushDocIfLoaded(@req, @res, @next)
  220. it "should call next with the error", ->
  221. @next
  222. .calledWith(new Error("oops"))
  223. .should.equal true
  224. describe "deleteDoc", ->
  225. beforeEach ->
  226. @req =
  227. params:
  228. project_id: @project_id
  229. doc_id: @doc_id
  230. query: {}
  231. describe "successfully", ->
  232. beforeEach ->
  233. @DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArgWith(3)
  234. @HttpController.deleteDoc(@req, @res, @next)
  235. it "should flush and delete the doc", ->
  236. @DocumentManager.flushAndDeleteDocWithLock
  237. .calledWith(@project_id, @doc_id, { ignoreFlushErrors: false })
  238. .should.equal true
  239. it "should flush project history", ->
  240. @HistoryManager.flushProjectChangesAsync
  241. .calledWithExactly(@project_id)
  242. .should.equal true
  243. it "should return a successful No Content response", ->
  244. @res.send
  245. .calledWith(204)
  246. .should.equal true
  247. it "should log the request", ->
  248. @logger.log
  249. .calledWith(doc_id: @doc_id, project_id: @project_id, "deleting doc via http")
  250. .should.equal true
  251. it "should time the request", ->
  252. @Metrics.Timer::done.called.should.equal true
  253. describe "ignoring errors", ->
  254. beforeEach ->
  255. @req.query.ignore_flush_errors = 'true'
  256. @DocumentManager.flushAndDeleteDocWithLock = sinon.stub().yields()
  257. @HttpController.deleteDoc(@req, @res, @next)
  258. it "should delete the doc", ->
  259. @DocumentManager.flushAndDeleteDocWithLock
  260. .calledWith(@project_id, @doc_id, { ignoreFlushErrors: true })
  261. .should.equal true
  262. it "should return a successful No Content response", ->
  263. @res.send.calledWith(204).should.equal true
  264. describe "when an errors occurs", ->
  265. beforeEach ->
  266. @DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArgWith(3, new Error("oops"))
  267. @HttpController.deleteDoc(@req, @res, @next)
  268. it "should flush project history", ->
  269. @HistoryManager.flushProjectChangesAsync
  270. .calledWithExactly(@project_id)
  271. .should.equal true
  272. it "should call next with the error", ->
  273. @next
  274. .calledWith(new Error("oops"))
  275. .should.equal true
  276. describe "deleteProject", ->
  277. beforeEach ->
  278. @req =
  279. params:
  280. project_id: @project_id
  281. describe "successfully", ->
  282. beforeEach ->
  283. @ProjectManager.flushAndDeleteProjectWithLocks = sinon.stub().callsArgWith(2)
  284. @HttpController.deleteProject(@req, @res, @next)
  285. it "should delete the project", ->
  286. @ProjectManager.flushAndDeleteProjectWithLocks
  287. .calledWith(@project_id)
  288. .should.equal true
  289. it "should return a successful No Content response", ->
  290. @res.send
  291. .calledWith(204)
  292. .should.equal true
  293. it "should log the request", ->
  294. @logger.log
  295. .calledWith(project_id: @project_id, "deleting project via http")
  296. .should.equal true
  297. it "should time the request", ->
  298. @Metrics.Timer::done.called.should.equal true
  299. describe "with the background=true option from realtime", ->
  300. beforeEach ->
  301. @ProjectManager.queueFlushAndDeleteProject = sinon.stub().callsArgWith(1)
  302. @req.query = {background:true, shutdown:true}
  303. @HttpController.deleteProject(@req, @res, @next)
  304. it "should queue the flush and delete", ->
  305. @ProjectManager.queueFlushAndDeleteProject
  306. .calledWith(@project_id)
  307. .should.equal true
  308. describe "when an errors occurs", ->
  309. beforeEach ->
  310. @ProjectManager.flushAndDeleteProjectWithLocks = sinon.stub().callsArgWith(2, new Error("oops"))
  311. @HttpController.deleteProject(@req, @res, @next)
  312. it "should call next with the error", ->
  313. @next
  314. .calledWith(new Error("oops"))
  315. .should.equal true
  316. describe "acceptChanges", ->
  317. beforeEach ->
  318. @req =
  319. params:
  320. project_id: @project_id
  321. doc_id: @doc_id
  322. change_id: @change_id = "mock-change-od-1"
  323. describe "successfully with a single change", ->
  324. beforeEach ->
  325. @DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3)
  326. @HttpController.acceptChanges(@req, @res, @next)
  327. it "should accept the change", ->
  328. @DocumentManager.acceptChangesWithLock
  329. .calledWith(@project_id, @doc_id, [ @change_id ])
  330. .should.equal true
  331. it "should return a successful No Content response", ->
  332. @res.send
  333. .calledWith(204)
  334. .should.equal true
  335. it "should log the request", ->
  336. @logger.log
  337. .calledWith({@project_id, @doc_id}, "accepting 1 changes via http")
  338. .should.equal true
  339. it "should time the request", ->
  340. @Metrics.Timer::done.called.should.equal true
  341. describe "succesfully with with multiple changes", ->
  342. beforeEach ->
  343. @change_ids = [ "mock-change-od-1", "mock-change-od-2", "mock-change-od-3", "mock-change-od-4" ]
  344. @req.body =
  345. change_ids: @change_ids
  346. @DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3)
  347. @HttpController.acceptChanges(@req, @res, @next)
  348. it "should accept the changes in the body payload", ->
  349. @DocumentManager.acceptChangesWithLock
  350. .calledWith(@project_id, @doc_id, @change_ids)
  351. .should.equal true
  352. it "should log the request with the correct number of changes", ->
  353. @logger.log
  354. .calledWith({@project_id, @doc_id}, "accepting #{ @change_ids.length } changes via http")
  355. .should.equal true
  356. describe "when an errors occurs", ->
  357. beforeEach ->
  358. @DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3, new Error("oops"))
  359. @HttpController.acceptChanges(@req, @res, @next)
  360. it "should call next with the error", ->
  361. @next
  362. .calledWith(new Error("oops"))
  363. .should.equal true
  364. describe "deleteComment", ->
  365. beforeEach ->
  366. @req =
  367. params:
  368. project_id: @project_id
  369. doc_id: @doc_id
  370. comment_id: @comment_id = "mock-comment-id"
  371. describe "successfully", ->
  372. beforeEach ->
  373. @DocumentManager.deleteCommentWithLock = sinon.stub().callsArgWith(3)
  374. @HttpController.deleteComment(@req, @res, @next)
  375. it "should accept the change", ->
  376. @DocumentManager.deleteCommentWithLock
  377. .calledWith(@project_id, @doc_id, @comment_id)
  378. .should.equal true
  379. it "should return a successful No Content response", ->
  380. @res.send
  381. .calledWith(204)
  382. .should.equal true
  383. it "should log the request", ->
  384. @logger.log
  385. .calledWith({@project_id, @doc_id, @comment_id}, "deleting comment via http")
  386. .should.equal true
  387. it "should time the request", ->
  388. @Metrics.Timer::done.called.should.equal true
  389. describe "when an errors occurs", ->
  390. beforeEach ->
  391. @DocumentManager.deleteCommentWithLock = sinon.stub().callsArgWith(3, new Error("oops"))
  392. @HttpController.deleteComment(@req, @res, @next)
  393. it "should call next with the error", ->
  394. @next
  395. .calledWith(new Error("oops"))
  396. .should.equal true
  397. describe "getProjectDocsAndFlushIfOld", ->
  398. beforeEach ->
  399. @state = "01234567890abcdef"
  400. @docs = [{_id: "1234", lines: "hello", v: 23}, {_id: "4567", lines: "world", v: 45}]
  401. @req =
  402. params:
  403. project_id: @project_id
  404. query:
  405. state: @state
  406. describe "successfully", ->
  407. beforeEach ->
  408. @ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3,null, @docs)
  409. @HttpController.getProjectDocsAndFlushIfOld(@req, @res, @next)
  410. it "should get docs from the project manager", ->
  411. @ProjectManager.getProjectDocsAndFlushIfOld
  412. .calledWith(@project_id, @state, {})
  413. .should.equal true
  414. it "should return a successful response", ->
  415. @res.send
  416. .calledWith(@docs)
  417. .should.equal true
  418. it "should log the request", ->
  419. @logger.log
  420. .calledWith({project_id: @project_id, exclude: []}, "getting docs via http")
  421. .should.equal true
  422. it "should log the response", ->
  423. @logger.log
  424. .calledWith({project_id: @project_id, result: ["1234:23", "4567:45"]}, "got docs via http")
  425. .should.equal true
  426. it "should time the request", ->
  427. @Metrics.Timer::done.called.should.equal true
  428. describe "when there is a conflict", ->
  429. beforeEach ->
  430. @ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3, new Errors.ProjectStateChangedError("project state changed"))
  431. @HttpController.getProjectDocsAndFlushIfOld(@req, @res, @next)
  432. it "should return an HTTP 409 Conflict response", ->
  433. @res.send
  434. .calledWith(409)
  435. .should.equal true
  436. describe "when an error occurs", ->
  437. beforeEach ->
  438. @ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3, new Error("oops"))
  439. @HttpController.getProjectDocsAndFlushIfOld(@req, @res, @next)
  440. it "should call next with the error", ->
  441. @next
  442. .calledWith(new Error("oops"))
  443. .should.equal true
  444. describe "updateProject", ->
  445. beforeEach ->
  446. @projectHistoryId = "history-id-123"
  447. @userId = "user-id-123"
  448. @docUpdates = sinon.stub()
  449. @fileUpdates = sinon.stub()
  450. @version = 1234567
  451. @req =
  452. body: {@projectHistoryId, @userId, @docUpdates, @fileUpdates, @version}
  453. params:
  454. project_id: @project_id
  455. describe "successfully", ->
  456. beforeEach ->
  457. @ProjectManager.updateProjectWithLocks = sinon.stub().callsArgWith(6)
  458. @HttpController.updateProject(@req, @res, @next)
  459. it "should accept the change", ->
  460. @ProjectManager.updateProjectWithLocks
  461. .calledWith(@project_id, @projectHistoryId, @userId, @docUpdates, @fileUpdates, @version)
  462. .should.equal true
  463. it "should return a successful No Content response", ->
  464. @res.send
  465. .calledWith(204)
  466. .should.equal true
  467. it "should time the request", ->
  468. @Metrics.Timer::done.called.should.equal true
  469. describe "when an errors occurs", ->
  470. beforeEach ->
  471. @ProjectManager.updateProjectWithLocks = sinon.stub().callsArgWith(6, new Error("oops"))
  472. @HttpController.updateProject(@req, @res, @next)
  473. it "should call next with the error", ->
  474. @next
  475. .calledWith(new Error("oops"))
  476. .should.equal true
  477. describe "resyncProjectHistory", ->
  478. beforeEach ->
  479. @projectHistoryId = "history-id-123"
  480. @docs = sinon.stub()
  481. @files = sinon.stub()
  482. @fileUpdates = sinon.stub()
  483. @req =
  484. body:
  485. {@projectHistoryId, @docs, @files}
  486. params:
  487. project_id: @project_id
  488. describe "successfully", ->
  489. beforeEach ->
  490. @HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4)
  491. @HttpController.resyncProjectHistory(@req, @res, @next)
  492. it "should accept the change", ->
  493. @HistoryManager.resyncProjectHistory
  494. .calledWith(@project_id, @projectHistoryId, @docs, @files)
  495. .should.equal true
  496. it "should return a successful No Content response", ->
  497. @res.send
  498. .calledWith(204)
  499. .should.equal true
  500. describe "when an errors occurs", ->
  501. beforeEach ->
  502. @HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4, new Error("oops"))
  503. @HttpController.resyncProjectHistory(@req, @res, @next)
  504. it "should call next with the error", ->
  505. @next
  506. .calledWith(new Error("oops"))
  507. .should.equal true