HttpControllerTests.coffee 18 KB

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