HttpControllerTests.js 30 KB

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