CompileControllerTests.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950
  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. clsiCacheSharded: false,
  244. }
  245. )
  246. })
  247. it('should set the content-type of the response to application/json', function () {
  248. this.res.type.should.equal('application/json')
  249. })
  250. it('should send a successful response reporting the status and files', function () {
  251. this.res.statusCode.should.equal(200)
  252. this.res.body.should.equal(
  253. JSON.stringify({
  254. status: this.status,
  255. outputFiles: this.outputFiles,
  256. outputFilesArchive: {
  257. path: 'output.zip',
  258. url: `/project/${this.projectId}/user/wat/build/${this.build_id}/output/output.zip`,
  259. type: 'zip',
  260. },
  261. })
  262. )
  263. })
  264. })
  265. describe('when an auto compile', function () {
  266. beforeEach(async function () {
  267. this.req.query = { auto_compile: 'true' }
  268. await this.CompileController.compile(this.req, this.res, this.next)
  269. })
  270. it('should do the compile with the auto compile flag', function () {
  271. this.CompileManager.promises.compile.should.have.been.calledWith(
  272. this.projectId,
  273. this.user_id,
  274. {
  275. isAutoCompile: true,
  276. compileFromClsiCache: false,
  277. populateClsiCache: false,
  278. enablePdfCaching: false,
  279. fileLineErrors: false,
  280. stopOnFirstError: false,
  281. editorId: undefined,
  282. clsiCacheSharded: false,
  283. }
  284. )
  285. })
  286. })
  287. describe('with the draft attribute', function () {
  288. beforeEach(async function () {
  289. this.req.body = { draft: true }
  290. await this.CompileController.compile(this.req, this.res, this.next)
  291. })
  292. it('should do the compile without the draft compile flag', function () {
  293. this.CompileManager.promises.compile.should.have.been.calledWith(
  294. this.projectId,
  295. this.user_id,
  296. {
  297. isAutoCompile: false,
  298. compileFromClsiCache: false,
  299. populateClsiCache: false,
  300. enablePdfCaching: false,
  301. draft: true,
  302. fileLineErrors: false,
  303. stopOnFirstError: false,
  304. editorId: undefined,
  305. clsiCacheSharded: false,
  306. }
  307. )
  308. })
  309. })
  310. describe('with an editor id', function () {
  311. beforeEach(async function () {
  312. this.req.body = { editorId: 'the-editor-id' }
  313. await this.CompileController.compile(this.req, this.res, this.next)
  314. })
  315. it('should pass the editor id to the compiler', function () {
  316. this.CompileManager.promises.compile.should.have.been.calledWith(
  317. this.projectId,
  318. this.user_id,
  319. {
  320. isAutoCompile: false,
  321. compileFromClsiCache: false,
  322. populateClsiCache: false,
  323. enablePdfCaching: false,
  324. fileLineErrors: false,
  325. stopOnFirstError: false,
  326. editorId: 'the-editor-id',
  327. clsiCacheSharded: false,
  328. }
  329. )
  330. })
  331. })
  332. })
  333. describe('compileSubmission', function () {
  334. beforeEach(function () {
  335. this.submission_id = 'sub-1234'
  336. this.req.params = { submission_id: this.submission_id }
  337. this.req.body = {}
  338. this.ClsiManager.promises.sendExternalRequest = sinon.stub().resolves({
  339. status: (this.status = 'success'),
  340. outputFiles: (this.outputFiles = ['mock-output-files']),
  341. clsiServerId: 'mock-server-id',
  342. validationProblems: null,
  343. })
  344. })
  345. it('should set the content-type of the response to application/json', async function () {
  346. await this.CompileController.compileSubmission(
  347. this.req,
  348. this.res,
  349. this.next
  350. )
  351. this.res.contentType.calledWith('application/json').should.equal(true)
  352. })
  353. it('should send a successful response reporting the status and files', async function () {
  354. await this.CompileController.compileSubmission(
  355. this.req,
  356. this.res,
  357. this.next
  358. )
  359. this.res.statusCode.should.equal(200)
  360. this.res.body.should.equal(
  361. JSON.stringify({
  362. status: this.status,
  363. outputFiles: this.outputFiles,
  364. clsiServerId: 'mock-server-id',
  365. validationProblems: null,
  366. })
  367. )
  368. })
  369. describe('with compileGroup and timeout', function () {
  370. beforeEach(function () {
  371. this.req.body = {
  372. compileGroup: 'special',
  373. timeout: 600,
  374. }
  375. this.CompileController.compileSubmission(this.req, this.res, this.next)
  376. })
  377. it('should use the supplied values', function () {
  378. this.ClsiManager.promises.sendExternalRequest.should.have.been.calledWith(
  379. this.submission_id,
  380. { compileGroup: 'special', timeout: 600 },
  381. { compileGroup: 'special', compileBackendClass: 'n2d', timeout: 600 }
  382. )
  383. })
  384. })
  385. describe('with other supported options but not compileGroup and timeout', function () {
  386. beforeEach(function () {
  387. this.req.body = {
  388. rootResourcePath: 'main.tex',
  389. compiler: 'lualatex',
  390. draft: true,
  391. check: 'validate',
  392. }
  393. this.CompileController.compileSubmission(this.req, this.res, this.next)
  394. })
  395. it('should use the other options but default values for compileGroup and timeout', function () {
  396. this.ClsiManager.promises.sendExternalRequest.should.have.been.calledWith(
  397. this.submission_id,
  398. {
  399. rootResourcePath: 'main.tex',
  400. compiler: 'lualatex',
  401. draft: true,
  402. check: 'validate',
  403. },
  404. {
  405. rootResourcePath: 'main.tex',
  406. compiler: 'lualatex',
  407. draft: true,
  408. check: 'validate',
  409. compileGroup: 'standard',
  410. compileBackendClass: 'n2d',
  411. timeout: 60,
  412. }
  413. )
  414. })
  415. })
  416. })
  417. describe('downloadPdf', function () {
  418. beforeEach(function () {
  419. this.CompileController._proxyToClsi = sinon.stub().resolves()
  420. this.req.params = { Project_id: this.projectId }
  421. this.project = { name: 'test namè; 1' }
  422. this.ProjectGetter.promises.getProject = sinon
  423. .stub()
  424. .resolves(this.project)
  425. })
  426. describe('when downloading for embedding', function () {
  427. beforeEach(async function () {
  428. await this.CompileController.downloadPdf(this.req, this.res, this.next)
  429. })
  430. it('should look up the project', function () {
  431. this.ProjectGetter.promises.getProject
  432. .calledWith(this.projectId, { name: 1 })
  433. .should.equal(true)
  434. })
  435. it('should set the content-type of the response to application/pdf', function () {
  436. this.res.contentType.calledWith('application/pdf').should.equal(true)
  437. })
  438. it('should set the content-disposition header with a safe version of the project name', function () {
  439. this.res.setContentDisposition.should.be.calledWith('inline', {
  440. filename: 'test_namè__1.pdf',
  441. })
  442. })
  443. it('should increment the pdf-downloads metric', function () {
  444. this.Metrics.inc.calledWith('pdf-downloads').should.equal(true)
  445. })
  446. it('should proxy the PDF from the CLSI', function () {
  447. this.CompileController._proxyToClsi
  448. .calledWith(
  449. this.projectId,
  450. 'output-file',
  451. `/project/${this.projectId}/user/${this.user_id}/output/output.pdf`,
  452. {},
  453. this.req,
  454. this.res
  455. )
  456. .should.equal(true)
  457. })
  458. })
  459. describe('when a build-id is provided', function () {
  460. beforeEach(async function () {
  461. this.req.params.build_id = this.build_id
  462. await this.CompileController.downloadPdf(this.req, this.res, this.next)
  463. })
  464. it('should proxy the PDF from the CLSI, with a build-id', function () {
  465. this.CompileController._proxyToClsi
  466. .calledWith(
  467. this.projectId,
  468. 'output-file',
  469. `/project/${this.projectId}/user/${this.user_id}/build/${this.build_id}/output/output.pdf`,
  470. {},
  471. this.req,
  472. this.res
  473. )
  474. .should.equal(true)
  475. })
  476. })
  477. describe('when rate-limited', function () {
  478. beforeEach(async function () {
  479. this.rateLimiter.consume.rejects({
  480. msBeforeNext: 250,
  481. remainingPoints: 0,
  482. consumedPoints: 5,
  483. isFirstInDuration: false,
  484. })
  485. })
  486. it('should return 500', async function () {
  487. await this.CompileController.downloadPdf(this.req, this.res, this.next)
  488. // should it be 429 instead?
  489. this.res.sendStatus.calledWith(500).should.equal(true)
  490. this.CompileController._proxyToClsi.should.not.have.been.called
  491. })
  492. })
  493. describe('when rate-limit errors', function () {
  494. beforeEach(async function () {
  495. this.rateLimiter.consume.rejects(new Error('uh oh'))
  496. })
  497. it('should return 500', async function () {
  498. await this.CompileController.downloadPdf(this.req, this.res, this.next)
  499. this.res.sendStatus.calledWith(500).should.equal(true)
  500. this.CompileController._proxyToClsi.should.not.have.been.called
  501. })
  502. })
  503. })
  504. describe('getFileFromClsiWithoutUser', function () {
  505. beforeEach(function () {
  506. this.submission_id = 'sub-1234'
  507. this.file = 'project.pdf'
  508. this.req.params = {
  509. submission_id: this.submission_id,
  510. build_id: this.build_id,
  511. file: this.file,
  512. }
  513. this.req.body = {}
  514. this.expected_url = `/project/${this.submission_id}/build/${this.build_id}/output/${this.file}`
  515. this.CompileController._proxyToClsiWithLimits = sinon.stub()
  516. })
  517. describe('without limits specified', function () {
  518. beforeEach(async function () {
  519. await this.CompileController.getFileFromClsiWithoutUser(
  520. this.req,
  521. this.res,
  522. this.next
  523. )
  524. })
  525. it('should proxy to CLSI with correct URL and default limits', function () {
  526. this.CompileController._proxyToClsiWithLimits.should.have.been.calledWith(
  527. this.submission_id,
  528. 'output-file',
  529. this.expected_url,
  530. {},
  531. { compileGroup: 'standard', compileBackendClass: 'n2d' }
  532. )
  533. })
  534. })
  535. describe('with limits specified', function () {
  536. beforeEach(function () {
  537. this.req.body = { compileTimeout: 600, compileGroup: 'special' }
  538. this.CompileController.getFileFromClsiWithoutUser(
  539. this.req,
  540. this.res,
  541. this.next
  542. )
  543. })
  544. it('should proxy to CLSI with correct URL and specified limits', function () {
  545. this.CompileController._proxyToClsiWithLimits.should.have.been.calledWith(
  546. this.submission_id,
  547. 'output-file',
  548. this.expected_url,
  549. {},
  550. {
  551. compileGroup: 'special',
  552. compileBackendClass: 'n2d',
  553. }
  554. )
  555. })
  556. })
  557. })
  558. describe('proxySyncCode', function () {
  559. let file, line, column, imageName, editorId, buildId
  560. beforeEach(async function () {
  561. this.req.params = { Project_id: this.projectId }
  562. file = 'main.tex'
  563. line = String(Date.now())
  564. column = String(Date.now() + 1)
  565. editorId = '172977cb-361e-4854-a4dc-a71cf11512e5'
  566. buildId = '195b4a3f9e7-03e5be430a9e7796'
  567. this.req.query = { file, line, column, editorId, buildId }
  568. imageName = 'foo/bar:tag-0'
  569. this.ProjectGetter.promises.getProject = sinon
  570. .stub()
  571. .resolves({ imageName })
  572. this.CompileController._proxyToClsi = sinon.stub().resolves()
  573. await this.CompileController.proxySyncCode(this.req, this.res, this.next)
  574. })
  575. it('should proxy the request with an imageName', function () {
  576. expect(this.CompileController._proxyToClsi).to.have.been.calledWith(
  577. this.projectId,
  578. 'sync-to-code',
  579. `/project/${this.projectId}/user/${this.user_id}/sync/code`,
  580. {
  581. file,
  582. line,
  583. column,
  584. imageName,
  585. editorId,
  586. buildId,
  587. compileFromClsiCache: false,
  588. },
  589. this.req,
  590. this.res
  591. )
  592. })
  593. })
  594. describe('proxySyncPdf', function () {
  595. let page, h, v, imageName, editorId, buildId
  596. beforeEach(async function () {
  597. this.req.params = { Project_id: this.projectId }
  598. page = String(Date.now())
  599. h = String(Math.random())
  600. v = String(Math.random())
  601. editorId = '172977cb-361e-4854-a4dc-a71cf11512e5'
  602. buildId = '195b4a3f9e7-03e5be430a9e7796'
  603. this.req.query = { page, h, v, editorId, buildId }
  604. imageName = 'foo/bar:tag-1'
  605. this.ProjectGetter.promises.getProject = sinon
  606. .stub()
  607. .resolves({ imageName })
  608. this.CompileController._proxyToClsi = sinon.stub()
  609. await this.CompileController.proxySyncPdf(this.req, this.res, this.next)
  610. })
  611. it('should proxy the request with an imageName', function () {
  612. expect(this.CompileController._proxyToClsi).to.have.been.calledWith(
  613. this.projectId,
  614. 'sync-to-pdf',
  615. `/project/${this.projectId}/user/${this.user_id}/sync/pdf`,
  616. {
  617. page,
  618. h,
  619. v,
  620. imageName,
  621. editorId,
  622. buildId,
  623. compileFromClsiCache: false,
  624. },
  625. this.req,
  626. this.res
  627. )
  628. })
  629. })
  630. describe('_proxyToClsi', function () {
  631. beforeEach(function () {
  632. this.req.method = 'mock-method'
  633. this.req.headers = {
  634. Mock: 'Headers',
  635. Range: '123-456',
  636. 'If-Range': 'abcdef',
  637. 'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT',
  638. }
  639. })
  640. describe('old pdf viewer', function () {
  641. describe('user with standard priority', function () {
  642. beforeEach(async function () {
  643. this.CompileManager.promises.getProjectCompileLimits = sinon
  644. .stub()
  645. .resolves({
  646. compileGroup: 'standard',
  647. compileBackendClass: 'e2',
  648. })
  649. await this.CompileController._proxyToClsi(
  650. this.projectId,
  651. 'output-file',
  652. (this.url = '/test'),
  653. { query: 'foo' },
  654. this.req,
  655. this.res,
  656. this.next
  657. )
  658. })
  659. it('should open a request to the CLSI', function () {
  660. this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
  661. `${this.settings.apis.clsi.url}${this.url}?compileGroup=standard&compileBackendClass=e2&query=foo`
  662. )
  663. })
  664. it('should pass the request on to the client', function () {
  665. this.pipeline.should.have.been.calledWith(this.clsiStream, this.res)
  666. })
  667. })
  668. describe('user with priority compile', function () {
  669. beforeEach(async function () {
  670. this.CompileManager.promises.getProjectCompileLimits = sinon
  671. .stub()
  672. .resolves({
  673. compileGroup: 'priority',
  674. compileBackendClass: 'c2d',
  675. })
  676. await this.CompileController._proxyToClsi(
  677. this.projectId,
  678. 'output-file',
  679. (this.url = '/test'),
  680. {},
  681. this.req,
  682. this.res,
  683. this.next
  684. )
  685. })
  686. it('should open a request to the CLSI', function () {
  687. this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
  688. `${this.settings.apis.clsi.url}${this.url}?compileGroup=priority&compileBackendClass=c2d`
  689. )
  690. })
  691. })
  692. describe('user with standard priority via query string', function () {
  693. beforeEach(async function () {
  694. this.req.query = { compileGroup: 'standard' }
  695. this.CompileManager.promises.getProjectCompileLimits = sinon
  696. .stub()
  697. .resolves({
  698. compileGroup: 'standard',
  699. compileBackendClass: 'e2',
  700. })
  701. await this.CompileController._proxyToClsi(
  702. this.projectId,
  703. 'output-file',
  704. (this.url = '/test'),
  705. {},
  706. this.req,
  707. this.res,
  708. this.next
  709. )
  710. })
  711. it('should open a request to the CLSI', function () {
  712. this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
  713. `${this.settings.apis.clsi.url}${this.url}?compileGroup=standard&compileBackendClass=e2`
  714. )
  715. })
  716. it('should pass the request on to the client', function () {
  717. this.pipeline.should.have.been.calledWith(this.clsiStream, this.res)
  718. })
  719. })
  720. describe('user with non-existent priority via query string', function () {
  721. beforeEach(async function () {
  722. this.req.query = { compileGroup: 'foobar' }
  723. this.CompileManager.promises.getProjectCompileLimits = sinon
  724. .stub()
  725. .resolves({
  726. compileGroup: 'standard',
  727. compileBackendClass: 'e2',
  728. })
  729. await this.CompileController._proxyToClsi(
  730. this.projectId,
  731. 'output-file',
  732. (this.url = '/test'),
  733. {},
  734. this.req,
  735. this.res,
  736. this.next
  737. )
  738. })
  739. it('should proxy to the standard url', function () {
  740. this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
  741. `${this.settings.apis.clsi.url}${this.url}?compileGroup=standard&compileBackendClass=e2`
  742. )
  743. })
  744. })
  745. describe('user with build parameter via query string', function () {
  746. beforeEach(async function () {
  747. this.CompileManager.promises.getProjectCompileLimits = sinon
  748. .stub()
  749. .resolves({
  750. compileGroup: 'standard',
  751. compileBackendClass: 'e2',
  752. })
  753. this.req.query = { build: 1234 }
  754. await this.CompileController._proxyToClsi(
  755. this.projectId,
  756. 'output-file',
  757. (this.url = '/test'),
  758. {},
  759. this.req,
  760. this.res,
  761. this.next
  762. )
  763. })
  764. it('should proxy to the standard url without the build parameter', function () {
  765. this.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
  766. `${this.settings.apis.clsi.url}${this.url}?compileGroup=standard&compileBackendClass=e2`
  767. )
  768. })
  769. })
  770. })
  771. })
  772. describe('deleteAuxFiles', function () {
  773. beforeEach(async function () {
  774. this.CompileManager.promises.deleteAuxFiles = sinon.stub().resolves()
  775. this.req.params = { Project_id: this.projectId }
  776. this.req.query = { clsiserverid: 'node-1' }
  777. this.res.sendStatus = sinon.stub()
  778. await this.CompileController.deleteAuxFiles(this.req, this.res, this.next)
  779. })
  780. it('should proxy to the CLSI', function () {
  781. this.CompileManager.promises.deleteAuxFiles
  782. .calledWith(this.projectId, this.user_id, 'node-1')
  783. .should.equal(true)
  784. })
  785. it('should return a 200', function () {
  786. this.res.sendStatus.calledWith(200).should.equal(true)
  787. })
  788. })
  789. describe('compileAndDownloadPdf', function () {
  790. beforeEach(function () {
  791. this.req = {
  792. params: {
  793. project_id: this.projectId,
  794. },
  795. }
  796. this.downloadPath = `/project/${this.projectId}/build/123/output/output.pdf`
  797. this.CompileManager.promises.compile.resolves({
  798. status: 'success',
  799. outputFiles: [{ path: 'output.pdf', url: this.downloadPath }],
  800. })
  801. this.CompileController._proxyToClsi = sinon.stub()
  802. this.res = { send: () => {}, sendStatus: sinon.stub() }
  803. })
  804. it('should call compile in the compile manager', async function () {
  805. await this.CompileController.compileAndDownloadPdf(this.req, this.res)
  806. this.CompileManager.promises.compile
  807. .calledWith(this.projectId)
  808. .should.equal(true)
  809. })
  810. it('should proxy the res to the clsi with correct url', async function () {
  811. await this.CompileController.compileAndDownloadPdf(this.req, this.res)
  812. sinon.assert.calledWith(
  813. this.CompileController._proxyToClsi,
  814. this.projectId,
  815. 'output-file',
  816. this.downloadPath,
  817. {},
  818. this.req,
  819. this.res
  820. )
  821. this.CompileController._proxyToClsi
  822. .calledWith(
  823. this.projectId,
  824. 'output-file',
  825. this.downloadPath,
  826. {},
  827. this.req,
  828. this.res
  829. )
  830. .should.equal(true)
  831. })
  832. it('should not download anything on compilation failures', async function () {
  833. this.CompileManager.promises.compile.rejects(new Error('failed'))
  834. await this.CompileController.compileAndDownloadPdf(
  835. this.req,
  836. this.res,
  837. this.next
  838. )
  839. this.res.sendStatus.should.have.been.calledWith(500)
  840. this.CompileController._proxyToClsi.should.not.have.been.called
  841. })
  842. it('should not download anything on missing pdf', async function () {
  843. this.CompileManager.promises.compile.resolves({
  844. status: 'success',
  845. outputFiles: [],
  846. })
  847. await this.CompileController.compileAndDownloadPdf(this.req, this.res)
  848. this.res.sendStatus.should.have.been.calledWith(500)
  849. this.CompileController._proxyToClsi.should.not.have.been.called
  850. })
  851. })
  852. describe('wordCount', function () {
  853. beforeEach(async function () {
  854. this.CompileManager.promises.wordCount = sinon
  855. .stub()
  856. .resolves({ content: 'body' })
  857. this.req.params = { Project_id: this.projectId }
  858. this.req.query = { clsiserverid: 'node-42' }
  859. this.res.json = sinon.stub()
  860. this.res.contentType = sinon.stub()
  861. await this.CompileController.wordCount(this.req, this.res, this.next)
  862. })
  863. it('should proxy to the CLSI', function () {
  864. this.CompileManager.promises.wordCount
  865. .calledWith(this.projectId, this.user_id, false, 'node-42')
  866. .should.equal(true)
  867. })
  868. it('should return a 200 and body', function () {
  869. this.res.json.calledWith({ content: 'body' }).should.equal(true)
  870. })
  871. })
  872. })