HttpControllerTests.coffee 18 KB

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