| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- const SandboxedModule = require('sandboxed-module')
- const sinon = require('sinon')
- const { expect } = require('chai')
- const modulePath = require('path').join(
- __dirname,
- '../../../app/js/CompileController'
- )
- function tryImageNameValidation(method, imageNameField) {
- describe('when allowedImages is set', function () {
- beforeEach(function () {
- this.Settings.clsi = { docker: {} }
- this.Settings.clsi.docker.allowedImages = [
- 'repo/image:tag1',
- 'repo/image:tag2',
- ]
- this.res.send = sinon.stub()
- this.res.status = sinon.stub().returns({ send: this.res.send })
- this.CompileManager[method].reset()
- })
- describe('with an invalid image', function () {
- beforeEach(function () {
- this.req.query[imageNameField] = 'something/evil:1337'
- this.CompileController[method](this.req, this.res, this.next)
- })
- it('should return a 400', function () {
- expect(this.res.status.calledWith(400)).to.equal(true)
- })
- it('should not run the query', function () {
- expect(this.CompileManager[method].called).to.equal(false)
- })
- })
- describe('with a valid image', function () {
- beforeEach(function () {
- this.req.query[imageNameField] = 'repo/image:tag1'
- this.CompileController[method](this.req, this.res, this.next)
- })
- it('should not return a 400', function () {
- expect(this.res.status.calledWith(400)).to.equal(false)
- })
- it('should run the query', function () {
- expect(this.CompileManager[method].called).to.equal(true)
- })
- })
- })
- }
- describe('CompileController', function () {
- beforeEach(function () {
- this.buildId = 'build-id-123'
- this.CompileController = SandboxedModule.require(modulePath, {
- requires: {
- './CompileManager': (this.CompileManager = {}),
- './RequestParser': (this.RequestParser = {}),
- '@overleaf/settings': (this.Settings = {
- apis: {
- clsi: {
- url: 'http://clsi.example.com',
- outputUrlPrefix: '/zone/b',
- },
- },
- }),
- '@overleaf/metrics': {
- Timer: sinon.stub().returns({ done: sinon.stub() }),
- },
- './ProjectPersistenceManager': (this.ProjectPersistenceManager = {}),
- },
- })
- this.Settings.externalUrl = 'http://www.example.com'
- this.req = {}
- this.res = {}
- this.next = sinon.stub()
- })
- describe('compile', function () {
- beforeEach(function () {
- this.req.body = {
- compile: 'mock-body',
- }
- this.req.params = { project_id: (this.project_id = 'project-id-123') }
- this.request = {
- compile: 'mock-parsed-request',
- }
- this.request_with_project_id = {
- compile: this.request.compile,
- project_id: this.project_id,
- }
- this.output_files = [
- {
- path: 'output.pdf',
- type: 'pdf',
- size: 1337,
- build: 1234,
- },
- {
- path: 'output.log',
- type: 'log',
- build: 1234,
- },
- ]
- this.RequestParser.parse = sinon
- .stub()
- .callsArgWith(1, null, this.request)
- this.ProjectPersistenceManager.markProjectAsJustAccessed = sinon
- .stub()
- .callsArg(1)
- this.stats = { foo: 1 }
- this.timings = { bar: 2 }
- this.res.status = sinon.stub().returnsThis()
- this.res.send = sinon.stub()
- })
- describe('successfully', function () {
- beforeEach(function () {
- this.CompileManager.doCompileWithLock = sinon.stub().yields(null, {
- outputFiles: this.output_files,
- stats: this.stats,
- timings: this.timings,
- buildId: this.buildId,
- })
- this.CompileController.compile(this.req, this.res)
- })
- it('should parse the request', function () {
- this.RequestParser.parse.calledWith(this.req.body).should.equal(true)
- })
- it('should run the compile for the specified project', function () {
- this.CompileManager.doCompileWithLock
- .calledWith(this.request_with_project_id)
- .should.equal(true)
- })
- it('should mark the project as accessed', function () {
- this.ProjectPersistenceManager.markProjectAsJustAccessed
- .calledWith(this.project_id)
- .should.equal(true)
- })
- it('should return the JSON response', function () {
- this.res.status.calledWith(200).should.equal(true)
- console.log(this.res.send.args[0][0].compile)
- sinon.assert.calledWith(
- this.res.send,
- sinon.match.has(
- 'compile',
- sinon.match({
- status: 'success',
- error: null,
- stats: this.stats,
- timings: this.timings,
- outputUrlPrefix: '/zone/b',
- outputFiles: [
- ...this.output_files.map(file => ({
- url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
- ...file,
- })),
- {
- url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${this.buildId}/output/output.zip`,
- build: this.buildId,
- path: 'output.zip',
- type: 'zip',
- },
- ],
- })
- )
- )
- })
- })
- describe('without a outputUrlPrefix', function () {
- beforeEach(function () {
- this.Settings.apis.clsi.outputUrlPrefix = ''
- this.CompileManager.doCompileWithLock = sinon.stub().yields(null, {
- outputFiles: this.output_files,
- stats: this.stats,
- timings: this.timings,
- buildId: this.buildId,
- })
- this.CompileController.compile(this.req, this.res)
- })
- it('should return the JSON response with empty outputUrlPrefix', function () {
- this.res.status.calledWith(200).should.equal(true)
- this.res.send
- .calledWith({
- compile: {
- status: 'success',
- error: null,
- stats: this.stats,
- timings: this.timings,
- outputUrlPrefix: '',
- outputFiles: [
- ...this.output_files.map(file => ({
- url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
- ...file,
- })),
- {
- url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${this.buildId}/output/output.zip`,
- build: this.buildId,
- path: 'output.zip',
- type: 'zip',
- },
- ],
- },
- })
- .should.equal(true)
- })
- })
- describe('with user provided fake_output.pdf', function () {
- beforeEach(function () {
- this.output_files = [
- {
- path: 'fake_output.pdf',
- type: 'pdf',
- build: 1234,
- },
- {
- path: 'output.log',
- type: 'log',
- build: 1234,
- },
- ]
- this.CompileManager.doCompileWithLock = sinon.stub().yields(null, {
- outputFiles: this.output_files,
- stats: this.stats,
- timings: this.timings,
- buildId: this.buildId,
- })
- this.CompileController.compile(this.req, this.res)
- })
- it('should return the JSON response with status failure', function () {
- this.res.status.calledWith(200).should.equal(true)
- this.res.send
- .calledWith({
- compile: {
- status: 'failure',
- error: null,
- stats: this.stats,
- timings: this.timings,
- outputUrlPrefix: '/zone/b',
- outputFiles: [
- ...this.output_files.map(file => ({
- url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
- ...file,
- })),
- {
- url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${this.buildId}/output/output.zip`,
- build: this.buildId,
- path: 'output.zip',
- type: 'zip',
- },
- ],
- },
- })
- .should.equal(true)
- })
- })
- describe('with an empty output.pdf', function () {
- beforeEach(function () {
- this.output_files = [
- {
- path: 'output.pdf',
- type: 'pdf',
- size: 0,
- build: 1234,
- },
- {
- path: 'output.log',
- type: 'log',
- build: 1234,
- },
- ]
- this.CompileManager.doCompileWithLock = sinon.stub().yields(null, {
- outputFiles: this.output_files,
- stats: this.stats,
- timings: this.timings,
- buildId: this.buildId,
- })
- this.CompileController.compile(this.req, this.res)
- })
- it('should return the JSON response with status failure', function () {
- this.res.status.calledWith(200).should.equal(true)
- this.res.send
- .calledWith({
- compile: {
- status: 'failure',
- error: null,
- stats: this.stats,
- timings: this.timings,
- outputUrlPrefix: '/zone/b',
- outputFiles: [
- ...this.output_files.map(file => ({
- url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
- ...file,
- })),
- {
- url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${this.buildId}/output/output.zip`,
- build: this.buildId,
- path: 'output.zip',
- type: 'zip',
- },
- ],
- },
- })
- .should.equal(true)
- })
- })
- describe('with an error', function () {
- beforeEach(function () {
- const error = new Error((this.message = 'error message'))
- error.buildId = this.buildId
- this.CompileManager.doCompileWithLock = sinon
- .stub()
- .callsArgWith(1, error, null)
- this.CompileController.compile(this.req, this.res)
- })
- it('should return the JSON response with the error', function () {
- this.res.status.calledWith(500).should.equal(true)
- this.res.send
- .calledWith({
- compile: {
- status: 'error',
- error: this.message,
- outputUrlPrefix: '/zone/b',
- outputFiles: [],
- // JSON.stringify will omit these
- stats: undefined,
- timings: undefined,
- },
- })
- .should.equal(true)
- })
- })
- describe('when the request times out', function () {
- beforeEach(function () {
- this.error = new Error((this.message = 'container timed out'))
- this.error.timedout = true
- this.CompileManager.doCompileWithLock = sinon
- .stub()
- .callsArgWith(1, this.error, null)
- this.CompileController.compile(this.req, this.res)
- })
- it('should return the JSON response with the timeout status', function () {
- this.res.status.calledWith(200).should.equal(true)
- this.res.send
- .calledWith({
- compile: {
- status: 'timedout',
- error: this.message,
- outputUrlPrefix: '/zone/b',
- outputFiles: [],
- // JSON.stringify will omit these
- stats: undefined,
- timings: undefined,
- },
- })
- .should.equal(true)
- })
- })
- describe('when the request returns no output files', function () {
- beforeEach(function () {
- this.CompileManager.doCompileWithLock = sinon
- .stub()
- .callsArgWith(1, null, [])
- this.CompileController.compile(this.req, this.res)
- })
- it('should return the JSON response with the failure status', function () {
- this.res.status.calledWith(200).should.equal(true)
- this.res.send
- .calledWith({
- compile: {
- error: null,
- status: 'failure',
- outputUrlPrefix: '/zone/b',
- outputFiles: [],
- // JSON.stringify will omit these
- stats: undefined,
- timings: undefined,
- },
- })
- .should.equal(true)
- })
- })
- })
- describe('syncFromCode', function () {
- beforeEach(function () {
- this.file = 'main.tex'
- this.line = 42
- this.column = 5
- this.project_id = 'mock-project-id'
- this.req.params = { project_id: this.project_id }
- this.req.query = {
- file: this.file,
- line: this.line.toString(),
- column: this.column.toString(),
- }
- this.res.json = sinon.stub()
- this.CompileManager.syncFromCode = sinon
- .stub()
- .yields(null, (this.pdfPositions = ['mock-positions']))
- this.CompileController.syncFromCode(this.req, this.res, this.next)
- })
- it('should find the corresponding location in the PDF', function () {
- this.CompileManager.syncFromCode
- .calledWith(
- this.project_id,
- undefined,
- this.file,
- this.line,
- this.column
- )
- .should.equal(true)
- })
- it('should return the positions', function () {
- this.res.json
- .calledWith({
- pdf: this.pdfPositions,
- })
- .should.equal(true)
- })
- tryImageNameValidation('syncFromCode', 'imageName')
- })
- describe('syncFromPdf', function () {
- beforeEach(function () {
- this.page = 5
- this.h = 100.23
- this.v = 45.67
- this.project_id = 'mock-project-id'
- this.req.params = { project_id: this.project_id }
- this.req.query = {
- page: this.page.toString(),
- h: this.h.toString(),
- v: this.v.toString(),
- }
- this.res.json = sinon.stub()
- this.CompileManager.syncFromPdf = sinon
- .stub()
- .yields(null, (this.codePositions = ['mock-positions']))
- this.CompileController.syncFromPdf(this.req, this.res, this.next)
- })
- it('should find the corresponding location in the code', function () {
- this.CompileManager.syncFromPdf
- .calledWith(this.project_id, undefined, this.page, this.h, this.v)
- .should.equal(true)
- })
- it('should return the positions', function () {
- this.res.json
- .calledWith({
- code: this.codePositions,
- })
- .should.equal(true)
- })
- tryImageNameValidation('syncFromPdf', 'imageName')
- })
- describe('wordcount', function () {
- beforeEach(function () {
- this.file = 'main.tex'
- this.project_id = 'mock-project-id'
- this.req.params = { project_id: this.project_id }
- this.req.query = {
- file: this.file,
- image: (this.image = 'example.com/image'),
- }
- this.res.json = sinon.stub()
- this.CompileManager.wordcount = sinon
- .stub()
- .callsArgWith(4, null, (this.texcount = ['mock-texcount']))
- })
- it('should return the word count of a file', function () {
- this.CompileController.wordcount(this.req, this.res, this.next)
- this.CompileManager.wordcount
- .calledWith(this.project_id, undefined, this.file, this.image)
- .should.equal(true)
- })
- it('should return the texcount info', function () {
- this.CompileController.wordcount(this.req, this.res, this.next)
- this.res.json
- .calledWith({
- texcount: this.texcount,
- })
- .should.equal(true)
- })
- tryImageNameValidation('wordcount', 'image')
- })
- })
|