HttpControllerTests.coffee 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  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. body: {}
  231. describe "successfully", ->
  232. beforeEach ->
  233. @DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArgWith(2)
  234. @HttpController.deleteDoc(@req, @res, @next)
  235. it "should flush and delete the doc", ->
  236. @DocumentManager.flushAndDeleteDocWithLock
  237. .calledWith(@project_id, @doc_id)
  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, flush: true, "deleting doc via http")
  250. .should.equal true
  251. it "should time the request", ->
  252. @Metrics.Timer::done.called.should.equal true
  253. describe "without flush", ->
  254. beforeEach ->
  255. @req.body.flush = false
  256. @DocumentManager.deleteDocWithLock = sinon.stub().yields()
  257. @HttpController.deleteDoc(@req, @res, @next)
  258. it "should delete the doc", ->
  259. @DocumentManager.deleteDocWithLock.calledWith(@project_id, @doc_id).should.equal true
  260. it "should return a successful No Content response", ->
  261. @res.send.calledWith(204).should.equal true
  262. describe "when an errors occurs", ->
  263. beforeEach ->
  264. @DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArgWith(2, new Error("oops"))
  265. @HttpController.deleteDoc(@req, @res, @next)
  266. it "should flush project history", ->
  267. @HistoryManager.flushProjectChangesAsync
  268. .calledWithExactly(@project_id)
  269. .should.equal true
  270. it "should call next with the error", ->
  271. @next
  272. .calledWith(new Error("oops"))
  273. .should.equal true
  274. describe "deleteProject", ->
  275. beforeEach ->
  276. @req =
  277. params:
  278. project_id: @project_id
  279. describe "successfully", ->
  280. beforeEach ->
  281. @ProjectManager.flushAndDeleteProjectWithLocks = sinon.stub().callsArgWith(2)
  282. @HttpController.deleteProject(@req, @res, @next)
  283. it "should delete the project", ->
  284. @ProjectManager.flushAndDeleteProjectWithLocks
  285. .calledWith(@project_id)
  286. .should.equal true
  287. it "should return a successful No Content response", ->
  288. @res.send
  289. .calledWith(204)
  290. .should.equal true
  291. it "should log the request", ->
  292. @logger.log
  293. .calledWith(project_id: @project_id, "deleting project via http")
  294. .should.equal true
  295. it "should time the request", ->
  296. @Metrics.Timer::done.called.should.equal true
  297. describe "with the background=true option from realtime", ->
  298. beforeEach ->
  299. @ProjectManager.queueFlushAndDeleteProject = sinon.stub().callsArgWith(1)
  300. @req.query = {background:true, shutdown:true}
  301. @HttpController.deleteProject(@req, @res, @next)
  302. it "should queue the flush and delete", ->
  303. @ProjectManager.queueFlushAndDeleteProject
  304. .calledWith(@project_id)
  305. .should.equal true
  306. describe "when an errors occurs", ->
  307. beforeEach ->
  308. @ProjectManager.flushAndDeleteProjectWithLocks = sinon.stub().callsArgWith(2, new Error("oops"))
  309. @HttpController.deleteProject(@req, @res, @next)
  310. it "should call next with the error", ->
  311. @next
  312. .calledWith(new Error("oops"))
  313. .should.equal true
  314. describe "acceptChanges", ->
  315. beforeEach ->
  316. @req =
  317. params:
  318. project_id: @project_id
  319. doc_id: @doc_id
  320. change_id: @change_id = "mock-change-od-1"
  321. describe "successfully with a single change", ->
  322. beforeEach ->
  323. @DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3)
  324. @HttpController.acceptChanges(@req, @res, @next)
  325. it "should accept the change", ->
  326. @DocumentManager.acceptChangesWithLock
  327. .calledWith(@project_id, @doc_id, [ @change_id ])
  328. .should.equal true
  329. it "should return a successful No Content response", ->
  330. @res.send
  331. .calledWith(204)
  332. .should.equal true
  333. it "should log the request", ->
  334. @logger.log
  335. .calledWith({@project_id, @doc_id}, "accepting 1 changes via http")
  336. .should.equal true
  337. it "should time the request", ->
  338. @Metrics.Timer::done.called.should.equal true
  339. describe "succesfully with with multiple changes", ->
  340. beforeEach ->
  341. @change_ids = [ "mock-change-od-1", "mock-change-od-2", "mock-change-od-3", "mock-change-od-4" ]
  342. @req.body =
  343. change_ids: @change_ids
  344. @DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3)
  345. @HttpController.acceptChanges(@req, @res, @next)
  346. it "should accept the changes in the body payload", ->
  347. @DocumentManager.acceptChangesWithLock
  348. .calledWith(@project_id, @doc_id, @change_ids)
  349. .should.equal true
  350. it "should log the request with the correct number of changes", ->
  351. @logger.log
  352. .calledWith({@project_id, @doc_id}, "accepting #{ @change_ids.length } changes via http")
  353. .should.equal true
  354. describe "when an errors occurs", ->
  355. beforeEach ->
  356. @DocumentManager.acceptChangesWithLock = sinon.stub().callsArgWith(3, new Error("oops"))
  357. @HttpController.acceptChanges(@req, @res, @next)
  358. it "should call next with the error", ->
  359. @next
  360. .calledWith(new Error("oops"))
  361. .should.equal true
  362. describe "deleteComment", ->
  363. beforeEach ->
  364. @req =
  365. params:
  366. project_id: @project_id
  367. doc_id: @doc_id
  368. comment_id: @comment_id = "mock-comment-id"
  369. describe "successfully", ->
  370. beforeEach ->
  371. @DocumentManager.deleteCommentWithLock = sinon.stub().callsArgWith(3)
  372. @HttpController.deleteComment(@req, @res, @next)
  373. it "should accept the change", ->
  374. @DocumentManager.deleteCommentWithLock
  375. .calledWith(@project_id, @doc_id, @comment_id)
  376. .should.equal true
  377. it "should return a successful No Content response", ->
  378. @res.send
  379. .calledWith(204)
  380. .should.equal true
  381. it "should log the request", ->
  382. @logger.log
  383. .calledWith({@project_id, @doc_id, @comment_id}, "deleting comment via http")
  384. .should.equal true
  385. it "should time the request", ->
  386. @Metrics.Timer::done.called.should.equal true
  387. describe "when an errors occurs", ->
  388. beforeEach ->
  389. @DocumentManager.deleteCommentWithLock = sinon.stub().callsArgWith(3, new Error("oops"))
  390. @HttpController.deleteComment(@req, @res, @next)
  391. it "should call next with the error", ->
  392. @next
  393. .calledWith(new Error("oops"))
  394. .should.equal true
  395. describe "getProjectDocsAndFlushIfOld", ->
  396. beforeEach ->
  397. @state = "01234567890abcdef"
  398. @docs = [{_id: "1234", lines: "hello", v: 23}, {_id: "4567", lines: "world", v: 45}]
  399. @req =
  400. params:
  401. project_id: @project_id
  402. query:
  403. state: @state
  404. describe "successfully", ->
  405. beforeEach ->
  406. @ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3,null, @docs)
  407. @HttpController.getProjectDocsAndFlushIfOld(@req, @res, @next)
  408. it "should get docs from the project manager", ->
  409. @ProjectManager.getProjectDocsAndFlushIfOld
  410. .calledWith(@project_id, @state, {})
  411. .should.equal true
  412. it "should return a successful response", ->
  413. @res.send
  414. .calledWith(@docs)
  415. .should.equal true
  416. it "should log the request", ->
  417. @logger.log
  418. .calledWith({project_id: @project_id, exclude: []}, "getting docs via http")
  419. .should.equal true
  420. it "should log the response", ->
  421. @logger.log
  422. .calledWith({project_id: @project_id, result: ["1234:23", "4567:45"]}, "got docs via http")
  423. .should.equal true
  424. it "should time the request", ->
  425. @Metrics.Timer::done.called.should.equal true
  426. describe "when there is a conflict", ->
  427. beforeEach ->
  428. @ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3, new Errors.ProjectStateChangedError("project state changed"))
  429. @HttpController.getProjectDocsAndFlushIfOld(@req, @res, @next)
  430. it "should return an HTTP 409 Conflict response", ->
  431. @res.send
  432. .calledWith(409)
  433. .should.equal true
  434. describe "when an error occurs", ->
  435. beforeEach ->
  436. @ProjectManager.getProjectDocsAndFlushIfOld = sinon.stub().callsArgWith(3, new Error("oops"))
  437. @HttpController.getProjectDocsAndFlushIfOld(@req, @res, @next)
  438. it "should call next with the error", ->
  439. @next
  440. .calledWith(new Error("oops"))
  441. .should.equal true
  442. describe "updateProject", ->
  443. beforeEach ->
  444. @projectHistoryId = "history-id-123"
  445. @userId = "user-id-123"
  446. @docUpdates = sinon.stub()
  447. @fileUpdates = sinon.stub()
  448. @version = 1234567
  449. @req =
  450. body: {@projectHistoryId, @userId, @docUpdates, @fileUpdates, @version}
  451. params:
  452. project_id: @project_id
  453. describe "successfully", ->
  454. beforeEach ->
  455. @ProjectManager.updateProjectWithLocks = sinon.stub().callsArgWith(6)
  456. @HttpController.updateProject(@req, @res, @next)
  457. it "should accept the change", ->
  458. @ProjectManager.updateProjectWithLocks
  459. .calledWith(@project_id, @projectHistoryId, @userId, @docUpdates, @fileUpdates, @version)
  460. .should.equal true
  461. it "should return a successful No Content response", ->
  462. @res.send
  463. .calledWith(204)
  464. .should.equal true
  465. it "should time the request", ->
  466. @Metrics.Timer::done.called.should.equal true
  467. describe "when an errors occurs", ->
  468. beforeEach ->
  469. @ProjectManager.updateProjectWithLocks = sinon.stub().callsArgWith(6, new Error("oops"))
  470. @HttpController.updateProject(@req, @res, @next)
  471. it "should call next with the error", ->
  472. @next
  473. .calledWith(new Error("oops"))
  474. .should.equal true
  475. describe "resyncProjectHistory", ->
  476. beforeEach ->
  477. @projectHistoryId = "history-id-123"
  478. @docs = sinon.stub()
  479. @files = sinon.stub()
  480. @fileUpdates = sinon.stub()
  481. @req =
  482. body:
  483. {@projectHistoryId, @docs, @files}
  484. params:
  485. project_id: @project_id
  486. describe "successfully", ->
  487. beforeEach ->
  488. @HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4)
  489. @HttpController.resyncProjectHistory(@req, @res, @next)
  490. it "should accept the change", ->
  491. @HistoryManager.resyncProjectHistory
  492. .calledWith(@project_id, @projectHistoryId, @docs, @files)
  493. .should.equal true
  494. it "should return a successful No Content response", ->
  495. @res.send
  496. .calledWith(204)
  497. .should.equal true
  498. describe "when an errors occurs", ->
  499. beforeEach ->
  500. @HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4, new Error("oops"))
  501. @HttpController.resyncProjectHistory(@req, @res, @next)
  502. it "should call next with the error", ->
  503. @next
  504. .calledWith(new Error("oops"))
  505. .should.equal true