sinon = require('sinon') chai = require('chai') should = chai.should() expect = chai.expect modulePath = "../../../../app/js/Features/Compile/ClsiManager.js" SandboxedModule = require('sandboxed-module') describe "ClsiManager", -> beforeEach -> @jar = {cookie:"stuff"} @ClsiCookieManager = getCookieJar: sinon.stub().callsArgWith(1, null, @jar) setServerId: sinon.stub().callsArgWith(2) _getServerId:sinon.stub() @ClsiStateManager = computeHash: sinon.stub().callsArgWith(2, null, "01234567890abcdef") @ClsiFormatChecker = checkRecoursesForProblems:sinon.stub().callsArgWith(1) @ClsiManager = SandboxedModule.require modulePath, requires: "settings-sharelatex": @settings = apis: filestore: url: "filestore.example.com" secret: "secret" clsi: url: "http://clsi.example.com" clsi_priority: url: "https://clsipremium.example.com" "../../models/Project": Project: @Project = {} "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} "../Project/ProjectGetter": @ProjectGetter = {} "../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler = getProjectDocsIfMatch: sinon.stub().callsArgWith(2,null,null) "./ClsiCookieManager": => @ClsiCookieManager "./ClsiStateManager": @ClsiStateManager "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), err: sinon.stub(), warn: sinon.stub() } "request": @request = sinon.stub() "./ClsiFormatChecker": @ClsiFormatChecker "metrics-sharelatex": @Metrics = Timer: class Timer done: sinon.stub() inc: sinon.stub() @project_id = "project-id" @user_id = "user-id" @callback = sinon.stub() describe "sendRequest", -> beforeEach -> @ClsiManager._buildRequest = sinon.stub().callsArgWith(2, null, @request = "mock-request") @ClsiCookieManager._getServerId.callsArgWith(1, null, "clsi3") describe "with a successful compile", -> beforeEach -> @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { compile: status: @status = "success" outputFiles: [{ url: "#{@settings.apis.clsi.url}/project/#{@project_id}/user/#{@user_id}/build/1234/output/output.pdf" path: "output.pdf" type: "pdf" build: 1234 },{ url: "#{@settings.apis.clsi.url}/project/#{@project_id}/user/#{@user_id}/build/1234/output/output.log" path: "output.log" type: "log" build: 1234 }] }) @ClsiManager.sendRequest @project_id, @user_id, {compileGroup:"standard"}, @callback it "should build the request", -> @ClsiManager._buildRequest .calledWith(@project_id) .should.equal true it "should send the request to the CLSI", -> @ClsiManager._postToClsi .calledWith(@project_id, @user_id, @request, "standard") .should.equal true it "should call the callback with the status and output files", -> outputFiles = [{ url: "/project/#{@project_id}/user/#{@user_id}/build/1234/output/output.pdf" path: "output.pdf" type: "pdf" build: 1234 },{ url: "/project/#{@project_id}/user/#{@user_id}/build/1234/output/output.log" path: "output.log" type: "log" build: 1234 }] @callback.calledWith(null, @status, outputFiles).should.equal true describe "with a failed compile", -> beforeEach -> @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { compile: status: @status = "failure" }) @ClsiManager.sendRequest @project_id, @user_id, {}, @callback it "should call the callback with a failure status", -> @callback.calledWith(null, @status).should.equal true describe "with a sync conflict", -> beforeEach -> @ClsiManager.sendRequestOnce = sinon.stub() @ClsiManager.sendRequestOnce.withArgs(@project_id, @user_id, {syncType:"full"}).callsArgWith(3, null, @status = "success") @ClsiManager.sendRequestOnce.withArgs(@project_id, @user_id, {}).callsArgWith(3, null, "conflict") @ClsiManager.sendRequest @project_id, @user_id, {}, @callback it "should call the sendRequestOnce method twice", -> @ClsiManager.sendRequestOnce.calledTwice.should.equal true it "should call the sendRequestOnce method with syncType:full", -> @ClsiManager.sendRequestOnce.calledWith(@project_id, @user_id, {syncType:"full"}).should.equal true it "should call the sendRequestOnce method without syncType:full", -> @ClsiManager.sendRequestOnce.calledWith(@project_id, @user_id, {}).should.equal true it "should call the callback with a success status", -> @callback.calledWith(null, @status, ).should.equal true describe "when the resources fail the precompile check", -> beforeEach -> @ClsiFormatChecker.checkRecoursesForProblems = sinon.stub().callsArgWith(1, new Error("failed")) @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { compile: status: @status = "failure" }) @ClsiManager.sendRequest @project_id, @user_id, {}, @callback it "should call the callback only once", -> @callback.calledOnce.should.equal true it "should call the callback with an error", -> @callback.calledWithExactly(new Error("failed")).should.equal true describe "sendExternalRequest", -> beforeEach -> @submission_id = "submission-id" @clsi_request = "mock-request" @ClsiCookieManager._getServerId.callsArgWith(1, null, "clsi3") describe "with a successful compile", -> beforeEach -> @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { compile: status: @status = "success" outputFiles: [{ url: "#{@settings.apis.clsi.url}/project/#{@submission_id}/build/1234/output/output.pdf" path: "output.pdf" type: "pdf" build: 1234 },{ url: "#{@settings.apis.clsi.url}/project/#{@submission_id}/build/1234/output/output.log" path: "output.log" type: "log" build: 1234 }] }) @ClsiManager.sendExternalRequest @submission_id, @clsi_request, {compileGroup:"standard"}, @callback it "should send the request to the CLSI", -> @ClsiManager._postToClsi .calledWith(@submission_id, null, @clsi_request, "standard") .should.equal true it "should call the callback with the status and output files", -> outputFiles = [{ url: "/project/#{@submission_id}/build/1234/output/output.pdf" path: "output.pdf" type: "pdf" build: 1234 },{ url: "/project/#{@submission_id}/build/1234/output/output.log" path: "output.log" type: "log" build: 1234 }] @callback.calledWith(null, @status, outputFiles).should.equal true describe "with a failed compile", -> beforeEach -> @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { compile: status: @status = "failure" }) @ClsiManager.sendExternalRequest @submission_id, @clsi_request, {}, @callback it "should call the callback with a failure status", -> @callback.calledWith(null, @status).should.equal true describe "when the resources fail the precompile check", -> beforeEach -> @ClsiFormatChecker.checkRecoursesForProblems = sinon.stub().callsArgWith(1, new Error("failed")) @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, { compile: status: @status = "failure" }) @ClsiManager.sendExternalRequest @submission_id, @clsi_request, {}, @callback it "should call the callback only once", -> @callback.calledOnce.should.equal true it "should call the callback with an error", -> @callback.calledWithExactly(new Error("failed")).should.equal true describe "deleteAuxFiles", -> beforeEach -> @ClsiManager._makeRequest = sinon.stub().callsArg(2) @DocumentUpdaterHandler.clearProjectState = sinon.stub().callsArg(1) describe "with the standard compileGroup", -> beforeEach -> @ClsiManager.deleteAuxFiles @project_id, @user_id, {compileGroup: "standard"}, @callback it "should call the delete method in the standard CLSI", -> @ClsiManager._makeRequest .calledWith(@project_id, { method:"DELETE", url:"#{@settings.apis.clsi.url}/project/#{@project_id}/user/#{@user_id}"}) .should.equal true it "should clear the project state from the docupdater", -> @DocumentUpdaterHandler.clearProjectState .calledWith(@project_id) .should.equal true it "should call the callback", -> @callback.called.should.equal true describe "_buildRequest", -> beforeEach -> @project = _id: @project_id compiler: @compiler = "latex" rootDoc_id: "mock-doc-id-1" imageName: @image = "mock-image-name" @docs = { "/main.tex": @doc_1 = { name: "main.tex" _id: "mock-doc-id-1" lines: ["Hello", "world"] }, "/chapters/chapter1.tex": @doc_2 = { name: "chapter1.tex" _id: "mock-doc-id-2" lines: [ "Chapter 1" ] } } @files = { "/images/image.png": @file_1 = { name: "image.png" _id: "mock-file-id-1" created: new Date() } } @Project.findById = sinon.stub().callsArgWith(2, null, @project) @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) @ProjectEntityHandler.getAllFiles = sinon.stub().callsArgWith(1, null, @files) @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) @DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith(1, null) describe "with a valid project", -> beforeEach (done) -> @ClsiManager._buildRequest @project_id, {timeout:100}, (error, request) => @request = request done() it "should get the project with the required fields", -> @ProjectGetter.getProject .calledWith(@project_id, {compiler:1, rootDoc_id: 1, imageName: 1, rootFolder: 1}) .should.equal true it "should flush the project to the database", -> @DocumentUpdaterHandler.flushProjectToMongo .calledWith(@project_id) .should.equal true it "should get all the docs", -> @ProjectEntityHandler.getAllDocs .calledWith(@project_id) .should.equal true it "should get all the files", -> @ProjectEntityHandler.getAllFiles .calledWith(@project_id) .should.equal true it "should build up the CLSI request", -> expect(@request).to.deep.equal( compile: options: compiler: @compiler timeout : 100 imageName: @image draft: false check: undefined syncType: undefined # "full" syncState: undefined # "01234567890abcdef" rootResourcePath: "main.tex" resources: [{ path: "main.tex" content: @doc_1.lines.join("\n") }, { path: "chapters/chapter1.tex" content: @doc_2.lines.join("\n") }, { path: "images/image.png" url: "#{@settings.apis.filestore.url}/project/#{@project_id}/file/#{@file_1._id}" modified: @file_1.created.getTime() }] ) describe "with the incremental compile option", -> beforeEach (done) -> @ClsiStateManager.computeHash = sinon.stub().callsArgWith(2, null, @project_state_hash = "01234567890abcdef") @DocumentUpdaterHandler.getProjectDocsIfMatch = sinon.stub().callsArgWith(2, null, [{_id:@doc_1._id, lines: @doc_1.lines, v: 123}]) @ProjectEntityHandler.getAllDocPathsFromProject = sinon.stub().callsArgWith(1, null, {"mock-doc-id-1":"main.tex"}) @ClsiManager._buildRequest @project_id, {timeout:100, incrementalCompilesEnabled:true}, (error, request) => @request = request done() it "should get the project with the required fields", -> @ProjectGetter.getProject .calledWith(@project_id, {compiler:1, rootDoc_id: 1, imageName: 1, rootFolder: 1}) .should.equal true it "should not explicitly flush the project to the database", -> @DocumentUpdaterHandler.flushProjectToMongo .calledWith(@project_id) .should.equal false it "should get only the live docs from the docupdater with a background flush in docupdater", -> @DocumentUpdaterHandler.getProjectDocsIfMatch .calledWith(@project_id) .should.equal true it "should not get any of the files", -> @ProjectEntityHandler.getAllFiles .called.should.equal false it "should build up the CLSI request", -> expect(@request).to.deep.equal( compile: options: compiler: @compiler timeout : 100 imageName: @image draft: false check: undefined syncType: "incremental" syncState: "01234567890abcdef" rootResourcePath: "main.tex" resources: [{ path: "main.tex" content: @doc_1.lines.join("\n") }] ) describe "when the root doc is set and not in the docupdater", -> beforeEach (done) -> @ClsiStateManager.computeHash = sinon.stub().callsArgWith(2, null, @project_state_hash = "01234567890abcdef") @DocumentUpdaterHandler.getProjectDocsIfMatch = sinon.stub().callsArgWith(2, null, [{_id:@doc_1._id, lines: @doc_1.lines, v: 123}]) @ProjectEntityHandler.getAllDocPathsFromProject = sinon.stub().callsArgWith(1, null, {"mock-doc-id-1":"main.tex", "mock-doc-id-2":"/chapters/chapter1.tex"}) @ClsiManager._buildRequest @project_id, {timeout:100, incrementalCompilesEnabled:true, rootDoc_id:"mock-doc-id-2"}, (error, request) => @request = request done() it "should still change the root path", -> @request.compile.rootResourcePath.should.equal "chapters/chapter1.tex" describe "when root doc override is valid", -> beforeEach (done) -> @ClsiManager._buildRequest @project_id, {rootDoc_id:"mock-doc-id-2"}, (error, request) => @request = request done() it "should change root path", -> @request.compile.rootResourcePath.should.equal "chapters/chapter1.tex" describe "when root doc override is invalid", -> beforeEach (done) -> @ClsiManager._buildRequest @project_id, {rootDoc_id:"invalid-id"}, (error, request) => @request = request done() it "should fallback to default root doc", -> @request.compile.rootResourcePath.should.equal "main.tex" describe "when the project has an invalid compiler", -> beforeEach (done) -> @project.compiler = "context" @ClsiManager._buildRequest @project, null, (error, request) => @request = request done() it "should set the compiler to pdflatex", -> @request.compile.options.compiler.should.equal "pdflatex" describe "when there is no valid root document", -> beforeEach (done) -> @project.rootDoc_id = "not-valid" @ClsiManager._buildRequest @project, null, (@error, @request) => done() it "should set to main.tex", -> @request.compile.rootResourcePath.should.equal "main.tex" describe "when there is no valid root document and no main.tex document", -> beforeEach () -> @project.rootDoc_id = "not-valid" @docs = { "/other.tex": @doc_1 = { name: "other.tex" _id: "mock-doc-id-1" lines: ["Hello", "world"] }, "/chapters/chapter1.tex": @doc_2 = { name: "chapter1.tex" _id: "mock-doc-id-2" lines: [ "Chapter 1" ] } } @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) @ClsiManager._buildRequest @project, null, @callback it "should report an error", -> @callback.calledWith(new Error("no main file specified")).should.equal true describe "when there is no valid root document and a single document which is not main.tex", -> beforeEach (done) -> @project.rootDoc_id = "not-valid" @docs = { "/other.tex": @doc_1 = { name: "other.tex" _id: "mock-doc-id-1" lines: ["Hello", "world"] } } @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) @ClsiManager._buildRequest @project, null, (@error, @request) => done() it "should set io to the only file", -> @request.compile.rootResourcePath.should.equal "other.tex" describe "with the draft option", -> it "should add the draft option into the request", (done) -> @ClsiManager._buildRequest @project_id, {timeout:100, draft: true}, (error, request) => request.compile.options.draft.should.equal true done() describe '_postToClsi', -> beforeEach -> @req = { mock: "req" } describe "successfully", -> beforeEach -> @ClsiManager._makeRequest = sinon.stub().callsArgWith(2, null, {statusCode: 204}, @body = { mock: "foo" }) @ClsiManager._postToClsi @project_id, @user_id, @req, "standard", @callback it 'should send the request to the CLSI', -> url = "#{@settings.apis.clsi.url}/project/#{@project_id}/user/#{@user_id}/compile" @ClsiManager._makeRequest.calledWith(@project_id, { method: "POST", url: url json: @req }).should.equal true it "should call the callback with the body and no error", -> @callback.calledWith(null, @body).should.equal true describe "when the CLSI returns an error", -> beforeEach -> @ClsiManager._makeRequest = sinon.stub().callsArgWith(2, null, {statusCode: 500}, @body = { mock: "foo" }) @ClsiManager._postToClsi @project_id, @user_id, @req, "standard", @callback it "should call the callback with the body and the error", -> @callback.calledWith(new Error("CLSI returned non-success code: 500"), @body).should.equal true describe "wordCount", -> beforeEach -> @ClsiManager._makeRequest = sinon.stub().callsArgWith(2, null, {statusCode: 200}, @body = { mock: "foo" }) @ClsiManager._buildRequest = sinon.stub().callsArgWith(2, null, @req = { compile: { rootResourcePath: "rootfile.text", options: {} } }) describe "with root file", -> beforeEach -> @ClsiManager.wordCount @project_id, @user_id, false, {}, @callback it "should call wordCount with root file", -> @ClsiManager._makeRequest .calledWith(@project_id, {method: "GET", url: "http://clsi.example.com/project/#{@project_id}/user/#{@user_id}/wordcount", qs: {file: "rootfile.text",image:undefined}}) .should.equal true it "should call the callback", -> @callback.called.should.equal true describe "with param file", -> beforeEach -> @ClsiManager.wordCount @project_id, @user_id, "main.tex", {}, @callback it "should call wordCount with param file", -> @ClsiManager._makeRequest .calledWith(@project_id, { method: "GET", url: "http://clsi.example.com/project/#{@project_id}/user/#{@user_id}/wordcount", qs:{file:"main.tex",image:undefined}}) .should.equal true describe "with image", -> beforeEach -> @req.compile.options.imageName = @image = "example.com/mock/image" @ClsiManager.wordCount @project_id, @user_id, "main.tex", {}, @callback it "should call wordCount with file and image", -> @ClsiManager._makeRequest .calledWith(@project_id, { method: "GET", url: "http://clsi.example.com/project/#{@project_id}/user/#{@user_id}/wordcount", qs:{file:"main.tex",image:@image}}) .should.equal true describe "_makeRequest", -> beforeEach -> @response = {there:"something"} @request.callsArgWith(1, null, @response) @opts = method: "SOMETHIGN" url: "http://a place on the web" it "should process a request with a cookie jar", (done)-> @ClsiManager._makeRequest @project_id, @opts, => args = @request.args[0] args[0].method.should.equal @opts.method args[0].url.should.equal @opts.url args[0].jar.should.equal @jar done() it "should set the cookie again on response as it might have changed", (done)-> @ClsiManager._makeRequest @project_id, @opts, => @ClsiCookieManager.setServerId.calledWith(@project_id, @response).should.equal true done() describe "_makeGoogleCloudRequest", -> beforeEach -> @settings.apis.clsi_new = url : "https://compiles.somewhere.test" @response = {there:"something"} @request.callsArgWith(1, null, @response) @opts = url: @ClsiManager._getCompilerUrl(null, @project_id) it "should change the domain on the url", (done)-> @ClsiManager._makeNewBackendRequest @project_id, @opts, => args = @request.args[0] args[0].url.should.equal "https://compiles.somewhere.test/project/#{@project_id}" done() it "should not make a request if there is not clsi_new url", (done)-> @settings.apis.clsi_new = undefined @ClsiManager._makeNewBackendRequest @project_id, @opts, (err)=> expect(err).to.equal undefined @request.callCount.should.equal 0 done()