CompileControllerTests.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972
  1. const sinon = require('sinon')
  2. const { expect } = require('chai')
  3. const modulePath = '../../../../app/src/Features/Compile/CompileController.js'
  4. const SandboxedModule = require('sandboxed-module')
  5. const MockRequest = require('../helpers/MockRequest')
  6. const MockResponse = require('../helpers/MockResponse')
  7. const { Headers } = require('node-fetch')
  8. const { ReadableString } = require('@overleaf/stream-utils')
  9. describe('CompileController', function () {
  10. beforeEach(function () {
  11. this.user_id = 'wat'
  12. this.user = {
  13. _id: this.user_id,
  14. email: 'user@example.com',
  15. features: {
  16. compileGroup: 'premium',
  17. compileTimeout: 100,
  18. },
  19. }
  20. this.CompileManager = {
  21. promises: {
  22. compile: sinon.stub(),
  23. getProjectCompileLimits: sinon.stub(),
  24. syncTeX: sinon.stub(),
  25. },
  26. }
  27. this.ClsiManager = {
  28. promises: {},
  29. }
  30. this.UserGetter = { getUser: sinon.stub() }
  31. this.rateLimiter = {
  32. consume: sinon.stub().resolves(),
  33. }
  34. this.RateLimiter = {
  35. RateLimiter: sinon.stub().returns(this.rateLimiter),
  36. }
  37. this.settings = {
  38. apis: {
  39. clsi: {
  40. url: 'http://clsi.example.com',
  41. submissionBackendClass: 'n2d',
  42. },
  43. clsi_priority: {
  44. url: 'http://clsi-priority.example.com',
  45. },
  46. },
  47. defaultFeatures: {
  48. compileGroup: 'standard',
  49. compileTimeout: 60,
  50. },
  51. clsiCookie: {
  52. key: 'cookie-key',
  53. },
  54. }
  55. this.ClsiCookieManager = {
  56. promises: {
  57. getServerId: sinon.stub().resolves('clsi-server-id-from-redis'),
  58. },
  59. }
  60. this.SessionManager = {
  61. getLoggedInUserId: sinon.stub().returns(this.user_id),
  62. getSessionUser: sinon.stub().returns(this.user),
  63. isUserLoggedIn: sinon.stub().returns(true),
  64. }
  65. this.pipeline = sinon.stub().callsFake(async (stream, res) => {
  66. if (res.callback) res.callback()
  67. })
  68. this.clsiStream = new ReadableString('{}')
  69. this.clsiResponse = {
  70. headers: new Headers({
  71. 'Content-Length': '2',
  72. 'Content-Type': 'application/json',
  73. }),
  74. }
  75. this.fetchUtils = {
  76. fetchStreamWithResponse: sinon.stub().resolves({
  77. stream: this.clsiStream,
  78. response: this.clsiResponse,
  79. }),
  80. }
  81. this.CompileController = SandboxedModule.require(modulePath, {
  82. requires: {
  83. 'stream/promises': { pipeline: this.pipeline },
  84. '@overleaf/settings': this.settings,
  85. '@overleaf/fetch-utils': this.fetchUtils,
  86. '../Project/ProjectGetter': (this.ProjectGetter = {
  87. promises: {},
  88. }),
  89. '@overleaf/metrics': (this.Metrics = {
  90. inc: sinon.stub(),
  91. Timer: class {
  92. constructor() {
  93. this.labels = {}
  94. }
  95. done() {}
  96. },
  97. }),
  98. // TODO: remove this once we remove Joi/Celebrate
  99. celebrate: (this.celebrate = {
  100. celebrate: sinon.stub(),
  101. errors: sinon.stub(),
  102. Joi: {
  103. any: sinon.stub(),
  104. extend: sinon.stub(),
  105. },
  106. }),
  107. './CompileManager': this.CompileManager,
  108. '../User/UserGetter': this.UserGetter,
  109. './ClsiManager': this.ClsiManager,
  110. '../Authentication/SessionManager': this.SessionManager,
  111. '../../infrastructure/RateLimiter': this.RateLimiter,
  112. './ClsiCookieManager': () => this.ClsiCookieManager,
  113. '../SplitTests/SplitTestHandler': {
  114. getAssignment: (this.getAssignment = sinon.stub().yields(null, {
  115. variant: 'default',
  116. })),
  117. promises: {
  118. getAssignment: sinon.stub().resolves({
  119. variant: 'default',
  120. }),
  121. },
  122. },
  123. '../Analytics/AnalyticsManager': {
  124. recordEventForSession: sinon.stub(),
  125. },
  126. },
  127. })
  128. this.projectId = 'abc123def456abc123def456'
  129. this.build_id = '18fbe9e7564-30dcb2f71250c690'
  130. this.next = sinon.stub()
  131. this.req = new MockRequest()
  132. this.res = new MockResponse()
  133. this.res = new MockResponse()
  134. })
  135. describe('compile', function () {
  136. beforeEach(function () {
  137. this.req.params = { Project_id: this.projectId }
  138. this.req.session = {}
  139. this.CompileManager.promises.compile = sinon.stub().resolves({
  140. status: (this.status = 'success'),
  141. outputFiles: (this.outputFiles = [
  142. {
  143. path: 'output.pdf',
  144. url: `/project/${this.projectId}/user/${this.user_id}/build/id/output.pdf`,
  145. type: 'pdf',
  146. },
  147. ]),
  148. clsiServerId: undefined,
  149. limits: undefined,
  150. validationProblems: undefined,
  151. stats: undefined,
  152. timings: undefined,
  153. outputUrlPrefix: undefined,
  154. buildId: this.build_id,
  155. })
  156. })
  157. describe('pdfDownloadDomain', function () {
  158. beforeEach(function () {
  159. this.settings.pdfDownloadDomain = 'https://compiles.overleaf.test'
  160. })
  161. describe('when clsi does not emit zone prefix', function () {
  162. beforeEach(async function () {
  163. await this.CompileController.compile(this.req, this.res, this.next)
  164. })
  165. it('should add domain verbatim', function () {
  166. this.res.statusCode.should.equal(200)
  167. this.res.body.should.equal(
  168. JSON.stringify({
  169. status: this.status,
  170. outputFiles: [
  171. {
  172. path: 'output.pdf',
  173. url: `/project/${this.projectId}/user/${this.user_id}/build/id/output.pdf`,
  174. type: 'pdf',
  175. },
  176. ],
  177. outputFilesArchive: {
  178. path: 'output.zip',
  179. url: `/project/${this.projectId}/user/wat/build/${this.build_id}/output/output.zip`,
  180. type: 'zip',
  181. },
  182. pdfDownloadDomain: 'https://compiles.overleaf.test',
  183. })
  184. )
  185. })
  186. })
  187. describe('when clsi emits a zone prefix', function () {
  188. beforeEach(async function () {
  189. this.CompileManager.promises.compile = sinon.stub().resolves({
  190. status: (this.status = 'success'),
  191. outputFiles: (this.outputFiles = [
  192. {
  193. path: 'output.pdf',
  194. url: `/project/${this.projectId}/user/${this.user_id}/build/id/output.pdf`,
  195. type: 'pdf',
  196. },
  197. ]),
  198. clsiServerId: undefined,
  199. limits: undefined,
  200. validationProblems: undefined,
  201. stats: undefined,
  202. timings: undefined,
  203. outputUrlPrefix: '/zone/b',
  204. buildId: this.build_id,
  205. })
  206. await this.CompileController.compile(this.req, this.res, this.next)
  207. })
  208. it('should add the zone prefix', function () {
  209. this.res.statusCode.should.equal(200)
  210. this.res.body.should.equal(
  211. JSON.stringify({
  212. status: this.status,
  213. outputFiles: [
  214. {
  215. path: 'output.pdf',
  216. url: `/project/${this.projectId}/user/${this.user_id}/build/id/output.pdf`,
  217. type: 'pdf',
  218. },
  219. ],
  220. outputFilesArchive: {
  221. path: 'output.zip',
  222. url: `/project/${this.projectId}/user/wat/build/${this.build_id}/output/output.zip`,
  223. type: 'zip',
  224. },
  225. outputUrlPrefix: '/zone/b',
  226. pdfDownloadDomain: 'https://compiles.overleaf.test/zone/b',
  227. })
  228. )
  229. })
  230. })
  231. })
  232. describe('when not an auto compile', function () {
  233. beforeEach(async function () {
  234. await this.CompileController.compile(this.req, this.res, this.next)
  235. })
  236. it('should look up the user id', function () {
  237. this.SessionManager.getLoggedInUserId
  238. .calledWith(this.req.session)
  239. .should.equal(true)
  240. })
  241. it('should do the compile without the auto compile flag', function () {
  242. this.CompileManager.promises.compile.should.have.been.calledWith(
  243. this.projectId,
  244. this.user_id,
  245. {
  246. isAutoCompile: false,
  247. compileFromClsiCache: false,
  248. populateClsiCache: false,
  249. enablePdfCaching: false,
  250. fileLineErrors: false,
  251. stopOnFirstError: false,
  252. editorId: undefined,
  253. }
  254. )
  255. })
  256. it('should set the content-type of the response to application/json', function () {
  257. this.res.type.should.equal('application/json')
  258. })
  259. it('should send a successful response reporting the status and files', function () {
  260. this.res.statusCode.should.equal(200)
  261. this.res.body.should.equal(
  262. JSON.stringify({
  263. status: this.status,
  264. outputFiles: this.outputFiles,
  265. outputFilesArchive: {
  266. path: 'output.zip',
  267. url: `/project/${this.projectId}/user/wat/build/${this.build_id}/output/output.zip`,
  268. type: 'zip',
  269. },
  270. })
  271. )
  272. })
  273. })
  274. describe('when an auto compile', function () {
  275. beforeEach(async function () {
  276. this.req.query = { auto_compile: 'true' }
  277. await this.CompileController.compile(this.req, this.res, this.next)
  278. })
  279. it('should do the compile with the auto compile flag', function () {
  280. this.CompileManager.promises.compile.should.have.been.calledWith(
  281. this.projectId,
  282. this.user_id,
  283. {
  284. isAutoCompile: true,
  285. compileFromClsiCache: false,
  286. populateClsiCache: false,
  287. enablePdfCaching: false,
  288. fileLineErrors: false,
  289. stopOnFirstError: false,
  290. editorId: undefined,
  291. }
  292. )
  293. })
  294. })
  295. describe('with the draft attribute', function () {
  296. beforeEach(async function () {
  297. this.req.body = { draft: true }
  298. await this.CompileController.compile(this.req, this.res, this.next)
  299. })
  300. it('should do the compile without the draft compile flag', function () {
  301. this.CompileManager.promises.compile.should.have.been.calledWith(
  302. this.projectId,
  303. this.user_id,
  304. {
  305. isAutoCompile: false,
  306. compileFromClsiCache: false,
  307. populateClsiCache: false,
  308. enablePdfCaching: false,
  309. draft: true,
  310. fileLineErrors: false,
  311. stopOnFirstError: false,
  312. editorId: undefined,
  313. }
  314. )
  315. })
  316. })
  317. describe('with an editor id', function () {
  318. beforeEach(async function () {
  319. this.req.body = { editorId: 'the-editor-id' }
  320. await this.CompileController.compile(this.req, this.res, this.next)
  321. })
  322. it('should pass the editor id to the compiler', function () {
  323. this.CompileManager.promises.compile.should.have.been.calledWith(
  324. this.projectId,
  325. this.user_id,
  326. {
  327. isAutoCompile: false,
  328. compileFromClsiCache: false,
  329. populateClsiCache: false,
  330. enablePdfCaching: false,
  331. fileLineErrors: false,
  332. stopOnFirstError: false,
  333. editorId: 'the-editor-id',
  334. }
  335. )
  336. })
  337. })
  338. })
  339. describe('compileSubmission', function () {
  340. beforeEach(function () {
  341. this.submission_id = 'sub-1234'
  342. this.req.params = { submission_id: this.submission_id }
  343. this.req.body = {}
  344. this.ClsiManager.promises.sendExternalRequest = sinon.stub().resolves({
  345. status: (this.status = 'success'),
  346. outputFiles: (this.outputFiles = ['mock-output-files']),
  347. clsiServerId: 'mock-server-id',
  348. validationProblems: null,
  349. })
  350. })
  351. it('should set the content-type of the response to application/json', async function () {
  352. await this.CompileController.compileSubmission(
  353. this.req,
  354. this.res,
  355. this.next
  356. )
  357. this.res.contentType.calledWith('application/json').should.equal(true)
  358. })
  359. it('should send a successful response reporting the status and files', async function () {
  360. await this.CompileController.compileSubmission(
  361. this.req,
  362. this.res,
  363. this.next
  364. )
  365. this.res.statusCode.should.equal(200)
  366. this.res.body.should.equal(
  367. JSON.stringify({
  368. status: this.status,
  369. outputFiles: this.outputFiles,
  370. clsiServerId: 'mock-server-id',
  371. validationProblems: null,
  372. })
  373. )
  374. })
  375. describe('with compileGroup and timeout', function () {
  376. beforeEach(function () {
  377. this.req.body = {
  378. compileGroup: 'special',
  379. timeout: 600,
  380. }
  381. this.CompileController.compileSubmission(this.req, this.res, this.next)
  382. })
  383. it('should use the supplied values', function () {
  384. this.ClsiManager.promises.sendExternalRequest.should.have.been.calledWith(
  385. this.submission_id,
  386. { compileGroup: 'special', timeout: 600 },
  387. { compileGroup: 'special', compileBackendClass: 'n2d', timeout: 600 }
  388. )
  389. })
  390. })
  391. describe('with other supported options but not compileGroup and timeout', function () {
  392. beforeEach(function () {
  393. this.req.body = {
  394. rootResourcePath: 'main.tex',
  395. compiler: 'lualatex',
  396. draft: true,
  397. check: 'validate',
  398. }
  399. this.CompileController.compileSubmission(this.req, this.res, this.next)
  400. })
  401. it('should use the other options but default values for compileGroup and timeout', function () {
  402. this.ClsiManager.promises.sendExternalRequest.should.have.been.calledWith(
  403. this.submission_id,
  404. {
  405. rootResourcePath: 'main.tex',
  406. compiler: 'lualatex',
  407. draft: true,
  408. check: 'validate',
  409. },
  410. {
  411. rootResourcePath: 'main.tex',
  412. compiler: 'lualatex',
  413. draft: true,
  414. check: 'validate',
  415. compileGroup: 'standard',
  416. compileBackendClass: 'n2d',
  417. timeout: 60,
  418. }
  419. )
  420. })
  421. })
  422. })
  423. describe('downloadPdf', function () {
  424. beforeEach(function () {
  425. this.CompileController._proxyToClsi = sinon.stub().resolves()
  426. this.req.params = { Project_id: this.projectId }
  427. this.project = { name: 'test namè; 1' }
  428. this.ProjectGetter.promises.getProject = sinon
  429. .stub()
  430. .resolves(this.project)
  431. })
  432. describe('when downloading for embedding', function () {
  433. beforeEach(async function () {
  434. await this.CompileController.downloadPdf(this.req, this.res, this.next)
  435. })
  436. it('should look up the project', function () {
  437. this.ProjectGetter.promises.getProject
  438. .calledWith(this.projectId, { name: 1 })
  439. .should.equal(true)
  440. })
  441. it('should set the content-type of the response to application/pdf', function () {
  442. this.res.contentType.calledWith('application/pdf').should.equal(true)
  443. })
  444. it('should set the content-disposition header with a safe version of the project name', function () {
  445. this.res.setContentDisposition.should.be.calledWith('inline', {
  446. filename: 'test_namè__1.pdf',
  447. })
  448. })
  449. it('should increment the pdf-downloads metric', function () {
  450. this.Metrics.inc.calledWith('pdf-downloads').should.equal(true)
  451. })
  452. it('should proxy the PDF from the CLSI', function () {
  453. this.CompileController._proxyToClsi
  454. .calledWith(
  455. this.projectId,
  456. 'output-file',
  457. `/project/${this.projectId}/user/${this.user_id}/output/output.pdf`,
  458. {},
  459. this.req,
  460. this.res
  461. )
  462. .should.equal(true)
  463. })
  464. })
  465. describe('when a build-id is provided', function () {
  466. beforeEach(async function () {
  467. this.req.params.build_id = this.build_id
  468. await this.CompileController.downloadPdf(this.req, this.res, this.next)
  469. })
  470. it('should proxy the PDF from the CLSI, with a build-id', function () {
  471. this.CompileController._proxyToClsi
  472. .calledWith(
  473. this.projectId,
  474. 'output-file',
  475. `/project/${this.projectId}/user/${this.user_id}/build/${this.build_id}/output/output.pdf`,
  476. {},
  477. this.req,
  478. this.res
  479. )
  480. .should.equal(true)
  481. })
  482. })
  483. describe('when rate-limited', function () {
  484. beforeEach(async function () {
  485. this.rateLimiter.consume.rejects({
  486. msBeforeNext: 250,
  487. remainingPoints: 0,
  488. consumedPoints: 5,
  489. isFirstInDuration: false,
  490. })
  491. })
  492. it('should return 500', async function () {
  493. await this.CompileController.downloadPdf(this.req, this.res, this.next)
  494. // should it be 429 instead?
  495. this.res.sendStatus.calledWith(500).should.equal(true)
  496. this.CompileController._proxyToClsi.should.not.have.been.called
  497. })
  498. })
  499. describe('when rate-limit errors', function () {
  500. beforeEach(async function () {
  501. this.rateLimiter.consume.rejects(new Error('uh oh'))
  502. })
  503. it('should return 500', async function () {
  504. await this.CompileController.downloadPdf(this.req, this.res, this.next)
  505. this.res.sendStatus.calledWith(500).should.equal(true)
  506. this.CompileController._proxyToClsi.should.not.have.been.called
  507. })
  508. })
  509. })
  510. describe('getFileFromClsiWithoutUser', function () {
  511. beforeEach(function () {
  512. this.submission_id = 'sub-1234'
  513. this.file = 'output.pdf'
  514. this.req.params = {
  515. submission_id: this.submission_id,
  516. build_id: this.build_id,
  517. file: this.file,
  518. }
  519. this.req.body = {}
  520. this.expected_url = `/project/${this.submission_id}/build/${this.build_id}/output/${this.file}`
  521. this.CompileController._proxyToClsiWithLimits = sinon.stub()
  522. })
  523. describe('without limits specified', function () {
  524. beforeEach(async function () {
  525. await this.CompileController.getFileFromClsiWithoutUser(
  526. this.req,
  527. this.res,
  528. this.next
  529. )
  530. })
  531. it('should proxy to CLSI with correct URL and default limits', function () {
  532. this.CompileController._proxyToClsiWithLimits.should.have.been.calledWith(
  533. this.submission_id,
  534. 'output-file',
  535. this.expected_url,
  536. {},
  537. { compileGroup: 'standard', compileBackendClass: 'n2d' }
  538. )
  539. })
  540. })
  541. describe('with limits specified', function () {
  542. beforeEach(function () {
  543. this.req.body = { compileTimeout: 600, compileGroup: 'special' }
  544. this.CompileController.getFileFromClsiWithoutUser(
  545. this.req,
  546. this.res,
  547. this.next
  548. )
  549. })
  550. it('should proxy to CLSI with correct URL and specified limits', function () {
  551. this.CompileController._proxyToClsiWithLimits.should.have.been.calledWith(
  552. this.submission_id,
  553. 'output-file',
  554. this.expected_url,
  555. {},
  556. {
  557. compileGroup: 'special',
  558. compileBackendClass: 'n2d',
  559. }
  560. )
  561. })
  562. })
  563. })
  564. describe('proxySyncCode', function () {
  565. let file, line, column, imageName, editorId, buildId, clsiServerId
  566. beforeEach(async function () {
  567. this.req.params = { Project_id: this.projectId }
  568. clsiServerId = 'clsi-1'
  569. file = 'main.tex'
  570. line = String(Date.now())
  571. column = String(Date.now() + 1)
  572. editorId = '172977cb-361e-4854-a4dc-a71cf11512e5'
  573. buildId = '195b4a3f9e7-03e5be430a9e7796'
  574. this.req.query = {
  575. file,
  576. line,
  577. column,
  578. editorId,
  579. buildId,
  580. clsiserverid: clsiServerId,
  581. }
  582. imageName = 'foo/bar:tag-0'
  583. this.ProjectGetter.promises.getProject = sinon
  584. .stub()
  585. .resolves({ imageName })
  586. this.CompileController._proxyToClsi = sinon.stub().resolves()
  587. await this.CompileController.proxySyncCode(this.req, this.res, this.next)
  588. })
  589. it('should parse the parameters', function () {
  590. expect(this.CompileManager.promises.syncTeX).to.have.been.calledWith(
  591. this.projectId,
  592. this.user_id,
  593. {
  594. direction: 'code',
  595. compileFromClsiCache: false,
  596. validatedOptions: {
  597. file,
  598. line,
  599. column,
  600. editorId,
  601. buildId,
  602. },
  603. clsiServerId,
  604. }
  605. )
  606. })
  607. })
  608. describe('proxySyncPdf', function () {
  609. let page, h, v, imageName, editorId, buildId, clsiServerId
  610. beforeEach(async function () {
  611. this.req.params = { Project_id: this.projectId }
  612. clsiServerId = 'clsi-1'
  613. page = String(Date.now())
  614. h = String(Math.random())
  615. v = String(Math.random())
  616. editorId = '172977cb-361e-4854-a4dc-a71cf11512e5'
  617. buildId = '195b4a3f9e7-03e5be430a9e7796'
  618. this.req.query = {
  619. page,
  620. h,
  621. v,
  622. editorId,
  623. buildId,
  624. clsiserverid: clsiServerId,
  625. }
  626. imageName = 'foo/bar:tag-1'
  627. this.ProjectGetter.promises.getProject = sinon
  628. .stub()
  629. .resolves({ imageName })
  630. this.CompileController._proxyToClsi = sinon.stub()
  631. await this.CompileController.proxySyncPdf(this.req, this.res, this.next)
  632. })
  633. it('should parse the parameters', function () {
  634. expect(this.CompileManager.promises.syncTeX).to.have.been.calledWith(
  635. this.projectId,
  636. this.user_id,
  637. {
  638. direction: 'pdf',
  639. compileFromClsiCache: false,
  640. validatedOptions: {
  641. page,
  642. h,
  643. v,
  644. editorId,
  645. buildId,
  646. },
  647. clsiServerId,
  648. }
  649. )
  650. })
  651. })
  652. describe('_proxyToClsi', function () {
  653. beforeEach(function () {
  654. this.req.method = 'mock-method'
  655. this.req.headers = {
  656. Mock: 'Headers',
  657. Range: '123-456',
  658. 'If-Range': 'abcdef',
  659. 'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT',
  660. }
  661. })
  662. describe('old pdf viewer', function () {
  663. describe('user with standard priority', function () {
  664. beforeEach(async function () {
  665. this.CompileManager.promises.getProjectCompileLimits = sinon
  666. .stub()
  667. .resolves({
  668. compileGroup: 'standard',
  669. compileBackendClass: 'n2d',
  670. })
  671. await this.CompileController._proxyToClsi(
  672. this.projectId,
  673. 'output-file',
  674. (this.url = '/test'),
  675. { query: 'foo' },
  676. this.req,
  677. this.res,
  678. this.next
  679. )
  680. })
  681. it('should open a request to the CLSI', function () {
  682. this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
  683. `${this.settings.apis.clsi.url}${this.url}?compileGroup=standard&compileBackendClass=n2d&query=foo`
  684. )
  685. })
  686. it('should pass the request on to the client', function () {
  687. this.pipeline.should.have.been.calledWith(this.clsiStream, this.res)
  688. })
  689. })
  690. describe('user with priority compile', function () {
  691. beforeEach(async function () {
  692. this.CompileManager.promises.getProjectCompileLimits = sinon
  693. .stub()
  694. .resolves({
  695. compileGroup: 'priority',
  696. compileBackendClass: 'c2d',
  697. })
  698. await this.CompileController._proxyToClsi(
  699. this.projectId,
  700. 'output-file',
  701. (this.url = '/test'),
  702. {},
  703. this.req,
  704. this.res,
  705. this.next
  706. )
  707. })
  708. it('should open a request to the CLSI', function () {
  709. this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
  710. `${this.settings.apis.clsi.url}${this.url}?compileGroup=priority&compileBackendClass=c2d`
  711. )
  712. })
  713. })
  714. describe('user with standard priority via query string', function () {
  715. beforeEach(async function () {
  716. this.req.query = { compileGroup: 'standard' }
  717. this.CompileManager.promises.getProjectCompileLimits = sinon
  718. .stub()
  719. .resolves({
  720. compileGroup: 'standard',
  721. compileBackendClass: 'n2d',
  722. })
  723. await this.CompileController._proxyToClsi(
  724. this.projectId,
  725. 'output-file',
  726. (this.url = '/test'),
  727. {},
  728. this.req,
  729. this.res,
  730. this.next
  731. )
  732. })
  733. it('should open a request to the CLSI', function () {
  734. this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
  735. `${this.settings.apis.clsi.url}${this.url}?compileGroup=standard&compileBackendClass=n2d`
  736. )
  737. })
  738. it('should pass the request on to the client', function () {
  739. this.pipeline.should.have.been.calledWith(this.clsiStream, this.res)
  740. })
  741. })
  742. describe('user with non-existent priority via query string', function () {
  743. beforeEach(async function () {
  744. this.req.query = { compileGroup: 'foobar' }
  745. this.CompileManager.promises.getProjectCompileLimits = sinon
  746. .stub()
  747. .resolves({
  748. compileGroup: 'standard',
  749. compileBackendClass: 'n2d',
  750. })
  751. await this.CompileController._proxyToClsi(
  752. this.projectId,
  753. 'output-file',
  754. (this.url = '/test'),
  755. {},
  756. this.req,
  757. this.res,
  758. this.next
  759. )
  760. })
  761. it('should proxy to the standard url', function () {
  762. this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
  763. `${this.settings.apis.clsi.url}${this.url}?compileGroup=standard&compileBackendClass=n2d`
  764. )
  765. })
  766. })
  767. describe('user with build parameter via query string', function () {
  768. beforeEach(async function () {
  769. this.CompileManager.promises.getProjectCompileLimits = sinon
  770. .stub()
  771. .resolves({
  772. compileGroup: 'standard',
  773. compileBackendClass: 'n2d',
  774. })
  775. this.req.query = { build: 1234 }
  776. await this.CompileController._proxyToClsi(
  777. this.projectId,
  778. 'output-file',
  779. (this.url = '/test'),
  780. {},
  781. this.req,
  782. this.res,
  783. this.next
  784. )
  785. })
  786. it('should proxy to the standard url without the build parameter', function () {
  787. this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
  788. `${this.settings.apis.clsi.url}${this.url}?compileGroup=standard&compileBackendClass=n2d`
  789. )
  790. })
  791. })
  792. })
  793. })
  794. describe('deleteAuxFiles', function () {
  795. beforeEach(async function () {
  796. this.CompileManager.promises.deleteAuxFiles = sinon.stub().resolves()
  797. this.req.params = { Project_id: this.projectId }
  798. this.req.query = { clsiserverid: 'node-1' }
  799. this.res.sendStatus = sinon.stub()
  800. await this.CompileController.deleteAuxFiles(this.req, this.res, this.next)
  801. })
  802. it('should proxy to the CLSI', function () {
  803. this.CompileManager.promises.deleteAuxFiles
  804. .calledWith(this.projectId, this.user_id, 'node-1')
  805. .should.equal(true)
  806. })
  807. it('should return a 200', function () {
  808. this.res.sendStatus.calledWith(200).should.equal(true)
  809. })
  810. })
  811. describe('compileAndDownloadPdf', function () {
  812. beforeEach(function () {
  813. this.req = {
  814. params: {
  815. project_id: this.projectId,
  816. },
  817. }
  818. this.downloadPath = `/project/${this.projectId}/build/123/output/output.pdf`
  819. this.CompileManager.promises.compile.resolves({
  820. status: 'success',
  821. outputFiles: [{ path: 'output.pdf', url: this.downloadPath }],
  822. })
  823. this.CompileController._proxyToClsi = sinon.stub()
  824. this.res = { send: () => {}, sendStatus: sinon.stub() }
  825. })
  826. it('should call compile in the compile manager', async function () {
  827. await this.CompileController.compileAndDownloadPdf(this.req, this.res)
  828. this.CompileManager.promises.compile
  829. .calledWith(this.projectId)
  830. .should.equal(true)
  831. })
  832. it('should proxy the res to the clsi with correct url', async function () {
  833. await this.CompileController.compileAndDownloadPdf(this.req, this.res)
  834. sinon.assert.calledWith(
  835. this.CompileController._proxyToClsi,
  836. this.projectId,
  837. 'output-file',
  838. this.downloadPath,
  839. {},
  840. this.req,
  841. this.res
  842. )
  843. this.CompileController._proxyToClsi
  844. .calledWith(
  845. this.projectId,
  846. 'output-file',
  847. this.downloadPath,
  848. {},
  849. this.req,
  850. this.res
  851. )
  852. .should.equal(true)
  853. })
  854. it('should not download anything on compilation failures', async function () {
  855. this.CompileManager.promises.compile.rejects(new Error('failed'))
  856. await this.CompileController.compileAndDownloadPdf(
  857. this.req,
  858. this.res,
  859. this.next
  860. )
  861. this.res.sendStatus.should.have.been.calledWith(500)
  862. this.CompileController._proxyToClsi.should.not.have.been.called
  863. })
  864. it('should not download anything on missing pdf', async function () {
  865. this.CompileManager.promises.compile.resolves({
  866. status: 'success',
  867. outputFiles: [],
  868. })
  869. await this.CompileController.compileAndDownloadPdf(this.req, this.res)
  870. this.res.sendStatus.should.have.been.calledWith(500)
  871. this.CompileController._proxyToClsi.should.not.have.been.called
  872. })
  873. })
  874. describe('wordCount', function () {
  875. beforeEach(async function () {
  876. this.CompileManager.promises.wordCount = sinon
  877. .stub()
  878. .resolves({ content: 'body' })
  879. this.req.params = { Project_id: this.projectId }
  880. this.req.query = { clsiserverid: 'node-42' }
  881. this.res.json = sinon.stub()
  882. this.res.contentType = sinon.stub()
  883. await this.CompileController.wordCount(this.req, this.res, this.next)
  884. })
  885. it('should proxy to the CLSI', function () {
  886. this.CompileManager.promises.wordCount
  887. .calledWith(this.projectId, this.user_id, false, 'node-42')
  888. .should.equal(true)
  889. })
  890. it('should return a 200 and body', function () {
  891. this.res.json.calledWith({ content: 'body' }).should.equal(true)
  892. })
  893. })
  894. })