CompileController.test.mjs 31 KB

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