|
@@ -5,12 +5,13 @@ import MockResponse from '../helpers/MockResponse.mjs'
|
|
|
import { Headers } from 'node-fetch'
|
|
import { Headers } from 'node-fetch'
|
|
|
import { ReadableString } from '@overleaf/stream-utils'
|
|
import { ReadableString } from '@overleaf/stream-utils'
|
|
|
import { RequestFailedError } from '@overleaf/fetch-utils'
|
|
import { RequestFailedError } from '@overleaf/fetch-utils'
|
|
|
|
|
+import { asZodError } from '@overleaf/validation-tools/testUtils.js'
|
|
|
|
|
|
|
|
const modulePath = '../../../../app/src/Features/Compile/CompileController.mjs'
|
|
const modulePath = '../../../../app/src/Features/Compile/CompileController.mjs'
|
|
|
|
|
|
|
|
describe('CompileController', function () {
|
|
describe('CompileController', function () {
|
|
|
beforeEach(async function (ctx) {
|
|
beforeEach(async function (ctx) {
|
|
|
- ctx.user_id = 'wat'
|
|
|
|
|
|
|
+ ctx.user_id = 'aaaaaaaaaaaaaaaaaaaaaaaa'
|
|
|
ctx.user = {
|
|
ctx.user = {
|
|
|
_id: ctx.user_id,
|
|
_id: ctx.user_id,
|
|
|
email: 'user@example.com',
|
|
email: 'user@example.com',
|
|
@@ -25,7 +26,10 @@ describe('CompileController', function () {
|
|
|
ctx.CompileManager = {
|
|
ctx.CompileManager = {
|
|
|
promises: {
|
|
promises: {
|
|
|
compile: sinon.stub(),
|
|
compile: sinon.stub(),
|
|
|
- getProjectCompileLimits: sinon.stub(),
|
|
|
|
|
|
|
+ getProjectCompileLimits: sinon.stub().resolves({
|
|
|
|
|
+ compileBackendClass: 'c3d',
|
|
|
|
|
+ compileGroup: 'standard',
|
|
|
|
|
+ }),
|
|
|
syncTeX: sinon.stub(),
|
|
syncTeX: sinon.stub(),
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
@@ -92,6 +96,12 @@ describe('CompileController', function () {
|
|
|
pipeline: ctx.pipeline,
|
|
pipeline: ctx.pipeline,
|
|
|
}))
|
|
}))
|
|
|
|
|
|
|
|
|
|
+ ctx.Metrics = {
|
|
|
|
|
+ inc: sinon.stub(),
|
|
|
|
|
+ Timer: sinon.stub().returns({ done: sinon.stub(), labels: {} }),
|
|
|
|
|
+ }
|
|
|
|
|
+ vi.doMock('@overleaf/metrics', () => ({ default: ctx.Metrics }))
|
|
|
|
|
+
|
|
|
vi.doMock('@overleaf/settings', () => ({
|
|
vi.doMock('@overleaf/settings', () => ({
|
|
|
default: ctx.settings,
|
|
default: ctx.settings,
|
|
|
}))
|
|
}))
|
|
@@ -167,7 +177,10 @@ describe('CompileController', function () {
|
|
|
ctx.CompileController = (await import(modulePath)).default
|
|
ctx.CompileController = (await import(modulePath)).default
|
|
|
ctx.projectId = 'abc123def456abc123def456'
|
|
ctx.projectId = 'abc123def456abc123def456'
|
|
|
ctx.build_id = '18fbe9e7564-30dcb2f71250c690'
|
|
ctx.build_id = '18fbe9e7564-30dcb2f71250c690'
|
|
|
- ctx.next = sinon.stub()
|
|
|
|
|
|
|
+ ctx.next = sinon.stub().callsFake(err => {
|
|
|
|
|
+ // Flag unexpected next calls.
|
|
|
|
|
+ throw err
|
|
|
|
|
+ })
|
|
|
ctx.req = new MockRequest(vi)
|
|
ctx.req = new MockRequest(vi)
|
|
|
ctx.res = new MockResponse(vi)
|
|
ctx.res = new MockResponse(vi)
|
|
|
ctx.res = new MockResponse(vi)
|
|
ctx.res = new MockResponse(vi)
|
|
@@ -220,7 +233,7 @@ describe('CompileController', function () {
|
|
|
],
|
|
],
|
|
|
outputFilesArchive: {
|
|
outputFilesArchive: {
|
|
|
path: 'output.zip',
|
|
path: 'output.zip',
|
|
|
- url: `/project/${ctx.projectId}/user/wat/build/${ctx.build_id}/output/output.zip`,
|
|
|
|
|
|
|
+ url: `/project/${ctx.projectId}/user/${ctx.user_id}/build/${ctx.build_id}/output/output.zip`,
|
|
|
type: 'zip',
|
|
type: 'zip',
|
|
|
},
|
|
},
|
|
|
pdfDownloadDomain: 'https://compiles.overleaf.test',
|
|
pdfDownloadDomain: 'https://compiles.overleaf.test',
|
|
@@ -265,7 +278,7 @@ describe('CompileController', function () {
|
|
|
],
|
|
],
|
|
|
outputFilesArchive: {
|
|
outputFilesArchive: {
|
|
|
path: 'output.zip',
|
|
path: 'output.zip',
|
|
|
- url: `/project/${ctx.projectId}/user/wat/build/${ctx.build_id}/output/output.zip`,
|
|
|
|
|
|
|
+ url: `/project/${ctx.projectId}/user/${ctx.user_id}/build/${ctx.build_id}/output/output.zip`,
|
|
|
type: 'zip',
|
|
type: 'zip',
|
|
|
},
|
|
},
|
|
|
outputUrlPrefix: '/zone/b',
|
|
outputUrlPrefix: '/zone/b',
|
|
@@ -317,7 +330,7 @@ describe('CompileController', function () {
|
|
|
outputFiles: ctx.outputFiles,
|
|
outputFiles: ctx.outputFiles,
|
|
|
outputFilesArchive: {
|
|
outputFilesArchive: {
|
|
|
path: 'output.zip',
|
|
path: 'output.zip',
|
|
|
- url: `/project/${ctx.projectId}/user/wat/build/${ctx.build_id}/output/output.zip`,
|
|
|
|
|
|
|
+ url: `/project/${ctx.projectId}/user/${ctx.user_id}/build/${ctx.build_id}/output/output.zip`,
|
|
|
type: 'zip',
|
|
type: 'zip',
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
@@ -511,13 +524,18 @@ describe('CompileController', function () {
|
|
|
|
|
|
|
|
describe('downloadPdf', function () {
|
|
describe('downloadPdf', function () {
|
|
|
beforeEach(function (ctx) {
|
|
beforeEach(function (ctx) {
|
|
|
- ctx.CompileController._proxyToClsi = sinon.stub().resolves()
|
|
|
|
|
- ctx.req.params = { Project_id: ctx.projectId }
|
|
|
|
|
|
|
+ ctx.clsiServerId = 'clsi-server-1'
|
|
|
|
|
+ ctx.req.params = {
|
|
|
|
|
+ Project_id: ctx.projectId,
|
|
|
|
|
+ build_id: ctx.build_id,
|
|
|
|
|
+ }
|
|
|
|
|
+ ctx.req.query = { clsiserverid: ctx.clsiServerId }
|
|
|
|
|
+ ctx.req.session = {}
|
|
|
ctx.project = { name: 'test namè; 1' }
|
|
ctx.project = { name: 'test namè; 1' }
|
|
|
ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(ctx.project)
|
|
ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(ctx.project)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- describe('when downloading for embedding', function () {
|
|
|
|
|
|
|
+ describe('logged-in', function () {
|
|
|
beforeEach(async function (ctx) {
|
|
beforeEach(async function (ctx) {
|
|
|
await ctx.CompileController.downloadPdf(ctx.req, ctx.res, ctx.next)
|
|
await ctx.CompileController.downloadPdf(ctx.req, ctx.res, ctx.next)
|
|
|
})
|
|
})
|
|
@@ -543,36 +561,22 @@ describe('CompileController', function () {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('should proxy the PDF from the CLSI', function (ctx) {
|
|
it('should proxy the PDF from the CLSI', function (ctx) {
|
|
|
- ctx.CompileController._proxyToClsi
|
|
|
|
|
- .calledWith(
|
|
|
|
|
- ctx.projectId,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- `/project/${ctx.projectId}/user/${ctx.user_id}/output/output.pdf`,
|
|
|
|
|
- {},
|
|
|
|
|
- ctx.req,
|
|
|
|
|
- ctx.res
|
|
|
|
|
- )
|
|
|
|
|
- .should.equal(true)
|
|
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
+ `${ctx.settings.apis.clsi.downloadHost}/project/${ctx.projectId}/user/${ctx.user_id}/build/${ctx.build_id}/output/output.pdf?clsiserverid=${ctx.clsiServerId}`
|
|
|
|
|
+ )
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- describe('when a build-id is provided', function () {
|
|
|
|
|
|
|
+ describe('anon', function () {
|
|
|
beforeEach(async function (ctx) {
|
|
beforeEach(async function (ctx) {
|
|
|
- ctx.req.params.build_id = ctx.build_id
|
|
|
|
|
|
|
+ ctx.SessionManager.getLoggedInUserId.returns(null)
|
|
|
await ctx.CompileController.downloadPdf(ctx.req, ctx.res, ctx.next)
|
|
await ctx.CompileController.downloadPdf(ctx.req, ctx.res, ctx.next)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('should proxy the PDF from the CLSI, with a build-id', function (ctx) {
|
|
|
|
|
- ctx.CompileController._proxyToClsi
|
|
|
|
|
- .calledWith(
|
|
|
|
|
- ctx.projectId,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- `/project/${ctx.projectId}/user/${ctx.user_id}/build/${ctx.build_id}/output/output.pdf`,
|
|
|
|
|
- {},
|
|
|
|
|
- ctx.req,
|
|
|
|
|
- ctx.res
|
|
|
|
|
- )
|
|
|
|
|
- .should.equal(true)
|
|
|
|
|
|
|
+ it('should proxy the PDF from the CLSI', function (ctx) {
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
+ `${ctx.settings.apis.clsi.downloadHost}/project/${ctx.projectId}/build/${ctx.build_id}/output/output.pdf?clsiserverid=${ctx.clsiServerId}`
|
|
|
|
|
+ )
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -587,9 +591,8 @@ describe('CompileController', function () {
|
|
|
})
|
|
})
|
|
|
it('should return 500', async function (ctx) {
|
|
it('should return 500', async function (ctx) {
|
|
|
await ctx.CompileController.downloadPdf(ctx.req, ctx.res, ctx.next)
|
|
await ctx.CompileController.downloadPdf(ctx.req, ctx.res, ctx.next)
|
|
|
- // should it be 429 instead?
|
|
|
|
|
- expect(ctx.res.sendStatus).toBeCalledWith(500)
|
|
|
|
|
- ctx.CompileController._proxyToClsi.should.not.have.been.called
|
|
|
|
|
|
|
+ expect(ctx.res.status).toBeCalledWith(429)
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.not.have.been.called
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -599,70 +602,111 @@ describe('CompileController', function () {
|
|
|
})
|
|
})
|
|
|
it('should return 500', async function (ctx) {
|
|
it('should return 500', async function (ctx) {
|
|
|
await ctx.CompileController.downloadPdf(ctx.req, ctx.res, ctx.next)
|
|
await ctx.CompileController.downloadPdf(ctx.req, ctx.res, ctx.next)
|
|
|
- expect(ctx.res.sendStatus).toBeCalledWith(500)
|
|
|
|
|
- ctx.CompileController._proxyToClsi.should.not.have.been.called
|
|
|
|
|
|
|
+ expect(ctx.res.status).toBeCalledWith(500)
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.not.have.been.called
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- describe('getFileFromClsiWithoutUser', function () {
|
|
|
|
|
|
|
+ describe('getOutputZipFromClsi', function () {
|
|
|
beforeEach(function (ctx) {
|
|
beforeEach(function (ctx) {
|
|
|
- ctx.submission_id = 'sub-1234'
|
|
|
|
|
- ctx.file = 'output.pdf'
|
|
|
|
|
|
|
+ ctx.clsiServerId = 'clsi-server-1'
|
|
|
ctx.req.params = {
|
|
ctx.req.params = {
|
|
|
- submission_id: ctx.submission_id,
|
|
|
|
|
|
|
+ Project_id: ctx.projectId,
|
|
|
build_id: ctx.build_id,
|
|
build_id: ctx.build_id,
|
|
|
- file: ctx.file,
|
|
|
|
|
}
|
|
}
|
|
|
- ctx.req.body = {}
|
|
|
|
|
- ctx.expected_url = `/project/${ctx.submission_id}/build/${ctx.build_id}/output/${ctx.file}`
|
|
|
|
|
- ctx.CompileController._proxyToClsiWithLimits = sinon.stub()
|
|
|
|
|
|
|
+ ctx.req.query = { clsiserverid: ctx.clsiServerId }
|
|
|
|
|
+ ctx.req.session = {}
|
|
|
|
|
+ ctx.project = { name: 'test namè; 1' }
|
|
|
|
|
+ ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(ctx.project)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- describe('without limits specified', function () {
|
|
|
|
|
|
|
+ describe('free user', function () {
|
|
|
beforeEach(async function (ctx) {
|
|
beforeEach(async function (ctx) {
|
|
|
- await ctx.CompileController.getFileFromClsiWithoutUser(
|
|
|
|
|
|
|
+ await ctx.CompileController.getOutputZipFromClsi(
|
|
|
ctx.req,
|
|
ctx.req,
|
|
|
ctx.res,
|
|
ctx.res,
|
|
|
ctx.next
|
|
ctx.next
|
|
|
)
|
|
)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('should proxy to CLSI with correct URL and default limits', function (ctx) {
|
|
|
|
|
- ctx.CompileController._proxyToClsiWithLimits.should.have.been.calledWith(
|
|
|
|
|
- ctx.submission_id,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- ctx.expected_url,
|
|
|
|
|
- {},
|
|
|
|
|
- { compileGroup: 'standard', compileBackendClass: 'c3d' }
|
|
|
|
|
|
|
+ it('should look up the project', function (ctx) {
|
|
|
|
|
+ ctx.ProjectGetter.promises.getProject
|
|
|
|
|
+ .calledWith(ctx.projectId, { name: 1 })
|
|
|
|
|
+ .should.equal(true)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('should set the content-type of the response to application/zip', function (ctx) {
|
|
|
|
|
+ expect(ctx.res.contentType).toBeCalledWith('application/zip')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('should set the content-disposition header with a safe version of the project name', function (ctx) {
|
|
|
|
|
+ expect(ctx.res.headers['Content-Disposition']).toEqual(
|
|
|
|
|
+ 'attachment; filename="test_namè__1-output.zip"'
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('should proxy the PDF from the CLSI', function (ctx) {
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
+ `${ctx.settings.apis.clsi.url}/project/${ctx.projectId}/user/${ctx.user_id}/build/${ctx.build_id}/output/output.zip?compileBackendClass=c3d&clsiserverid=${ctx.clsiServerId}`
|
|
|
)
|
|
)
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- describe('with limits specified', function () {
|
|
|
|
|
- beforeEach(function (ctx) {
|
|
|
|
|
- ctx.req.body = { compileTimeout: 600, compileGroup: 'special' }
|
|
|
|
|
- ctx.CompileController.getFileFromClsiWithoutUser(
|
|
|
|
|
|
|
+ describe('premium user', function () {
|
|
|
|
|
+ beforeEach(async function (ctx) {
|
|
|
|
|
+ ctx.CompileManager.promises.getProjectCompileLimits = sinon
|
|
|
|
|
+ .stub()
|
|
|
|
|
+ .resolves({
|
|
|
|
|
+ compileGroup: 'priority',
|
|
|
|
|
+ compileBackendClass: 'c4d',
|
|
|
|
|
+ })
|
|
|
|
|
+ await ctx.CompileController.getOutputZipFromClsi(
|
|
|
ctx.req,
|
|
ctx.req,
|
|
|
ctx.res,
|
|
ctx.res,
|
|
|
ctx.next
|
|
ctx.next
|
|
|
)
|
|
)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('should proxy to CLSI with correct URL and specified limits', function (ctx) {
|
|
|
|
|
- ctx.CompileController._proxyToClsiWithLimits.should.have.been.calledWith(
|
|
|
|
|
- ctx.submission_id,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- ctx.expected_url,
|
|
|
|
|
- {},
|
|
|
|
|
- {
|
|
|
|
|
- compileGroup: 'special',
|
|
|
|
|
- compileBackendClass: 'c3d',
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ it('should proxy the PDF from the CLSI', function (ctx) {
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
+ `${ctx.settings.apis.clsi.url}/project/${ctx.projectId}/user/${ctx.user_id}/build/${ctx.build_id}/output/output.zip?compileBackendClass=c4d&clsiserverid=${ctx.clsiServerId}`
|
|
|
)
|
|
)
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+ describe('getFileForSubmissionFromClsi', function () {
|
|
|
|
|
+ beforeEach(function (ctx) {
|
|
|
|
|
+ ctx.submission_id = 'sub-1234'
|
|
|
|
|
+ ctx.clsiServerId = 'clsi-server-1'
|
|
|
|
|
+ ctx.file = 'output.pdf'
|
|
|
|
|
+ ctx.req.params = {
|
|
|
|
|
+ submissionId: ctx.submission_id,
|
|
|
|
|
+ build_id: ctx.build_id,
|
|
|
|
|
+ file: ctx.file,
|
|
|
|
|
+ }
|
|
|
|
|
+ ctx.req.query = { clsiserverid: ctx.clsiServerId }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ describe('proxy to CLSI with correct URL', function () {
|
|
|
|
|
+ beforeEach(async function (ctx) {
|
|
|
|
|
+ await ctx.CompileController.getFileForSubmissionFromClsi(
|
|
|
|
|
+ ctx.req,
|
|
|
|
|
+ ctx.res,
|
|
|
|
|
+ ctx.next
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('should proxy to CLSI with correct URL', function (ctx) {
|
|
|
|
|
+ const expectedUrl = `${ctx.settings.apis.clsi.downloadHost}/project/${ctx.submission_id}/build/${ctx.build_id}/output/${ctx.file}?clsiserverid=${ctx.clsiServerId}`
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
+ expectedUrl
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
describe('proxySyncCode', function () {
|
|
describe('proxySyncCode', function () {
|
|
|
let file, line, column, imageName, editorId, buildId, clsiServerId
|
|
let file, line, column, imageName, editorId, buildId, clsiServerId
|
|
|
|
|
|
|
@@ -763,248 +807,148 @@ describe('CompileController', function () {
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- describe('_proxyToClsi', function () {
|
|
|
|
|
|
|
+ describe('getFileFromClsi', function () {
|
|
|
beforeEach(function (ctx) {
|
|
beforeEach(function (ctx) {
|
|
|
- ctx.req.method = 'mock-method'
|
|
|
|
|
- ctx.req.headers = {
|
|
|
|
|
- Mock: 'Headers',
|
|
|
|
|
- Range: '123-456',
|
|
|
|
|
- 'If-Range': 'abcdef',
|
|
|
|
|
- 'If-Modified-Since': 'Mon, 15 Dec 2014 15:23:56 GMT',
|
|
|
|
|
|
|
+ ctx.clsiServerId = 'clsi-server-1'
|
|
|
|
|
+ ctx.req.params = {
|
|
|
|
|
+ Project_id: ctx.projectId,
|
|
|
|
|
+ build_id: ctx.build_id,
|
|
|
|
|
+ file: 'output.blg',
|
|
|
}
|
|
}
|
|
|
|
|
+ ctx.req.query = { clsiserverid: ctx.clsiServerId }
|
|
|
|
|
+ ctx.req.session = {}
|
|
|
|
|
+ ctx.req.method = 'GET'
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- describe('old pdf viewer', function () {
|
|
|
|
|
- describe('user with standard priority', function () {
|
|
|
|
|
- beforeEach(async function (ctx) {
|
|
|
|
|
- ctx.CompileManager.promises.getProjectCompileLimits = sinon
|
|
|
|
|
- .stub()
|
|
|
|
|
- .resolves({
|
|
|
|
|
- compileGroup: 'standard',
|
|
|
|
|
- compileBackendClass: 'c3d',
|
|
|
|
|
- })
|
|
|
|
|
- await ctx.CompileController._proxyToClsi(
|
|
|
|
|
- ctx.projectId,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- (ctx.url = '/test'),
|
|
|
|
|
- { query: 'foo' },
|
|
|
|
|
- ctx.req,
|
|
|
|
|
- ctx.res,
|
|
|
|
|
- ctx.next
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- it('should open a request to the CLSI', function (ctx) {
|
|
|
|
|
- ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
- `${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=standard&compileBackendClass=c3d&query=foo`
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- it('should pass the request on to the client', function (ctx) {
|
|
|
|
|
- ctx.pipeline.should.have.been.calledWith(ctx.clsiStream, ctx.res)
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ describe('when the output.blg exists', function () {
|
|
|
|
|
+ beforeEach(async function (ctx) {
|
|
|
|
|
+ await ctx.CompileController.getFileFromClsi(ctx.req, ctx.res, ctx.next)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- describe('user with priority compile', function () {
|
|
|
|
|
- beforeEach(async function (ctx) {
|
|
|
|
|
- ctx.CompileManager.promises.getProjectCompileLimits = sinon
|
|
|
|
|
- .stub()
|
|
|
|
|
- .resolves({
|
|
|
|
|
- compileGroup: 'priority',
|
|
|
|
|
- compileBackendClass: 'c4d',
|
|
|
|
|
- })
|
|
|
|
|
- await ctx.CompileController._proxyToClsi(
|
|
|
|
|
- ctx.projectId,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- (ctx.url = '/test'),
|
|
|
|
|
- {},
|
|
|
|
|
- ctx.req,
|
|
|
|
|
- ctx.res,
|
|
|
|
|
- ctx.next
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ it('should open a request to the CLSI download host with compile limits', function (ctx) {
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
+ `${ctx.settings.apis.clsi.downloadHost}/project/${ctx.projectId}/user/${ctx.user_id}/build/${ctx.build_id}/output/output.blg?clsiserverid=${ctx.clsiServerId}`
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- it('should open a request to the CLSI', function (ctx) {
|
|
|
|
|
- ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
- `${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ it('should pass the response stream on to the client', function (ctx) {
|
|
|
|
|
+ ctx.pipeline.should.have.been.calledWith(ctx.clsiStream, ctx.res)
|
|
|
})
|
|
})
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- describe('when the output.pdf does not exist', function () {
|
|
|
|
|
- beforeEach(async function (ctx) {
|
|
|
|
|
- ctx.req.params.file = 'output.pdf'
|
|
|
|
|
- ctx.req.params.build_id = ctx.build_id
|
|
|
|
|
- ctx.url = `/project/${ctx.projectId}/build/${ctx.build_id}/output/${ctx.req.params.file}`
|
|
|
|
|
- ctx.editorId = '00000000-0000-0000-0000-000000000042'
|
|
|
|
|
- ctx.req.query = { editorId: ctx.editorId }
|
|
|
|
|
- ctx.CompileManager.promises.getProjectCompileLimits = sinon
|
|
|
|
|
- .stub()
|
|
|
|
|
- .resolves({
|
|
|
|
|
- compileGroup: 'priority',
|
|
|
|
|
- compileBackendClass: 'c4d',
|
|
|
|
|
- })
|
|
|
|
|
- ctx.fetchUtils.fetchStreamWithResponse.rejects(
|
|
|
|
|
- new RequestFailedError(
|
|
|
|
|
- `${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`,
|
|
|
|
|
- { method: 'GET' },
|
|
|
|
|
- { status: 404 }
|
|
|
|
|
- )
|
|
|
|
|
- )
|
|
|
|
|
- await ctx.CompileController._proxyToClsi(
|
|
|
|
|
- ctx.projectId,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- ctx.url,
|
|
|
|
|
- {},
|
|
|
|
|
- ctx.req,
|
|
|
|
|
- ctx.res,
|
|
|
|
|
- ctx.next
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ describe('when the output.blg traverses up', function () {
|
|
|
|
|
+ beforeEach(async function (ctx) {
|
|
|
|
|
+ ctx.req.params.file = '../output.blg'
|
|
|
|
|
+ ctx.next = sinon.stub()
|
|
|
|
|
+ await ctx.CompileController.getFileFromClsi(ctx.req, ctx.res, ctx.next)
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- it('should open a request to the CLSI', function (ctx) {
|
|
|
|
|
- ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
- `${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ it('should reject the request', function (ctx) {
|
|
|
|
|
+ ctx.next.should.have.been.calledWithMatch({
|
|
|
|
|
+ name: 'ParamsError',
|
|
|
|
|
+ cause: asZodError({
|
|
|
|
|
+ code: 'custom',
|
|
|
|
|
+ path: ['params', 'file'],
|
|
|
|
|
+ message: 'path traversal detected',
|
|
|
|
|
+ }),
|
|
|
})
|
|
})
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- it('should fallback to clsi-cache', function (ctx) {
|
|
|
|
|
- ctx.ClsiCacheController._downloadFromCacheWithParams.should.have.been.calledWith(
|
|
|
|
|
- ctx.req,
|
|
|
|
|
- ctx.res,
|
|
|
|
|
- ctx.projectId,
|
|
|
|
|
- `${ctx.editorId}-${ctx.build_id}`,
|
|
|
|
|
- 'output.pdf'
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ it('should not open a request to CLSI', function (ctx) {
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.not.have.been.called
|
|
|
})
|
|
})
|
|
|
- describe('when the output.stderr does not exist', function () {
|
|
|
|
|
- beforeEach(async function (ctx) {
|
|
|
|
|
- ctx.req.params.file = 'output.stderr'
|
|
|
|
|
- ctx.req.params.build_id = ctx.build_id
|
|
|
|
|
- ctx.url = `/project/${ctx.projectId}/build/${ctx.build_id}/output/${ctx.req.params.file}`
|
|
|
|
|
- ctx.editorId = '00000000-0000-0000-0000-000000000042'
|
|
|
|
|
- ctx.req.query = { editorId: ctx.editorId }
|
|
|
|
|
- ctx.CompileManager.promises.getProjectCompileLimits = sinon
|
|
|
|
|
- .stub()
|
|
|
|
|
- .resolves({
|
|
|
|
|
- compileGroup: 'priority',
|
|
|
|
|
- compileBackendClass: 'c4d',
|
|
|
|
|
- })
|
|
|
|
|
- ctx.fetchUtils.fetchStreamWithResponse.rejects(
|
|
|
|
|
- new RequestFailedError(
|
|
|
|
|
- `${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`,
|
|
|
|
|
- { method: 'GET' },
|
|
|
|
|
- { status: 404 }
|
|
|
|
|
- )
|
|
|
|
|
- )
|
|
|
|
|
- await ctx.CompileController._proxyToClsi(
|
|
|
|
|
- ctx.projectId,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- ctx.url,
|
|
|
|
|
- {},
|
|
|
|
|
- ctx.req,
|
|
|
|
|
- ctx.res,
|
|
|
|
|
- ctx.next
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- it('should open a request to the CLSI', function (ctx) {
|
|
|
|
|
- ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
- `${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ describe('when the buildId traverses up', function () {
|
|
|
|
|
+ beforeEach(async function (ctx) {
|
|
|
|
|
+ ctx.req.params.build_id = '../..'
|
|
|
|
|
+ ctx.next = sinon.stub()
|
|
|
|
|
+ await ctx.CompileController.getFileFromClsi(ctx.req, ctx.res, ctx.next)
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- it('should not fallback to clsi-cache', function (ctx) {
|
|
|
|
|
- ctx.ClsiCacheController._downloadFromCacheWithParams.should.not.have
|
|
|
|
|
- .been.called
|
|
|
|
|
- ctx.res.statusCode.should.equal(404)
|
|
|
|
|
|
|
+ it('should reject the request', function (ctx) {
|
|
|
|
|
+ ctx.next.should.have.been.calledWithMatch({
|
|
|
|
|
+ name: 'ParamsError',
|
|
|
|
|
+ cause: asZodError({
|
|
|
|
|
+ origin: 'string',
|
|
|
|
|
+ code: 'invalid_format',
|
|
|
|
|
+ format: 'regex',
|
|
|
|
|
+ pattern: '/^[0-9a-f]+-[0-9a-f]+$/',
|
|
|
|
|
+ path: ['params', 'build_id'],
|
|
|
|
|
+ message: 'invalid buildId',
|
|
|
|
|
+ }),
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- describe('user with standard priority via query string', function () {
|
|
|
|
|
- beforeEach(async function (ctx) {
|
|
|
|
|
- ctx.req.query = { compileGroup: 'standard' }
|
|
|
|
|
- ctx.CompileManager.promises.getProjectCompileLimits = sinon
|
|
|
|
|
- .stub()
|
|
|
|
|
- .resolves({
|
|
|
|
|
- compileGroup: 'standard',
|
|
|
|
|
- compileBackendClass: 'c3d',
|
|
|
|
|
- })
|
|
|
|
|
- await ctx.CompileController._proxyToClsi(
|
|
|
|
|
- ctx.projectId,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- (ctx.url = '/test'),
|
|
|
|
|
- {},
|
|
|
|
|
- ctx.req,
|
|
|
|
|
- ctx.res,
|
|
|
|
|
- ctx.next
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ it('should not open a request to CLSI', function (ctx) {
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.not.have.been.called
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- it('should open a request to the CLSI', function (ctx) {
|
|
|
|
|
- ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
- `${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=standard&compileBackendClass=c3d`
|
|
|
|
|
|
|
+ describe('when the output.blg does not exist', function () {
|
|
|
|
|
+ beforeEach(async function (ctx) {
|
|
|
|
|
+ ctx.editorId = '0e546f78-928e-4e8a-b5ea-3136ccf1dc53'
|
|
|
|
|
+ ctx.req.query = {
|
|
|
|
|
+ clsiserverid: ctx.clsiServerId,
|
|
|
|
|
+ editorId: ctx.editorId,
|
|
|
|
|
+ }
|
|
|
|
|
+ ctx.clsiURL = `${ctx.settings.apis.clsi.downloadHost}/project/${ctx.projectId}/user/${ctx.user_id}/build/${ctx.build_id}/output/output.blg?clsiserverid=${ctx.clsiServerId}`
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.rejects(
|
|
|
|
|
+ new RequestFailedError(
|
|
|
|
|
+ ctx.clsiURL,
|
|
|
|
|
+ { method: 'GET' },
|
|
|
|
|
+ { status: 404 }
|
|
|
)
|
|
)
|
|
|
- })
|
|
|
|
|
|
|
+ )
|
|
|
|
|
+ await ctx.CompileController.getFileFromClsi(ctx.req, ctx.res, ctx.next)
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- it('should pass the request on to the client', function (ctx) {
|
|
|
|
|
- ctx.pipeline.should.have.been.calledWith(ctx.clsiStream, ctx.res)
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ it('should open a request to the CLSI', function (ctx) {
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
+ ctx.clsiURL
|
|
|
|
|
+ )
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- describe('user with non-existent priority via query string', function () {
|
|
|
|
|
- beforeEach(async function (ctx) {
|
|
|
|
|
- ctx.req.query = { compileGroup: 'foobar' }
|
|
|
|
|
- ctx.CompileManager.promises.getProjectCompileLimits = sinon
|
|
|
|
|
- .stub()
|
|
|
|
|
- .resolves({
|
|
|
|
|
- compileGroup: 'standard',
|
|
|
|
|
- compileBackendClass: 'c3d',
|
|
|
|
|
- })
|
|
|
|
|
- await ctx.CompileController._proxyToClsi(
|
|
|
|
|
- ctx.projectId,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- (ctx.url = '/test'),
|
|
|
|
|
- {},
|
|
|
|
|
- ctx.req,
|
|
|
|
|
- ctx.res,
|
|
|
|
|
- ctx.next
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ it('should fallback to clsi-cache', function (ctx) {
|
|
|
|
|
+ ctx.ClsiCacheController._downloadFromCacheWithParams.should.have.been.calledWith(
|
|
|
|
|
+ ctx.req,
|
|
|
|
|
+ ctx.res,
|
|
|
|
|
+ ctx.projectId,
|
|
|
|
|
+ `${ctx.editorId}-${ctx.build_id}`,
|
|
|
|
|
+ 'output.blg'
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- it('should proxy to the standard url', function (ctx) {
|
|
|
|
|
- ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
- `${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=standard&compileBackendClass=c3d`
|
|
|
|
|
|
|
+ describe('when the output.stderr does not exist', function () {
|
|
|
|
|
+ beforeEach(async function (ctx) {
|
|
|
|
|
+ ctx.req.params.file = 'output.stderr'
|
|
|
|
|
+ ctx.editorId = '0e546f78-928e-4e8a-b5ea-3136ccf1dc53'
|
|
|
|
|
+ ctx.req.query = {
|
|
|
|
|
+ clsiserverid: ctx.clsiServerId,
|
|
|
|
|
+ editorId: ctx.editorId,
|
|
|
|
|
+ }
|
|
|
|
|
+ ctx.clsiURL = `${ctx.settings.apis.clsi.downloadHost}/project/${ctx.projectId}/user/${ctx.user_id}/build/${ctx.build_id}/output/output.stderr?clsiserverid=${ctx.clsiServerId}`
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.rejects(
|
|
|
|
|
+ new RequestFailedError(
|
|
|
|
|
+ ctx.clsiURL,
|
|
|
|
|
+ { method: 'GET' },
|
|
|
|
|
+ { status: 404 }
|
|
|
)
|
|
)
|
|
|
- })
|
|
|
|
|
|
|
+ )
|
|
|
|
|
+ await ctx.CompileController.getFileFromClsi(ctx.req, ctx.res, ctx.next)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- describe('user with build parameter via query string', function () {
|
|
|
|
|
- beforeEach(async function (ctx) {
|
|
|
|
|
- ctx.CompileManager.promises.getProjectCompileLimits = sinon
|
|
|
|
|
- .stub()
|
|
|
|
|
- .resolves({
|
|
|
|
|
- compileGroup: 'standard',
|
|
|
|
|
- compileBackendClass: 'c3d',
|
|
|
|
|
- })
|
|
|
|
|
- ctx.req.query = { build: 1234 }
|
|
|
|
|
- await ctx.CompileController._proxyToClsi(
|
|
|
|
|
- ctx.projectId,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- (ctx.url = '/test'),
|
|
|
|
|
- {},
|
|
|
|
|
- ctx.req,
|
|
|
|
|
- ctx.res,
|
|
|
|
|
- ctx.next
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ it('should open a request to the CLSI', function (ctx) {
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
+ ctx.clsiURL
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- it('should proxy to the standard url without the build parameter', function (ctx) {
|
|
|
|
|
- ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
- `${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=standard&compileBackendClass=c3d`
|
|
|
|
|
- )
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ it('should not fallback to clsi-cache', function (ctx) {
|
|
|
|
|
+ ctx.ClsiCacheController._downloadFromCacheWithParams.should.not.have
|
|
|
|
|
+ .been.called
|
|
|
|
|
+ expect(ctx.res.statusCode).to.equal(404)
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
@@ -1030,21 +974,28 @@ describe('CompileController', function () {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
describe('compileAndDownloadPdf', function () {
|
|
describe('compileAndDownloadPdf', function () {
|
|
|
|
|
+ const clsiServerId = 'server-1'
|
|
|
|
|
+
|
|
|
beforeEach(function (ctx) {
|
|
beforeEach(function (ctx) {
|
|
|
ctx.req = {
|
|
ctx.req = {
|
|
|
params: {
|
|
params: {
|
|
|
project_id: ctx.projectId,
|
|
project_id: ctx.projectId,
|
|
|
},
|
|
},
|
|
|
|
|
+ method: 'GET',
|
|
|
}
|
|
}
|
|
|
- ctx.downloadPath = `/project/${ctx.projectId}/build/123/output/output.pdf`
|
|
|
|
|
ctx.CompileManager.promises.compile.resolves({
|
|
ctx.CompileManager.promises.compile.resolves({
|
|
|
status: 'success',
|
|
status: 'success',
|
|
|
- outputFiles: [{ path: 'output.pdf', url: ctx.downloadPath }],
|
|
|
|
|
|
|
+ outputFiles: [{ path: 'output.pdf' }],
|
|
|
|
|
+ clsiServerId,
|
|
|
|
|
+ buildId: ctx.build_id,
|
|
|
})
|
|
})
|
|
|
- ctx.CompileController._proxyToClsi = sinon.stub()
|
|
|
|
|
ctx.res = {
|
|
ctx.res = {
|
|
|
send: () => {},
|
|
send: () => {},
|
|
|
sendStatus: sinon.stub(),
|
|
sendStatus: sinon.stub(),
|
|
|
|
|
+ writeHead: sinon.stub(),
|
|
|
|
|
+ setHeader: sinon.stub(),
|
|
|
|
|
+ setTimeout: sinon.stub(),
|
|
|
|
|
+ headersSent: false,
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -1055,28 +1006,11 @@ describe('CompileController', function () {
|
|
|
.should.equal(true)
|
|
.should.equal(true)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('should proxy the res to the clsi with correct url', async function (ctx) {
|
|
|
|
|
|
|
+ it('should proxy the PDF from the CLSI with the correct URL', async function (ctx) {
|
|
|
await ctx.CompileController.compileAndDownloadPdf(ctx.req, ctx.res)
|
|
await ctx.CompileController.compileAndDownloadPdf(ctx.req, ctx.res)
|
|
|
- sinon.assert.calledWith(
|
|
|
|
|
- ctx.CompileController._proxyToClsi,
|
|
|
|
|
- ctx.projectId,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- ctx.downloadPath,
|
|
|
|
|
- {},
|
|
|
|
|
- ctx.req,
|
|
|
|
|
- ctx.res
|
|
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
|
|
|
|
+ `${ctx.settings.apis.clsi.downloadHost}/project/${ctx.projectId}/build/${ctx.build_id}/output/output.pdf?clsiserverid=${clsiServerId}`
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
- ctx.CompileController._proxyToClsi
|
|
|
|
|
- .calledWith(
|
|
|
|
|
- ctx.projectId,
|
|
|
|
|
- 'output-file',
|
|
|
|
|
- ctx.downloadPath,
|
|
|
|
|
- {},
|
|
|
|
|
- ctx.req,
|
|
|
|
|
- ctx.res
|
|
|
|
|
- )
|
|
|
|
|
- .should.equal(true)
|
|
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('should not download anything on compilation failures', async function (ctx) {
|
|
it('should not download anything on compilation failures', async function (ctx) {
|
|
@@ -1087,17 +1021,19 @@ describe('CompileController', function () {
|
|
|
ctx.next
|
|
ctx.next
|
|
|
)
|
|
)
|
|
|
ctx.res.sendStatus.should.have.been.calledWith(500)
|
|
ctx.res.sendStatus.should.have.been.calledWith(500)
|
|
|
- ctx.CompileController._proxyToClsi.should.not.have.been.called
|
|
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.not.have.been.called
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('should not download anything on missing pdf', async function (ctx) {
|
|
it('should not download anything on missing pdf', async function (ctx) {
|
|
|
ctx.CompileManager.promises.compile.resolves({
|
|
ctx.CompileManager.promises.compile.resolves({
|
|
|
status: 'success',
|
|
status: 'success',
|
|
|
outputFiles: [],
|
|
outputFiles: [],
|
|
|
|
|
+ clsiServerId,
|
|
|
|
|
+ buildId: ctx.build_id,
|
|
|
})
|
|
})
|
|
|
await ctx.CompileController.compileAndDownloadPdf(ctx.req, ctx.res)
|
|
await ctx.CompileController.compileAndDownloadPdf(ctx.req, ctx.res)
|
|
|
ctx.res.sendStatus.should.have.been.calledWith(500)
|
|
ctx.res.sendStatus.should.have.been.calledWith(500)
|
|
|
- ctx.CompileController._proxyToClsi.should.not.have.been.called
|
|
|
|
|
|
|
+ ctx.fetchUtils.fetchStreamWithResponse.should.not.have.been.called
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|