CompileControllerTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. /* eslint-disable
  2. camelcase,
  3. max-len,
  4. no-return-assign,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. const sinon = require('sinon')
  15. const chai = require('chai')
  16. const should = chai.should()
  17. const { assert } = require('chai')
  18. const { expect } = chai
  19. const modulePath = '../../../../app/src/Features/Compile/CompileController.js'
  20. const SandboxedModule = require('sandboxed-module')
  21. const MockRequest = require('../helpers/MockRequest')
  22. const MockResponse = require('../helpers/MockResponse')
  23. describe('CompileController', function() {
  24. beforeEach(function() {
  25. this.user_id = 'wat'
  26. this.user = {
  27. _id: this.user_id,
  28. email: 'user@example.com',
  29. features: {
  30. compileGroup: 'premium',
  31. compileTimeout: 100
  32. }
  33. }
  34. this.CompileManager = { compile: sinon.stub() }
  35. this.ClsiManager = {}
  36. this.UserGetter = { getUser: sinon.stub() }
  37. this.RateLimiter = { addCount: sinon.stub() }
  38. this.settings = {
  39. apis: {
  40. clsi: {
  41. url: 'clsi.example.com'
  42. },
  43. clsi_priority: {
  44. url: 'clsi-priority.example.com'
  45. }
  46. },
  47. defaultFeatures: {
  48. compileGroup: 'standard',
  49. compileTimeout: 60
  50. }
  51. }
  52. this.jar = { cookie: 'stuff' }
  53. this.ClsiCookieManager = {
  54. getCookieJar: sinon.stub().callsArgWith(1, null, this.jar)
  55. }
  56. this.AuthenticationController = {
  57. getLoggedInUser: sinon.stub().callsArgWith(1, null, this.user),
  58. getLoggedInUserId: sinon.stub().returns(this.user_id),
  59. getSessionUser: sinon.stub().returns(this.user),
  60. isUserLoggedIn: sinon.stub().returns(true)
  61. }
  62. this.CompileController = SandboxedModule.require(modulePath, {
  63. globals: {
  64. console: console
  65. },
  66. requires: {
  67. 'settings-sharelatex': this.settings,
  68. request: (this.request = sinon.stub()),
  69. '../Project/ProjectGetter': (this.ProjectGetter = {}),
  70. 'logger-sharelatex': (this.logger = {
  71. log: sinon.stub(),
  72. error: sinon.stub()
  73. }),
  74. '@overleaf/metrics': (this.Metrics = { inc: sinon.stub() }),
  75. './CompileManager': this.CompileManager,
  76. '../User/UserGetter': this.UserGetter,
  77. './ClsiManager': this.ClsiManager,
  78. '../Authentication/AuthenticationController': this
  79. .AuthenticationController,
  80. '../../infrastructure/RateLimiter': this.RateLimiter,
  81. './ClsiCookieManager': () => this.ClsiCookieManager
  82. }
  83. })
  84. this.project_id = 'project-id'
  85. this.next = sinon.stub()
  86. this.req = new MockRequest()
  87. return (this.res = new MockResponse())
  88. })
  89. describe('compile', function() {
  90. beforeEach(function() {
  91. this.req.params = { Project_id: this.project_id }
  92. this.req.session = {}
  93. return (this.CompileManager.compile = sinon
  94. .stub()
  95. .callsArgWith(
  96. 3,
  97. null,
  98. (this.status = 'success'),
  99. (this.outputFiles = ['mock-output-files'])
  100. ))
  101. })
  102. describe('when not an auto compile', function() {
  103. beforeEach(function() {
  104. return this.CompileController.compile(this.req, this.res, this.next)
  105. })
  106. it('should look up the user id', function() {
  107. return this.AuthenticationController.getLoggedInUserId
  108. .calledWith(this.req)
  109. .should.equal(true)
  110. })
  111. it('should do the compile without the auto compile flag', function() {
  112. return this.CompileManager.compile
  113. .calledWith(this.project_id, this.user_id, { isAutoCompile: false })
  114. .should.equal(true)
  115. })
  116. it('should set the content-type of the response to application/json', function() {
  117. this.res.type.should.equal('application/json')
  118. })
  119. it('should send a successful response reporting the status and files', function() {
  120. this.res.statusCode.should.equal(200)
  121. this.res.body.should.equal(
  122. JSON.stringify({
  123. status: this.status,
  124. outputFiles: this.outputFiles
  125. })
  126. )
  127. })
  128. })
  129. describe('when an auto compile', function() {
  130. beforeEach(function() {
  131. this.req.query = { auto_compile: 'true' }
  132. return this.CompileController.compile(this.req, this.res, this.next)
  133. })
  134. it('should do the compile with the auto compile flag', function() {
  135. return this.CompileManager.compile
  136. .calledWith(this.project_id, this.user_id, { isAutoCompile: true })
  137. .should.equal(true)
  138. })
  139. })
  140. describe('with the draft attribute', function() {
  141. beforeEach(function() {
  142. this.req.body = { draft: true }
  143. return this.CompileController.compile(this.req, this.res, this.next)
  144. })
  145. it('should do the compile without the draft compile flag', function() {
  146. return this.CompileManager.compile
  147. .calledWith(this.project_id, this.user_id, {
  148. isAutoCompile: false,
  149. draft: true
  150. })
  151. .should.equal(true)
  152. })
  153. })
  154. })
  155. describe('compileSubmission', function() {
  156. beforeEach(function() {
  157. this.submission_id = 'sub-1234'
  158. this.req.params = { submission_id: this.submission_id }
  159. this.req.body = {}
  160. return (this.ClsiManager.sendExternalRequest = sinon
  161. .stub()
  162. .callsArgWith(
  163. 3,
  164. null,
  165. (this.status = 'success'),
  166. (this.outputFiles = ['mock-output-files']),
  167. (this.clsiServerId = 'mock-server-id'),
  168. (this.validationProblems = null)
  169. ))
  170. })
  171. it('should set the content-type of the response to application/json', function() {
  172. this.CompileController.compileSubmission(this.req, this.res, this.next)
  173. return this.res.contentType
  174. .calledWith('application/json')
  175. .should.equal(true)
  176. })
  177. it('should send a successful response reporting the status and files', function() {
  178. this.CompileController.compileSubmission(this.req, this.res, this.next)
  179. this.res.statusCode.should.equal(200)
  180. return this.res.body.should.equal(
  181. JSON.stringify({
  182. status: this.status,
  183. outputFiles: this.outputFiles,
  184. clsiServerId: 'mock-server-id',
  185. validationProblems: null
  186. })
  187. )
  188. })
  189. describe('with compileGroup and timeout', function() {
  190. beforeEach(function() {
  191. this.req.body = {
  192. compileGroup: 'special',
  193. timeout: 600
  194. }
  195. return this.CompileController.compileSubmission(
  196. this.req,
  197. this.res,
  198. this.next
  199. )
  200. })
  201. it('should use the supplied values', function() {
  202. return this.ClsiManager.sendExternalRequest
  203. .calledWith(
  204. this.submission_id,
  205. { compileGroup: 'special', timeout: 600 },
  206. { compileGroup: 'special', timeout: 600 }
  207. )
  208. .should.equal(true)
  209. })
  210. })
  211. describe('with other supported options but not compileGroup and timeout', function() {
  212. beforeEach(function() {
  213. this.req.body = {
  214. rootResourcePath: 'main.tex',
  215. compiler: 'lualatex',
  216. draft: true,
  217. check: 'validate'
  218. }
  219. return this.CompileController.compileSubmission(
  220. this.req,
  221. this.res,
  222. this.next
  223. )
  224. })
  225. it('should use the other options but default values for compileGroup and timeout', function() {
  226. return this.ClsiManager.sendExternalRequest
  227. .calledWith(
  228. this.submission_id,
  229. {
  230. rootResourcePath: 'main.tex',
  231. compiler: 'lualatex',
  232. draft: true,
  233. check: 'validate'
  234. },
  235. {
  236. rootResourcePath: 'main.tex',
  237. compiler: 'lualatex',
  238. draft: true,
  239. check: 'validate',
  240. compileGroup: 'standard',
  241. timeout: 60
  242. }
  243. )
  244. .should.equal(true)
  245. })
  246. })
  247. })
  248. describe('downloadPdf', function() {
  249. beforeEach(function() {
  250. this.req.params = { Project_id: this.project_id }
  251. this.req.query = { pdfng: true }
  252. this.project = { name: 'test namè' }
  253. return (this.ProjectGetter.getProject = sinon
  254. .stub()
  255. .callsArgWith(2, null, this.project))
  256. })
  257. describe('when downloading for embedding', function() {
  258. beforeEach(function() {
  259. this.CompileController.proxyToClsi = sinon.stub()
  260. this.RateLimiter.addCount.callsArgWith(1, null, true)
  261. return this.CompileController.downloadPdf(this.req, this.res, this.next)
  262. })
  263. it('should look up the project', function() {
  264. return this.ProjectGetter.getProject
  265. .calledWith(this.project_id, { name: 1 })
  266. .should.equal(true)
  267. })
  268. it('should set the content-type of the response to application/pdf', function() {
  269. return this.res.contentType
  270. .calledWith('application/pdf')
  271. .should.equal(true)
  272. })
  273. it('should set the content-disposition header with a safe version of the project name', function() {
  274. return this.res.setContentDisposition
  275. .calledWith('', { filename: 'test_nam_.pdf' })
  276. .should.equal(true)
  277. })
  278. it('should increment the pdf-downloads metric', function() {
  279. return this.Metrics.inc.calledWith('pdf-downloads').should.equal(true)
  280. })
  281. it('should proxy the PDF from the CLSI', function() {
  282. return this.CompileController.proxyToClsi
  283. .calledWith(
  284. this.project_id,
  285. `/project/${this.project_id}/user/${
  286. this.user_id
  287. }/output/output.pdf`,
  288. this.req,
  289. this.res,
  290. this.next
  291. )
  292. .should.equal(true)
  293. })
  294. })
  295. describe('when the a build-id is provided', function() {
  296. beforeEach(function() {
  297. this.req.params.build_id = this.buildId = '1234-5678'
  298. this.CompileController.proxyToClsi = sinon.stub()
  299. this.RateLimiter.addCount.callsArgWith(1, null, true)
  300. return this.CompileController.downloadPdf(this.req, this.res, this.next)
  301. })
  302. it('should proxy the PDF from the CLSI, with a build-id', function() {
  303. return this.CompileController.proxyToClsi
  304. .calledWith(
  305. this.project_id,
  306. `/project/${this.project_id}/user/${this.user_id}/build/${
  307. this.buildId
  308. }/output/output.pdf`,
  309. this.req,
  310. this.res,
  311. this.next
  312. )
  313. .should.equal(true)
  314. })
  315. })
  316. describe('when the pdf is not going to be used in pdfjs viewer', function() {
  317. it('should check the rate limiter when pdfng is not set', function(done) {
  318. this.req.query = {}
  319. this.RateLimiter.addCount.callsArgWith(1, null, true)
  320. this.CompileController.proxyToClsi = (project_id, url) => {
  321. this.RateLimiter.addCount.args[0][0].throttle.should.equal(1000)
  322. return done()
  323. }
  324. return this.CompileController.downloadPdf(this.req, this.res)
  325. })
  326. it('should check the rate limiter when pdfng is false', function(done) {
  327. this.req.query = { pdfng: false }
  328. this.RateLimiter.addCount.callsArgWith(1, null, true)
  329. this.CompileController.proxyToClsi = (project_id, url) => {
  330. this.RateLimiter.addCount.args[0][0].throttle.should.equal(1000)
  331. return done()
  332. }
  333. return this.CompileController.downloadPdf(this.req, this.res)
  334. })
  335. })
  336. })
  337. describe('getFileFromClsiWithoutUser', function() {
  338. beforeEach(function() {
  339. this.submission_id = 'sub-1234'
  340. this.build_id = 123456
  341. this.file = 'project.pdf'
  342. this.req.params = {
  343. submission_id: this.submission_id,
  344. build_id: this.build_id,
  345. file: this.file
  346. }
  347. this.req.body = {}
  348. this.expected_url = `/project/${this.submission_id}/build/${
  349. this.build_id
  350. }/output/${this.file}`
  351. return (this.CompileController.proxyToClsiWithLimits = sinon.stub())
  352. })
  353. describe('without limits specified', function() {
  354. beforeEach(function() {
  355. return this.CompileController.getFileFromClsiWithoutUser(
  356. this.req,
  357. this.res,
  358. this.next
  359. )
  360. })
  361. it('should proxy to CLSI with correct URL and default limits', function() {
  362. return this.CompileController.proxyToClsiWithLimits
  363. .calledWith(this.submission_id, this.expected_url, {
  364. compileGroup: 'standard'
  365. })
  366. .should.equal(true)
  367. })
  368. })
  369. describe('with limits specified', function() {
  370. beforeEach(function() {
  371. this.req.body = { compileTimeout: 600, compileGroup: 'special' }
  372. return this.CompileController.getFileFromClsiWithoutUser(
  373. this.req,
  374. this.res,
  375. this.next
  376. )
  377. })
  378. it('should proxy to CLSI with correct URL and specified limits', function() {
  379. return this.CompileController.proxyToClsiWithLimits
  380. .calledWith(this.submission_id, this.expected_url, {
  381. compileGroup: 'special'
  382. })
  383. .should.equal(true)
  384. })
  385. })
  386. })
  387. describe('proxyToClsi', function() {
  388. beforeEach(function() {
  389. this.request.returns(
  390. (this.proxy = {
  391. pipe: sinon.stub(),
  392. on: sinon.stub()
  393. })
  394. )
  395. this.upstream = {
  396. statusCode: 204,
  397. headers: { mock: 'header' }
  398. }
  399. this.req.method = 'mock-method'
  400. return (this.req.headers = {
  401. Mock: 'Headers',
  402. Range: '123-456',
  403. 'If-Range': 'abcdef',
  404. 'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT'
  405. })
  406. })
  407. describe('old pdf viewer', function() {
  408. describe('user with standard priority', function() {
  409. beforeEach(function() {
  410. this.CompileManager.getProjectCompileLimits = sinon
  411. .stub()
  412. .callsArgWith(1, null, { compileGroup: 'standard' })
  413. return this.CompileController.proxyToClsi(
  414. this.project_id,
  415. (this.url = '/test'),
  416. this.req,
  417. this.res,
  418. this.next
  419. )
  420. })
  421. it('should open a request to the CLSI', function() {
  422. return this.request
  423. .calledWith({
  424. jar: this.jar,
  425. method: this.req.method,
  426. url: `${this.settings.apis.clsi.url}${this.url}`,
  427. timeout: 60 * 1000
  428. })
  429. .should.equal(true)
  430. })
  431. it('should pass the request on to the client', function() {
  432. return this.proxy.pipe.calledWith(this.res).should.equal(true)
  433. })
  434. it('should bind an error handle to the request proxy', function() {
  435. return this.proxy.on.calledWith('error').should.equal(true)
  436. })
  437. })
  438. describe('user with priority compile', function() {
  439. beforeEach(function() {
  440. this.CompileManager.getProjectCompileLimits = sinon
  441. .stub()
  442. .callsArgWith(1, null, { compileGroup: 'priority' })
  443. return this.CompileController.proxyToClsi(
  444. this.project_id,
  445. (this.url = '/test'),
  446. this.req,
  447. this.res,
  448. this.next
  449. )
  450. })
  451. })
  452. describe('user with standard priority via query string', function() {
  453. beforeEach(function() {
  454. this.req.query = { compileGroup: 'standard' }
  455. return this.CompileController.proxyToClsi(
  456. this.project_id,
  457. (this.url = '/test'),
  458. this.req,
  459. this.res,
  460. this.next
  461. )
  462. })
  463. it('should open a request to the CLSI', function() {
  464. return this.request
  465. .calledWith({
  466. jar: this.jar,
  467. method: this.req.method,
  468. url: `${this.settings.apis.clsi.url}${this.url}`,
  469. timeout: 60 * 1000
  470. })
  471. .should.equal(true)
  472. })
  473. it('should pass the request on to the client', function() {
  474. return this.proxy.pipe.calledWith(this.res).should.equal(true)
  475. })
  476. it('should bind an error handle to the request proxy', function() {
  477. return this.proxy.on.calledWith('error').should.equal(true)
  478. })
  479. })
  480. describe('user with non-existent priority via query string', function() {
  481. beforeEach(function() {
  482. this.req.query = { compileGroup: 'foobar' }
  483. return this.CompileController.proxyToClsi(
  484. this.project_id,
  485. (this.url = '/test'),
  486. this.req,
  487. this.res,
  488. this.next
  489. )
  490. })
  491. it('should proxy to the standard url', function() {
  492. return this.request
  493. .calledWith({
  494. jar: this.jar,
  495. method: this.req.method,
  496. url: `${this.settings.apis.clsi.url}${this.url}`,
  497. timeout: 60 * 1000
  498. })
  499. .should.equal(true)
  500. })
  501. })
  502. describe('user with build parameter via query string', function() {
  503. beforeEach(function() {
  504. this.CompileManager.getProjectCompileLimits = sinon
  505. .stub()
  506. .callsArgWith(1, null, { compileGroup: 'standard' })
  507. this.req.query = { build: 1234 }
  508. return this.CompileController.proxyToClsi(
  509. this.project_id,
  510. (this.url = '/test'),
  511. this.req,
  512. this.res,
  513. this.next
  514. )
  515. })
  516. it('should proxy to the standard url without the build parameter', function() {
  517. return this.request
  518. .calledWith({
  519. jar: this.jar,
  520. method: this.req.method,
  521. url: `${this.settings.apis.clsi.url}${this.url}`,
  522. timeout: 60 * 1000
  523. })
  524. .should.equal(true)
  525. })
  526. })
  527. })
  528. describe('new pdf viewer', function() {
  529. beforeEach(function() {
  530. return (this.req.query = { pdfng: true })
  531. })
  532. describe('user with standard priority', function() {
  533. beforeEach(function() {
  534. this.CompileManager.getProjectCompileLimits = sinon
  535. .stub()
  536. .callsArgWith(1, null, { compileGroup: 'standard' })
  537. return this.CompileController.proxyToClsi(
  538. this.project_id,
  539. (this.url = '/test'),
  540. this.req,
  541. this.res,
  542. this.next
  543. )
  544. })
  545. it('should open a request to the CLSI', function() {
  546. return this.request
  547. .calledWith({
  548. jar: this.jar,
  549. method: this.req.method,
  550. url: `${this.settings.apis.clsi.url}${this.url}`,
  551. timeout: 60 * 1000,
  552. headers: {
  553. Range: '123-456',
  554. 'If-Range': 'abcdef',
  555. 'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT'
  556. }
  557. })
  558. .should.equal(true)
  559. })
  560. it('should pass the request on to the client', function() {
  561. return this.proxy.pipe.calledWith(this.res).should.equal(true)
  562. })
  563. it('should bind an error handle to the request proxy', function() {
  564. return this.proxy.on.calledWith('error').should.equal(true)
  565. })
  566. })
  567. describe('user with build parameter via query string', function() {
  568. beforeEach(function() {
  569. this.CompileManager.getProjectCompileLimits = sinon
  570. .stub()
  571. .callsArgWith(1, null, { compileGroup: 'standard' })
  572. this.req.query = { build: 1234, pdfng: true }
  573. return this.CompileController.proxyToClsi(
  574. this.project_id,
  575. (this.url = '/test'),
  576. this.req,
  577. this.res,
  578. this.next
  579. )
  580. })
  581. it('should proxy to the standard url with the build parameter', function() {
  582. return this.request
  583. .calledWith({
  584. jar: this.jar,
  585. method: this.req.method,
  586. qs: { build: 1234 },
  587. url: `${this.settings.apis.clsi.url}${this.url}`,
  588. timeout: 60 * 1000,
  589. headers: {
  590. Range: '123-456',
  591. 'If-Range': 'abcdef',
  592. 'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT'
  593. }
  594. })
  595. .should.equal(true)
  596. })
  597. })
  598. })
  599. })
  600. describe('deleteAuxFiles', function() {
  601. beforeEach(function() {
  602. this.CompileManager.deleteAuxFiles = sinon.stub().callsArg(2)
  603. this.req.params = { Project_id: this.project_id }
  604. this.res.sendStatus = sinon.stub()
  605. return this.CompileController.deleteAuxFiles(
  606. this.req,
  607. this.res,
  608. this.next
  609. )
  610. })
  611. it('should proxy to the CLSI', function() {
  612. return this.CompileManager.deleteAuxFiles
  613. .calledWith(this.project_id)
  614. .should.equal(true)
  615. })
  616. it('should return a 200', function() {
  617. return this.res.sendStatus.calledWith(200).should.equal(true)
  618. })
  619. })
  620. describe('compileAndDownloadPdf', function() {
  621. beforeEach(function() {
  622. this.req = {
  623. params: {
  624. project_id: this.project_id
  625. }
  626. }
  627. this.CompileManager.compile.callsArgWith(3)
  628. this.CompileController.proxyToClsi = sinon.stub()
  629. return (this.res = { send: () => {} })
  630. })
  631. it('should call compile in the compile manager', function(done) {
  632. this.CompileController.compileAndDownloadPdf(this.req, this.res)
  633. this.CompileManager.compile.calledWith(this.project_id).should.equal(true)
  634. return done()
  635. })
  636. it('should proxy the res to the clsi with correct url', function(done) {
  637. this.CompileController.compileAndDownloadPdf(this.req, this.res)
  638. sinon.assert.calledWith(
  639. this.CompileController.proxyToClsi,
  640. this.project_id,
  641. `/project/${this.project_id}/output/output.pdf`,
  642. this.req,
  643. this.res
  644. )
  645. this.CompileController.proxyToClsi
  646. .calledWith(
  647. this.project_id,
  648. `/project/${this.project_id}/output/output.pdf`,
  649. this.req,
  650. this.res
  651. )
  652. .should.equal(true)
  653. return done()
  654. })
  655. })
  656. describe('wordCount', function() {
  657. beforeEach(function() {
  658. this.CompileManager.wordCount = sinon
  659. .stub()
  660. .callsArgWith(3, null, { content: 'body' })
  661. this.req.params = { Project_id: this.project_id }
  662. this.res.send = sinon.stub()
  663. this.res.contentType = sinon.stub()
  664. return this.CompileController.wordCount(this.req, this.res, this.next)
  665. })
  666. it('should proxy to the CLSI', function() {
  667. return this.CompileManager.wordCount
  668. .calledWith(this.project_id, this.user_id, false)
  669. .should.equal(true)
  670. })
  671. it('should return a 200 and body', function() {
  672. return this.res.send.calledWith({ content: 'body' }).should.equal(true)
  673. })
  674. })
  675. })