HttpControllerTests.coffee 17 KB

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