CompileControllerTests.js 30 KB

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