CompileControllerTests.js 29 KB

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