HttpControllerTests.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915
  1. const sinon = require('sinon')
  2. const modulePath = '../../../../app/js/HttpController.js'
  3. const SandboxedModule = require('sandboxed-module')
  4. const Errors = require('../../../../app/js/Errors.js')
  5. describe('HttpController', function () {
  6. beforeEach(function () {
  7. this.HttpController = SandboxedModule.require(modulePath, {
  8. requires: {
  9. './DocumentManager': (this.DocumentManager = {}),
  10. './HistoryManager': (this.HistoryManager = {
  11. flushProjectChangesAsync: sinon.stub()
  12. }),
  13. './ProjectManager': (this.ProjectManager = {}),
  14. 'logger-sharelatex': (this.logger = { log: sinon.stub() }),
  15. './ProjectFlusher': { flushAllProjects() {} },
  16. './DeleteQueueManager': (this.DeleteQueueManager = {}),
  17. './Metrics': (this.Metrics = {}),
  18. './Errors': Errors
  19. }
  20. })
  21. this.Metrics.Timer = class Timer {}
  22. this.Metrics.Timer.prototype.done = sinon.stub()
  23. this.project_id = 'project-id-123'
  24. this.doc_id = 'doc-id-123'
  25. this.next = sinon.stub()
  26. this.res = {
  27. send: sinon.stub(),
  28. sendStatus: sinon.stub(),
  29. json: sinon.stub()
  30. }
  31. })
  32. describe('getDoc', function () {
  33. beforeEach(function () {
  34. this.lines = ['one', 'two', 'three']
  35. this.ops = ['mock-op-1', 'mock-op-2']
  36. this.version = 42
  37. this.fromVersion = 42
  38. this.ranges = { changes: 'mock', comments: 'mock' }
  39. this.pathname = '/a/b/c'
  40. this.req = {
  41. params: {
  42. project_id: this.project_id,
  43. doc_id: this.doc_id
  44. }
  45. }
  46. })
  47. describe('when the document exists and no recent ops are requested', function () {
  48. beforeEach(function () {
  49. this.DocumentManager.getDocAndRecentOpsWithLock = sinon
  50. .stub()
  51. .callsArgWith(
  52. 3,
  53. null,
  54. this.lines,
  55. this.version,
  56. [],
  57. this.ranges,
  58. this.pathname
  59. )
  60. this.HttpController.getDoc(this.req, this.res, this.next)
  61. })
  62. it('should get the doc', function () {
  63. this.DocumentManager.getDocAndRecentOpsWithLock
  64. .calledWith(this.project_id, this.doc_id, -1)
  65. .should.equal(true)
  66. })
  67. it('should return the doc as JSON', function () {
  68. this.res.json
  69. .calledWith({
  70. id: this.doc_id,
  71. lines: this.lines,
  72. version: this.version,
  73. ops: [],
  74. ranges: this.ranges,
  75. pathname: this.pathname
  76. })
  77. .should.equal(true)
  78. })
  79. it('should log the request', function () {
  80. this.logger.log
  81. .calledWith(
  82. { doc_id: this.doc_id, project_id: this.project_id },
  83. 'getting doc via http'
  84. )
  85. .should.equal(true)
  86. })
  87. it('should time the request', function () {
  88. this.Metrics.Timer.prototype.done.called.should.equal(true)
  89. })
  90. })
  91. describe('when recent ops are requested', function () {
  92. beforeEach(function () {
  93. this.DocumentManager.getDocAndRecentOpsWithLock = sinon
  94. .stub()
  95. .callsArgWith(
  96. 3,
  97. null,
  98. this.lines,
  99. this.version,
  100. this.ops,
  101. this.ranges,
  102. this.pathname
  103. )
  104. this.req.query = { fromVersion: `${this.fromVersion}` }
  105. this.HttpController.getDoc(this.req, this.res, this.next)
  106. })
  107. it('should get the doc', function () {
  108. this.DocumentManager.getDocAndRecentOpsWithLock
  109. .calledWith(this.project_id, this.doc_id, this.fromVersion)
  110. .should.equal(true)
  111. })
  112. it('should return the doc as JSON', function () {
  113. this.res.json
  114. .calledWith({
  115. id: this.doc_id,
  116. lines: this.lines,
  117. version: this.version,
  118. ops: this.ops,
  119. ranges: this.ranges,
  120. pathname: this.pathname
  121. })
  122. .should.equal(true)
  123. })
  124. it('should log the request', function () {
  125. this.logger.log
  126. .calledWith(
  127. { doc_id: this.doc_id, project_id: this.project_id },
  128. 'getting doc via http'
  129. )
  130. .should.equal(true)
  131. })
  132. it('should time the request', function () {
  133. this.Metrics.Timer.prototype.done.called.should.equal(true)
  134. })
  135. })
  136. describe('when the document does not exist', function () {
  137. beforeEach(function () {
  138. this.DocumentManager.getDocAndRecentOpsWithLock = sinon
  139. .stub()
  140. .callsArgWith(3, null, null, null)
  141. this.HttpController.getDoc(this.req, this.res, this.next)
  142. })
  143. it('should call next with NotFoundError', function () {
  144. this.next
  145. .calledWith(new Errors.NotFoundError('not found'))
  146. .should.equal(true)
  147. })
  148. })
  149. describe('when an errors occurs', function () {
  150. beforeEach(function () {
  151. this.DocumentManager.getDocAndRecentOpsWithLock = sinon
  152. .stub()
  153. .callsArgWith(3, new Error('oops'), null, null)
  154. this.HttpController.getDoc(this.req, this.res, this.next)
  155. })
  156. it('should call next with the error', function () {
  157. this.next.calledWith(new Error('oops')).should.equal(true)
  158. })
  159. })
  160. })
  161. describe('setDoc', function () {
  162. beforeEach(function () {
  163. this.lines = ['one', 'two', 'three']
  164. this.source = 'dropbox'
  165. this.user_id = 'user-id-123'
  166. this.req = {
  167. headers: {},
  168. params: {
  169. project_id: this.project_id,
  170. doc_id: this.doc_id
  171. },
  172. body: {
  173. lines: this.lines,
  174. source: this.source,
  175. user_id: this.user_id,
  176. undoing: (this.undoing = true)
  177. }
  178. }
  179. })
  180. describe('successfully', function () {
  181. beforeEach(function () {
  182. this.DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6)
  183. this.HttpController.setDoc(this.req, this.res, this.next)
  184. })
  185. it('should set the doc', function () {
  186. this.DocumentManager.setDocWithLock
  187. .calledWith(
  188. this.project_id,
  189. this.doc_id,
  190. this.lines,
  191. this.source,
  192. this.user_id,
  193. this.undoing
  194. )
  195. .should.equal(true)
  196. })
  197. it('should return a successful No Content response', function () {
  198. this.res.sendStatus.calledWith(204).should.equal(true)
  199. })
  200. it('should log the request', function () {
  201. this.logger.log
  202. .calledWith(
  203. {
  204. doc_id: this.doc_id,
  205. project_id: this.project_id,
  206. lines: this.lines,
  207. source: this.source,
  208. user_id: this.user_id,
  209. undoing: this.undoing
  210. },
  211. 'setting doc via http'
  212. )
  213. .should.equal(true)
  214. })
  215. it('should time the request', function () {
  216. this.Metrics.Timer.prototype.done.called.should.equal(true)
  217. })
  218. })
  219. describe('when an errors occurs', function () {
  220. beforeEach(function () {
  221. this.DocumentManager.setDocWithLock = sinon
  222. .stub()
  223. .callsArgWith(6, new Error('oops'))
  224. this.HttpController.setDoc(this.req, this.res, this.next)
  225. })
  226. it('should call next with the error', function () {
  227. this.next.calledWith(new Error('oops')).should.equal(true)
  228. })
  229. })
  230. describe('when the payload is too large', function () {
  231. beforeEach(function () {
  232. const lines = []
  233. for (let _ = 0; _ <= 200000; _++) {
  234. lines.push('test test test')
  235. }
  236. this.req.body.lines = lines
  237. this.DocumentManager.setDocWithLock = sinon.stub().callsArgWith(6)
  238. this.HttpController.setDoc(this.req, this.res, this.next)
  239. })
  240. it('should send back a 406 response', function () {
  241. this.res.sendStatus.calledWith(406).should.equal(true)
  242. })
  243. it('should not call setDocWithLock', function () {
  244. this.DocumentManager.setDocWithLock.callCount.should.equal(0)
  245. })
  246. })
  247. })
  248. describe('flushProject', function () {
  249. beforeEach(function () {
  250. this.req = {
  251. params: {
  252. project_id: this.project_id
  253. }
  254. }
  255. })
  256. describe('successfully', function () {
  257. beforeEach(function () {
  258. this.ProjectManager.flushProjectWithLocks = sinon.stub().callsArgWith(1)
  259. this.HttpController.flushProject(this.req, this.res, this.next)
  260. })
  261. it('should flush the project', function () {
  262. this.ProjectManager.flushProjectWithLocks
  263. .calledWith(this.project_id)
  264. .should.equal(true)
  265. })
  266. it('should return a successful No Content response', function () {
  267. this.res.sendStatus.calledWith(204).should.equal(true)
  268. })
  269. it('should log the request', function () {
  270. this.logger.log
  271. .calledWith(
  272. { project_id: this.project_id },
  273. 'flushing project via http'
  274. )
  275. .should.equal(true)
  276. })
  277. it('should time the request', function () {
  278. this.Metrics.Timer.prototype.done.called.should.equal(true)
  279. })
  280. })
  281. describe('when an errors occurs', function () {
  282. beforeEach(function () {
  283. this.ProjectManager.flushProjectWithLocks = sinon
  284. .stub()
  285. .callsArgWith(1, new Error('oops'))
  286. this.HttpController.flushProject(this.req, this.res, this.next)
  287. })
  288. it('should call next with the error', function () {
  289. this.next.calledWith(new Error('oops')).should.equal(true)
  290. })
  291. })
  292. })
  293. describe('flushDocIfLoaded', function () {
  294. beforeEach(function () {
  295. this.lines = ['one', 'two', 'three']
  296. this.version = 42
  297. this.req = {
  298. params: {
  299. project_id: this.project_id,
  300. doc_id: this.doc_id
  301. }
  302. }
  303. })
  304. describe('successfully', function () {
  305. beforeEach(function () {
  306. this.DocumentManager.flushDocIfLoadedWithLock = sinon
  307. .stub()
  308. .callsArgWith(2)
  309. this.HttpController.flushDocIfLoaded(this.req, this.res, this.next)
  310. })
  311. it('should flush the doc', function () {
  312. this.DocumentManager.flushDocIfLoadedWithLock
  313. .calledWith(this.project_id, this.doc_id)
  314. .should.equal(true)
  315. })
  316. it('should return a successful No Content response', function () {
  317. this.res.sendStatus.calledWith(204).should.equal(true)
  318. })
  319. it('should log the request', function () {
  320. this.logger.log
  321. .calledWith(
  322. { doc_id: this.doc_id, project_id: this.project_id },
  323. 'flushing doc via http'
  324. )
  325. .should.equal(true)
  326. })
  327. it('should time the request', function () {
  328. this.Metrics.Timer.prototype.done.called.should.equal(true)
  329. })
  330. })
  331. describe('when an errors occurs', function () {
  332. beforeEach(function () {
  333. this.DocumentManager.flushDocIfLoadedWithLock = sinon
  334. .stub()
  335. .callsArgWith(2, new Error('oops'))
  336. this.HttpController.flushDocIfLoaded(this.req, this.res, this.next)
  337. })
  338. it('should call next with the error', function () {
  339. this.next.calledWith(new Error('oops')).should.equal(true)
  340. })
  341. })
  342. })
  343. describe('deleteDoc', function () {
  344. beforeEach(function () {
  345. this.req = {
  346. params: {
  347. project_id: this.project_id,
  348. doc_id: this.doc_id
  349. },
  350. query: {}
  351. }
  352. })
  353. describe('successfully', function () {
  354. beforeEach(function () {
  355. this.DocumentManager.flushAndDeleteDocWithLock = sinon
  356. .stub()
  357. .callsArgWith(3)
  358. this.HttpController.deleteDoc(this.req, this.res, this.next)
  359. })
  360. it('should flush and delete the doc', function () {
  361. this.DocumentManager.flushAndDeleteDocWithLock
  362. .calledWith(this.project_id, this.doc_id, {
  363. ignoreFlushErrors: false
  364. })
  365. .should.equal(true)
  366. })
  367. it('should flush project history', function () {
  368. this.HistoryManager.flushProjectChangesAsync
  369. .calledWithExactly(this.project_id)
  370. .should.equal(true)
  371. })
  372. it('should return a successful No Content response', function () {
  373. this.res.sendStatus.calledWith(204).should.equal(true)
  374. })
  375. it('should log the request', function () {
  376. this.logger.log
  377. .calledWith(
  378. { doc_id: this.doc_id, project_id: this.project_id },
  379. 'deleting doc via http'
  380. )
  381. .should.equal(true)
  382. })
  383. it('should time the request', function () {
  384. this.Metrics.Timer.prototype.done.called.should.equal(true)
  385. })
  386. })
  387. describe('ignoring errors', function () {
  388. beforeEach(function () {
  389. this.req.query.ignore_flush_errors = 'true'
  390. this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().yields()
  391. this.HttpController.deleteDoc(this.req, this.res, this.next)
  392. })
  393. it('should delete the doc', function () {
  394. this.DocumentManager.flushAndDeleteDocWithLock
  395. .calledWith(this.project_id, this.doc_id, { ignoreFlushErrors: true })
  396. .should.equal(true)
  397. })
  398. it('should return a successful No Content response', function () {
  399. this.res.sendStatus.calledWith(204).should.equal(true)
  400. })
  401. })
  402. describe('when an errors occurs', function () {
  403. beforeEach(function () {
  404. this.DocumentManager.flushAndDeleteDocWithLock = sinon
  405. .stub()
  406. .callsArgWith(3, new Error('oops'))
  407. this.HttpController.deleteDoc(this.req, this.res, this.next)
  408. })
  409. it('should flush project history', function () {
  410. this.HistoryManager.flushProjectChangesAsync
  411. .calledWithExactly(this.project_id)
  412. .should.equal(true)
  413. })
  414. it('should call next with the error', function () {
  415. this.next.calledWith(new Error('oops')).should.equal(true)
  416. })
  417. })
  418. })
  419. describe('deleteProject', function () {
  420. beforeEach(function () {
  421. this.req = {
  422. params: {
  423. project_id: this.project_id
  424. }
  425. }
  426. })
  427. describe('successfully', function () {
  428. beforeEach(function () {
  429. this.ProjectManager.flushAndDeleteProjectWithLocks = sinon
  430. .stub()
  431. .callsArgWith(2)
  432. this.HttpController.deleteProject(this.req, this.res, this.next)
  433. })
  434. it('should delete the project', function () {
  435. this.ProjectManager.flushAndDeleteProjectWithLocks
  436. .calledWith(this.project_id)
  437. .should.equal(true)
  438. })
  439. it('should return a successful No Content response', function () {
  440. this.res.sendStatus.calledWith(204).should.equal(true)
  441. })
  442. it('should log the request', function () {
  443. this.logger.log
  444. .calledWith(
  445. { project_id: this.project_id },
  446. 'deleting project via http'
  447. )
  448. .should.equal(true)
  449. })
  450. it('should time the request', function () {
  451. this.Metrics.Timer.prototype.done.called.should.equal(true)
  452. })
  453. })
  454. describe('with the background=true option from realtime', function () {
  455. beforeEach(function () {
  456. this.ProjectManager.queueFlushAndDeleteProject = sinon
  457. .stub()
  458. .callsArgWith(1)
  459. this.req.query = { background: true, shutdown: true }
  460. this.HttpController.deleteProject(this.req, this.res, this.next)
  461. })
  462. it('should queue the flush and delete', function () {
  463. this.ProjectManager.queueFlushAndDeleteProject
  464. .calledWith(this.project_id)
  465. .should.equal(true)
  466. })
  467. })
  468. describe('when an errors occurs', function () {
  469. beforeEach(function () {
  470. this.ProjectManager.flushAndDeleteProjectWithLocks = sinon
  471. .stub()
  472. .callsArgWith(2, new Error('oops'))
  473. this.HttpController.deleteProject(this.req, this.res, this.next)
  474. })
  475. it('should call next with the error', function () {
  476. this.next.calledWith(new Error('oops')).should.equal(true)
  477. })
  478. })
  479. })
  480. describe('acceptChanges', function () {
  481. beforeEach(function () {
  482. this.req = {
  483. params: {
  484. project_id: this.project_id,
  485. doc_id: this.doc_id,
  486. change_id: (this.change_id = 'mock-change-od-1')
  487. }
  488. }
  489. })
  490. describe('successfully with a single change', function () {
  491. beforeEach(function () {
  492. this.DocumentManager.acceptChangesWithLock = sinon
  493. .stub()
  494. .callsArgWith(3)
  495. this.HttpController.acceptChanges(this.req, this.res, this.next)
  496. })
  497. it('should accept the change', function () {
  498. this.DocumentManager.acceptChangesWithLock
  499. .calledWith(this.project_id, this.doc_id, [this.change_id])
  500. .should.equal(true)
  501. })
  502. it('should return a successful No Content response', function () {
  503. this.res.sendStatus.calledWith(204).should.equal(true)
  504. })
  505. it('should log the request', function () {
  506. this.logger.log
  507. .calledWith(
  508. { project_id: this.project_id, doc_id: this.doc_id },
  509. 'accepting 1 changes via http'
  510. )
  511. .should.equal(true)
  512. })
  513. it('should time the request', function () {
  514. this.Metrics.Timer.prototype.done.called.should.equal(true)
  515. })
  516. })
  517. describe('succesfully with with multiple changes', function () {
  518. beforeEach(function () {
  519. this.change_ids = [
  520. 'mock-change-od-1',
  521. 'mock-change-od-2',
  522. 'mock-change-od-3',
  523. 'mock-change-od-4'
  524. ]
  525. this.req.body = { change_ids: this.change_ids }
  526. this.DocumentManager.acceptChangesWithLock = sinon
  527. .stub()
  528. .callsArgWith(3)
  529. this.HttpController.acceptChanges(this.req, this.res, this.next)
  530. })
  531. it('should accept the changes in the body payload', function () {
  532. this.DocumentManager.acceptChangesWithLock
  533. .calledWith(this.project_id, this.doc_id, this.change_ids)
  534. .should.equal(true)
  535. })
  536. it('should log the request with the correct number of changes', function () {
  537. this.logger.log
  538. .calledWith(
  539. { project_id: this.project_id, doc_id: this.doc_id },
  540. `accepting ${this.change_ids.length} changes via http`
  541. )
  542. .should.equal(true)
  543. })
  544. })
  545. describe('when an errors occurs', function () {
  546. beforeEach(function () {
  547. this.DocumentManager.acceptChangesWithLock = sinon
  548. .stub()
  549. .callsArgWith(3, new Error('oops'))
  550. this.HttpController.acceptChanges(this.req, this.res, this.next)
  551. })
  552. it('should call next with the error', function () {
  553. this.next.calledWith(new Error('oops')).should.equal(true)
  554. })
  555. })
  556. })
  557. describe('deleteComment', function () {
  558. beforeEach(function () {
  559. this.req = {
  560. params: {
  561. project_id: this.project_id,
  562. doc_id: this.doc_id,
  563. comment_id: (this.comment_id = 'mock-comment-id')
  564. }
  565. }
  566. })
  567. describe('successfully', function () {
  568. beforeEach(function () {
  569. this.DocumentManager.deleteCommentWithLock = sinon
  570. .stub()
  571. .callsArgWith(3)
  572. this.HttpController.deleteComment(this.req, this.res, this.next)
  573. })
  574. it('should accept the change', function () {
  575. this.DocumentManager.deleteCommentWithLock
  576. .calledWith(this.project_id, this.doc_id, this.comment_id)
  577. .should.equal(true)
  578. })
  579. it('should return a successful No Content response', function () {
  580. this.res.sendStatus.calledWith(204).should.equal(true)
  581. })
  582. it('should log the request', function () {
  583. this.logger.log
  584. .calledWith(
  585. {
  586. project_id: this.project_id,
  587. doc_id: this.doc_id,
  588. comment_id: this.comment_id
  589. },
  590. 'deleting comment via http'
  591. )
  592. .should.equal(true)
  593. })
  594. it('should time the request', function () {
  595. this.Metrics.Timer.prototype.done.called.should.equal(true)
  596. })
  597. })
  598. describe('when an errors occurs', function () {
  599. beforeEach(function () {
  600. this.DocumentManager.deleteCommentWithLock = sinon
  601. .stub()
  602. .callsArgWith(3, new Error('oops'))
  603. this.HttpController.deleteComment(this.req, this.res, this.next)
  604. })
  605. it('should call next with the error', function () {
  606. this.next.calledWith(new Error('oops')).should.equal(true)
  607. })
  608. })
  609. })
  610. describe('getProjectDocsAndFlushIfOld', function () {
  611. beforeEach(function () {
  612. this.state = '01234567890abcdef'
  613. this.docs = [
  614. { _id: '1234', lines: 'hello', v: 23 },
  615. { _id: '4567', lines: 'world', v: 45 }
  616. ]
  617. this.req = {
  618. params: {
  619. project_id: this.project_id
  620. },
  621. query: {
  622. state: this.state
  623. }
  624. }
  625. })
  626. describe('successfully', function () {
  627. beforeEach(function () {
  628. this.ProjectManager.getProjectDocsAndFlushIfOld = sinon
  629. .stub()
  630. .callsArgWith(3, null, this.docs)
  631. this.HttpController.getProjectDocsAndFlushIfOld(
  632. this.req,
  633. this.res,
  634. this.next
  635. )
  636. })
  637. it('should get docs from the project manager', function () {
  638. this.ProjectManager.getProjectDocsAndFlushIfOld
  639. .calledWith(this.project_id, this.state, {})
  640. .should.equal(true)
  641. })
  642. it('should return a successful response', function () {
  643. this.res.send.calledWith(this.docs).should.equal(true)
  644. })
  645. it('should log the request', function () {
  646. this.logger.log
  647. .calledWith(
  648. { project_id: this.project_id, exclude: [] },
  649. 'getting docs via http'
  650. )
  651. .should.equal(true)
  652. })
  653. it('should log the response', function () {
  654. this.logger.log
  655. .calledWith(
  656. { project_id: this.project_id, result: ['1234:23', '4567:45'] },
  657. 'got docs via http'
  658. )
  659. .should.equal(true)
  660. })
  661. it('should time the request', function () {
  662. this.Metrics.Timer.prototype.done.called.should.equal(true)
  663. })
  664. })
  665. describe('when there is a conflict', function () {
  666. beforeEach(function () {
  667. this.ProjectManager.getProjectDocsAndFlushIfOld = sinon
  668. .stub()
  669. .callsArgWith(
  670. 3,
  671. new Errors.ProjectStateChangedError('project state changed')
  672. )
  673. this.HttpController.getProjectDocsAndFlushIfOld(
  674. this.req,
  675. this.res,
  676. this.next
  677. )
  678. })
  679. it('should return an HTTP 409 Conflict response', function () {
  680. this.res.sendStatus.calledWith(409).should.equal(true)
  681. })
  682. })
  683. describe('when an error occurs', function () {
  684. beforeEach(function () {
  685. this.ProjectManager.getProjectDocsAndFlushIfOld = sinon
  686. .stub()
  687. .callsArgWith(3, new Error('oops'))
  688. this.HttpController.getProjectDocsAndFlushIfOld(
  689. this.req,
  690. this.res,
  691. this.next
  692. )
  693. })
  694. it('should call next with the error', function () {
  695. this.next.calledWith(new Error('oops')).should.equal(true)
  696. })
  697. })
  698. })
  699. describe('updateProject', function () {
  700. beforeEach(function () {
  701. this.projectHistoryId = 'history-id-123'
  702. this.userId = 'user-id-123'
  703. this.docUpdates = sinon.stub()
  704. this.fileUpdates = sinon.stub()
  705. this.version = 1234567
  706. this.req = {
  707. body: {
  708. projectHistoryId: this.projectHistoryId,
  709. userId: this.userId,
  710. docUpdates: this.docUpdates,
  711. fileUpdates: this.fileUpdates,
  712. version: this.version
  713. },
  714. params: {
  715. project_id: this.project_id
  716. }
  717. }
  718. })
  719. describe('successfully', function () {
  720. beforeEach(function () {
  721. this.ProjectManager.updateProjectWithLocks = sinon
  722. .stub()
  723. .callsArgWith(6)
  724. this.HttpController.updateProject(this.req, this.res, this.next)
  725. })
  726. it('should accept the change', function () {
  727. this.ProjectManager.updateProjectWithLocks
  728. .calledWith(
  729. this.project_id,
  730. this.projectHistoryId,
  731. this.userId,
  732. this.docUpdates,
  733. this.fileUpdates,
  734. this.version
  735. )
  736. .should.equal(true)
  737. })
  738. it('should return a successful No Content response', function () {
  739. this.res.sendStatus.calledWith(204).should.equal(true)
  740. })
  741. it('should time the request', function () {
  742. this.Metrics.Timer.prototype.done.called.should.equal(true)
  743. })
  744. })
  745. describe('when an errors occurs', function () {
  746. beforeEach(function () {
  747. this.ProjectManager.updateProjectWithLocks = sinon
  748. .stub()
  749. .callsArgWith(6, new Error('oops'))
  750. this.HttpController.updateProject(this.req, this.res, this.next)
  751. })
  752. it('should call next with the error', function () {
  753. this.next.calledWith(new Error('oops')).should.equal(true)
  754. })
  755. })
  756. })
  757. describe('resyncProjectHistory', function () {
  758. beforeEach(function () {
  759. this.projectHistoryId = 'history-id-123'
  760. this.docs = sinon.stub()
  761. this.files = sinon.stub()
  762. this.fileUpdates = sinon.stub()
  763. this.req = {
  764. body: {
  765. projectHistoryId: this.projectHistoryId,
  766. docs: this.docs,
  767. files: this.files
  768. },
  769. params: {
  770. project_id: this.project_id
  771. }
  772. }
  773. })
  774. describe('successfully', function () {
  775. beforeEach(function () {
  776. this.HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4)
  777. this.HttpController.resyncProjectHistory(this.req, this.res, this.next)
  778. })
  779. it('should accept the change', function () {
  780. this.HistoryManager.resyncProjectHistory
  781. .calledWith(
  782. this.project_id,
  783. this.projectHistoryId,
  784. this.docs,
  785. this.files
  786. )
  787. .should.equal(true)
  788. })
  789. it('should return a successful No Content response', function () {
  790. this.res.sendStatus.calledWith(204).should.equal(true)
  791. })
  792. })
  793. describe('when an errors occurs', function () {
  794. beforeEach(function () {
  795. this.HistoryManager.resyncProjectHistory = sinon
  796. .stub()
  797. .callsArgWith(4, new Error('oops'))
  798. this.HttpController.resyncProjectHistory(this.req, this.res, this.next)
  799. })
  800. it('should call next with the error', function () {
  801. this.next.calledWith(new Error('oops')).should.equal(true)
  802. })
  803. })
  804. })
  805. })