HttpControllerTests.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. /* eslint-disable
  2. no-return-assign,
  3. no-unused-vars,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS102: Remove unnecessary code created because of implicit returns
  10. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  11. */
  12. const SandboxedModule = require('sandboxed-module')
  13. const { assert } = require('chai')
  14. const sinon = require('sinon')
  15. const chai = require('chai')
  16. chai.should()
  17. const { expect } = chai
  18. const modulePath = require('path').join(
  19. __dirname,
  20. '../../../app/js/HttpController'
  21. )
  22. const { ObjectId } = require('mongodb')
  23. describe('HttpController', function () {
  24. beforeEach(function () {
  25. const settings = {
  26. max_doc_length: 2 * 1024 * 1024
  27. }
  28. this.HttpController = SandboxedModule.require(modulePath, {
  29. requires: {
  30. './DocManager': (this.DocManager = {}),
  31. './DocArchiveManager': (this.DocArchiveManager = {}),
  32. 'logger-sharelatex': (this.logger = {
  33. log: sinon.stub(),
  34. error: sinon.stub(),
  35. fatal: sinon.stub()
  36. }),
  37. 'settings-sharelatex': settings,
  38. './HealthChecker': {}
  39. },
  40. globals: { process }
  41. })
  42. this.res = {
  43. send: sinon.stub(),
  44. sendStatus: sinon.stub(),
  45. json: sinon.stub(),
  46. setHeader: sinon.stub()
  47. }
  48. this.res.status = sinon.stub().returns(this.res)
  49. this.req = { query: {} }
  50. this.next = sinon.stub()
  51. this.project_id = 'mock-project-id'
  52. this.doc_id = 'mock-doc-id'
  53. this.doc = {
  54. _id: this.doc_id,
  55. lines: ['mock', 'lines', ' here', '', '', ' spaces '],
  56. version: 42,
  57. rev: 5
  58. }
  59. return (this.deletedDoc = {
  60. deleted: true,
  61. _id: this.doc_id,
  62. lines: ['mock', 'lines', ' here', '', '', ' spaces '],
  63. version: 42,
  64. rev: 5
  65. })
  66. })
  67. describe('getDoc', function () {
  68. describe('without deleted docs', function () {
  69. beforeEach(function () {
  70. this.req.params = {
  71. project_id: this.project_id,
  72. doc_id: this.doc_id
  73. }
  74. this.DocManager.getFullDoc = sinon
  75. .stub()
  76. .callsArgWith(2, null, this.doc)
  77. return this.HttpController.getDoc(this.req, this.res, this.next)
  78. })
  79. it('should get the document with the version (including deleted)', function () {
  80. return this.DocManager.getFullDoc
  81. .calledWith(this.project_id, this.doc_id)
  82. .should.equal(true)
  83. })
  84. return it('should return the doc as JSON', function () {
  85. return this.res.json
  86. .calledWith({
  87. _id: this.doc_id,
  88. lines: this.doc.lines,
  89. rev: this.doc.rev,
  90. version: this.doc.version
  91. })
  92. .should.equal(true)
  93. })
  94. })
  95. return describe('which is deleted', function () {
  96. beforeEach(function () {
  97. this.req.params = {
  98. project_id: this.project_id,
  99. doc_id: this.doc_id
  100. }
  101. return (this.DocManager.getFullDoc = sinon
  102. .stub()
  103. .callsArgWith(2, null, this.deletedDoc))
  104. })
  105. it('should get the doc from the doc manager', function () {
  106. this.HttpController.getDoc(this.req, this.res, this.next)
  107. return this.DocManager.getFullDoc
  108. .calledWith(this.project_id, this.doc_id)
  109. .should.equal(true)
  110. })
  111. it('should return 404 if the query string delete is not set ', function () {
  112. this.HttpController.getDoc(this.req, this.res, this.next)
  113. return this.res.sendStatus.calledWith(404).should.equal(true)
  114. })
  115. return it('should return the doc as JSON if include_deleted is set to true', function () {
  116. this.req.query.include_deleted = 'true'
  117. this.HttpController.getDoc(this.req, this.res, this.next)
  118. return this.res.json
  119. .calledWith({
  120. _id: this.doc_id,
  121. lines: this.doc.lines,
  122. rev: this.doc.rev,
  123. deleted: true,
  124. version: this.doc.version
  125. })
  126. .should.equal(true)
  127. })
  128. })
  129. })
  130. describe('getRawDoc', function () {
  131. beforeEach(function () {
  132. this.req.params = {
  133. project_id: this.project_id,
  134. doc_id: this.doc_id
  135. }
  136. this.DocManager.getDocLines = sinon.stub().callsArgWith(2, null, this.doc)
  137. return this.HttpController.getRawDoc(this.req, this.res, this.next)
  138. })
  139. it('should get the document without the version', function () {
  140. return this.DocManager.getDocLines
  141. .calledWith(this.project_id, this.doc_id)
  142. .should.equal(true)
  143. })
  144. it('should set the content type header', function () {
  145. return this.res.setHeader
  146. .calledWith('content-type', 'text/plain')
  147. .should.equal(true)
  148. })
  149. return it('should send the raw version of the doc', function () {
  150. return assert.deepEqual(
  151. this.res.send.args[0][0],
  152. `${this.doc.lines[0]}\n${this.doc.lines[1]}\n${this.doc.lines[2]}\n${this.doc.lines[3]}\n${this.doc.lines[4]}\n${this.doc.lines[5]}`
  153. )
  154. })
  155. })
  156. describe('getAllDocs', function () {
  157. describe('normally', function () {
  158. beforeEach(function () {
  159. this.req.params = { project_id: this.project_id }
  160. this.docs = [
  161. {
  162. _id: ObjectId(),
  163. lines: ['mock', 'lines', 'one'],
  164. rev: 2
  165. },
  166. {
  167. _id: ObjectId(),
  168. lines: ['mock', 'lines', 'two'],
  169. rev: 4
  170. }
  171. ]
  172. this.DocManager.getAllNonDeletedDocs = sinon
  173. .stub()
  174. .callsArgWith(2, null, this.docs)
  175. return this.HttpController.getAllDocs(this.req, this.res, this.next)
  176. })
  177. it('should get all the (non-deleted) docs', function () {
  178. return this.DocManager.getAllNonDeletedDocs
  179. .calledWith(this.project_id, { lines: true, rev: true })
  180. .should.equal(true)
  181. })
  182. return it('should return the doc as JSON', function () {
  183. return this.res.json
  184. .calledWith([
  185. {
  186. _id: this.docs[0]._id.toString(),
  187. lines: this.docs[0].lines,
  188. rev: this.docs[0].rev
  189. },
  190. {
  191. _id: this.docs[1]._id.toString(),
  192. lines: this.docs[1].lines,
  193. rev: this.docs[1].rev
  194. }
  195. ])
  196. .should.equal(true)
  197. })
  198. })
  199. return describe('with a null doc', function () {
  200. beforeEach(function () {
  201. this.req.params = { project_id: this.project_id }
  202. this.docs = [
  203. {
  204. _id: ObjectId(),
  205. lines: ['mock', 'lines', 'one'],
  206. rev: 2
  207. },
  208. null,
  209. {
  210. _id: ObjectId(),
  211. lines: ['mock', 'lines', 'two'],
  212. rev: 4
  213. }
  214. ]
  215. this.DocManager.getAllNonDeletedDocs = sinon
  216. .stub()
  217. .callsArgWith(2, null, this.docs)
  218. return this.HttpController.getAllDocs(this.req, this.res, this.next)
  219. })
  220. it('should return the non null docs as JSON', function () {
  221. return this.res.json
  222. .calledWith([
  223. {
  224. _id: this.docs[0]._id.toString(),
  225. lines: this.docs[0].lines,
  226. rev: this.docs[0].rev
  227. },
  228. {
  229. _id: this.docs[2]._id.toString(),
  230. lines: this.docs[2].lines,
  231. rev: this.docs[2].rev
  232. }
  233. ])
  234. .should.equal(true)
  235. })
  236. return it('should log out an error', function () {
  237. return this.logger.error
  238. .calledWith(
  239. {
  240. err: sinon.match.has('message', 'null doc'),
  241. project_id: this.project_id
  242. },
  243. 'encountered null doc'
  244. )
  245. .should.equal(true)
  246. })
  247. })
  248. })
  249. describe('getAllRanges', function () {
  250. return describe('normally', function () {
  251. beforeEach(function () {
  252. this.req.params = { project_id: this.project_id }
  253. this.docs = [
  254. {
  255. _id: ObjectId(),
  256. ranges: { mock_ranges: 'one' }
  257. },
  258. {
  259. _id: ObjectId(),
  260. ranges: { mock_ranges: 'two' }
  261. }
  262. ]
  263. this.DocManager.getAllNonDeletedDocs = sinon
  264. .stub()
  265. .callsArgWith(2, null, this.docs)
  266. return this.HttpController.getAllRanges(this.req, this.res, this.next)
  267. })
  268. it('should get all the (non-deleted) doc ranges', function () {
  269. return this.DocManager.getAllNonDeletedDocs
  270. .calledWith(this.project_id, { ranges: true })
  271. .should.equal(true)
  272. })
  273. return it('should return the doc as JSON', function () {
  274. return this.res.json
  275. .calledWith([
  276. {
  277. _id: this.docs[0]._id.toString(),
  278. ranges: this.docs[0].ranges
  279. },
  280. {
  281. _id: this.docs[1]._id.toString(),
  282. ranges: this.docs[1].ranges
  283. }
  284. ])
  285. .should.equal(true)
  286. })
  287. })
  288. })
  289. describe('updateDoc', function () {
  290. beforeEach(function () {
  291. return (this.req.params = {
  292. project_id: this.project_id,
  293. doc_id: this.doc_id
  294. })
  295. })
  296. describe('when the doc lines exist and were updated', function () {
  297. beforeEach(function () {
  298. this.req.body = {
  299. lines: (this.lines = ['hello', 'world']),
  300. version: (this.version = 42),
  301. ranges: (this.ranges = { changes: 'mock' })
  302. }
  303. this.DocManager.updateDoc = sinon
  304. .stub()
  305. .yields(null, true, (this.rev = 5))
  306. return this.HttpController.updateDoc(this.req, this.res, this.next)
  307. })
  308. it('should update the document', function () {
  309. return this.DocManager.updateDoc
  310. .calledWith(
  311. this.project_id,
  312. this.doc_id,
  313. this.lines,
  314. this.version,
  315. this.ranges
  316. )
  317. .should.equal(true)
  318. })
  319. return it('should return a modified status', function () {
  320. return this.res.json
  321. .calledWith({ modified: true, rev: this.rev })
  322. .should.equal(true)
  323. })
  324. })
  325. describe('when the doc lines exist and were not updated', function () {
  326. beforeEach(function () {
  327. this.req.body = {
  328. lines: (this.lines = ['hello', 'world']),
  329. version: (this.version = 42),
  330. ranges: {}
  331. }
  332. this.DocManager.updateDoc = sinon
  333. .stub()
  334. .yields(null, false, (this.rev = 5))
  335. return this.HttpController.updateDoc(this.req, this.res, this.next)
  336. })
  337. return it('should return a modified status', function () {
  338. return this.res.json
  339. .calledWith({ modified: false, rev: this.rev })
  340. .should.equal(true)
  341. })
  342. })
  343. describe('when the doc lines are not provided', function () {
  344. beforeEach(function () {
  345. this.req.body = { version: 42, ranges: {} }
  346. this.DocManager.updateDoc = sinon.stub().yields(null, false)
  347. return this.HttpController.updateDoc(this.req, this.res, this.next)
  348. })
  349. it('should not update the document', function () {
  350. return this.DocManager.updateDoc.called.should.equal(false)
  351. })
  352. return it('should return a 400 (bad request) response', function () {
  353. return this.res.sendStatus.calledWith(400).should.equal(true)
  354. })
  355. })
  356. describe('when the doc version are not provided', function () {
  357. beforeEach(function () {
  358. this.req.body = { version: 42, lines: ['hello world'] }
  359. this.DocManager.updateDoc = sinon.stub().yields(null, false)
  360. return this.HttpController.updateDoc(this.req, this.res, this.next)
  361. })
  362. it('should not update the document', function () {
  363. return this.DocManager.updateDoc.called.should.equal(false)
  364. })
  365. return it('should return a 400 (bad request) response', function () {
  366. return this.res.sendStatus.calledWith(400).should.equal(true)
  367. })
  368. })
  369. describe('when the doc ranges is not provided', function () {
  370. beforeEach(function () {
  371. this.req.body = { lines: ['foo'], version: 42 }
  372. this.DocManager.updateDoc = sinon.stub().yields(null, false)
  373. return this.HttpController.updateDoc(this.req, this.res, this.next)
  374. })
  375. it('should not update the document', function () {
  376. return this.DocManager.updateDoc.called.should.equal(false)
  377. })
  378. return it('should return a 400 (bad request) response', function () {
  379. return this.res.sendStatus.calledWith(400).should.equal(true)
  380. })
  381. })
  382. return describe('when the doc body is too large', function () {
  383. beforeEach(function () {
  384. this.req.body = {
  385. lines: (this.lines = Array(2049).fill('a'.repeat(1024))),
  386. version: (this.version = 42),
  387. ranges: (this.ranges = { changes: 'mock' })
  388. }
  389. return this.HttpController.updateDoc(this.req, this.res, this.next)
  390. })
  391. it('should return a 413 (too large) response', function () {
  392. return sinon.assert.calledWith(this.res.status, 413)
  393. })
  394. return it('should report that the document body is too large', function () {
  395. return sinon.assert.calledWith(this.res.send, 'document body too large')
  396. })
  397. })
  398. })
  399. describe('deleteDoc', function () {
  400. beforeEach(function () {
  401. this.req.params = {
  402. project_id: this.project_id,
  403. doc_id: this.doc_id
  404. }
  405. this.DocManager.deleteDoc = sinon.stub().callsArg(2)
  406. return this.HttpController.deleteDoc(this.req, this.res, this.next)
  407. })
  408. it('should delete the document', function () {
  409. return this.DocManager.deleteDoc
  410. .calledWith(this.project_id, this.doc_id)
  411. .should.equal(true)
  412. })
  413. return it('should return a 204 (No Content)', function () {
  414. return this.res.sendStatus.calledWith(204).should.equal(true)
  415. })
  416. })
  417. describe('patchDoc', function () {
  418. beforeEach(function () {
  419. this.req.params = {
  420. project_id: this.project_id,
  421. doc_id: this.doc_id
  422. }
  423. this.req.body = { name: 'foo.tex' }
  424. this.DocManager.patchDoc = sinon.stub().yields(null)
  425. this.HttpController.patchDoc(this.req, this.res, this.next)
  426. })
  427. it('should delete the document', function () {
  428. expect(this.DocManager.patchDoc).to.have.been.calledWith(
  429. this.project_id,
  430. this.doc_id
  431. )
  432. })
  433. it('should return a 204 (No Content)', function () {
  434. expect(this.res.sendStatus).to.have.been.calledWith(204)
  435. })
  436. describe('with an invalid payload', function () {
  437. beforeEach(function () {
  438. this.req.body = { cannot: 'happen' }
  439. this.DocManager.patchDoc = sinon.stub().yields(null)
  440. this.HttpController.patchDoc(this.req, this.res, this.next)
  441. })
  442. it('should log a message', function () {
  443. expect(this.logger.fatal).to.have.been.calledWith(
  444. { field: 'cannot' },
  445. 'joi validation for pathDoc is broken'
  446. )
  447. })
  448. it('should not pass the invalid field along', function () {
  449. expect(this.DocManager.patchDoc).to.have.been.calledWith(
  450. this.project_id,
  451. this.doc_id,
  452. {}
  453. )
  454. })
  455. })
  456. })
  457. describe('archiveAllDocs', function () {
  458. beforeEach(function () {
  459. this.req.params = { project_id: this.project_id }
  460. this.DocArchiveManager.archiveAllDocs = sinon.stub().callsArg(1)
  461. return this.HttpController.archiveAllDocs(this.req, this.res, this.next)
  462. })
  463. it('should archive the project', function () {
  464. return this.DocArchiveManager.archiveAllDocs
  465. .calledWith(this.project_id)
  466. .should.equal(true)
  467. })
  468. return it('should return a 204 (No Content)', function () {
  469. return this.res.sendStatus.calledWith(204).should.equal(true)
  470. })
  471. })
  472. return describe('destroyAllDocs', function () {
  473. beforeEach(function () {
  474. this.req.params = { project_id: this.project_id }
  475. this.DocArchiveManager.destroyAllDocs = sinon.stub().callsArg(1)
  476. return this.HttpController.destroyAllDocs(this.req, this.res, this.next)
  477. })
  478. it('should destroy the docs', function () {
  479. return sinon.assert.calledWith(
  480. this.DocArchiveManager.destroyAllDocs,
  481. this.project_id
  482. )
  483. })
  484. return it('should return 204', function () {
  485. return sinon.assert.calledWith(this.res.sendStatus, 204)
  486. })
  487. })
  488. })