diff --git a/services/web/app/coffee/Features/Exports/ExportsHandler.coffee b/services/web/app/coffee/Features/Exports/ExportsHandler.coffee new file mode 100644 index 0000000000..727b01a575 --- /dev/null +++ b/services/web/app/coffee/Features/Exports/ExportsHandler.coffee @@ -0,0 +1,93 @@ +ProjectGetter = require('../Project/ProjectGetter') +ProjectLocator = require('../Project/ProjectLocator') +UserGetter = require('../User/UserGetter') +logger = require('logger-sharelatex') +settings = require 'settings-sharelatex' +async = require 'async' +request = require 'request' +request = request.defaults() +settings = require 'settings-sharelatex' + +module.exports = ExportsHandler = self = + + exportProject: (project_id, user_id, brand_variation_id, callback=(error, export_data) ->) -> + self._buildExport project_id, user_id, brand_variation_id, (err, export_data) -> + return callback(err) if err? + self._requestExport export_data, (err, export_v1_id) -> + return callback(err) if err? + export_data.v1_id = export_v1_id + # TODO: possibly store the export data in Mongo + callback null, export_data + + _buildExport: (project_id, user_id, brand_variation_id, callback=(err, export_data) ->) -> + jobs = + project: (cb) -> + ProjectGetter.getProject project_id, cb + # TODO: when we update async, signature will change from (cb, results) to (results, cb) + rootDoc: [ 'project', (cb, results) -> + ProjectLocator.findRootDoc {project: results.project, project_id: project_id}, cb + ] + user: (cb) -> + UserGetter.getUser user_id, {first_name: 1, last_name: 1, email: 1}, cb + historyVersion: (cb) -> + self._requestVersion project_id, cb + + async.auto jobs, (err, results) -> + if err? + logger.err err:err, project_id:project_id, user_id:user_id, brand_variation_id:brand_variation_id, "error building project export" + return callback(err) + + {project, rootDoc, user, historyVersion} = results + if !rootDoc[1]? + err = new Error("cannot export project without root doc") + logger.err err:err, project_id: project_id + return callback(err) + + export_data = + project: + id: project_id + rootDocPath: rootDoc[1]?.fileSystem + historyId: project.overleaf?.history?.id + historyVersion: historyVersion + user: + id: user_id + firstName: user.first_name + lastName: user.last_name + email: user.email + orcidId: null # until v2 gets ORCID + destination: + brandVariationId: brand_variation_id + options: + callbackUrl: null # for now, until we want v1 to call us back + callback null, export_data + + _requestExport: (export_data, callback=(err, export_v1_id) ->) -> + request.post { + url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports" + auth: {user: settings.apis.v1.user, pass: settings.apis.v1.pass } + json: export_data + }, (err, res, body) -> + if err? + logger.err err:err, export:export_data, "error making request to v1 export" + callback err + else if 200 <= res.statusCode < 300 + callback null, body.exportId + else + err = new Error("v1 export returned a failure status code: #{res.statusCode}") + logger.err err:err, export:export_data, "v1 export returned failure status code: #{res.statusCode}" + callback err + + _requestVersion: (project_id, callback=(err, export_v1_id) ->) -> + request.get { + url: "#{settings.apis.project_history.url}/project/#{project_id}/version" + json: true + }, (err, res, body) -> + if err? + logger.err err:err, project_id:project_id, "error making request to project history" + callback err + else if res.statusCode >= 200 and res.statusCode < 300 + callback null, body.version + else + err = new Error("project history version returned a failure status code: #{res.statusCode}") + logger.err err:err, project_id:project_id, "project history version returned failure status code: #{res.statusCode}" + callback err diff --git a/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee b/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee new file mode 100644 index 0000000000..6333db8270 --- /dev/null +++ b/services/web/test/unit/coffee/Exports/ExportsHandlerTests.coffee @@ -0,0 +1,202 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +expect = chai.expect +modulePath = '../../../../app/js/Features/Exports/ExportsHandler.js' +SandboxedModule = require('sandboxed-module') + +describe 'ExportsHandler', -> + + beforeEach -> + @ProjectGetter = {} + @ProjectLocator = {} + @UserGetter = {} + @settings = {} + @stubRequest = {} + @request = defaults: => return @stubRequest + @ExportsHandler = SandboxedModule.require modulePath, requires: + 'logger-sharelatex': + log: -> + err: -> + '../Project/ProjectGetter': @ProjectGetter + '../Project/ProjectLocator': @ProjectLocator + '../User/UserGetter': @UserGetter + 'settings-sharelatex': @settings + 'request': @request + @project_id = "project-id-123" + @project_history_id = 987 + @user_id = "user-id-456" + @brand_variation_id = 789 + @callback = sinon.stub() + + describe 'exportProject', -> + beforeEach (done) -> + @export_data = {iAmAnExport: true} + @response_body = {iAmAResponseBody: true} + @ExportsHandler._buildExport = sinon.stub().yields(null, @export_data) + @ExportsHandler._requestExport = sinon.stub().yields(null, @response_body) + @ExportsHandler.exportProject @project_id, @user_id, @brand_variation_id, (error, export_data) => + @callback(error, export_data) + done() + + it "should build the export", -> + @ExportsHandler._buildExport + .calledWith(@project_id, @user_id, @brand_variation_id) + .should.equal true + + it "should request the export", -> + @ExportsHandler._requestExport + .calledWith(@export_data) + .should.equal true + + it "should return the export", -> + @callback + .calledWith(null, @export_data) + .should.equal true + + describe '_buildExport', -> + beforeEach (done) -> + @project = + id: @project_id + overleaf: + history: + id: @project_history_id + @user = + id: @user_id + first_name: 'Arthur' + last_name: 'Author' + email: 'arthur.author@arthurauthoring.org' + @rootDocPath = 'main.tex' + @historyVersion = 777 + @ProjectGetter.getProject = sinon.stub().yields(null, @project) + @ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, {fileSystem: 'main.tex'}]) + @UserGetter.getUser = sinon.stub().yields(null, @user) + @ExportsHandler._requestVersion = sinon.stub().yields(null, @historyVersion) + done() + + describe "when all goes well", -> + beforeEach (done) -> + @ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) => + @callback(error, export_data) + done() + + it "should request the project history version", -> + @ExportsHandler._requestVersion.called + .should.equal true + + it "should return export data", -> + expected_export_data = + project: + id: @project_id + rootDocPath: @rootDocPath + historyId: @project_history_id + historyVersion: @historyVersion + user: + id: @user_id + firstName: @user.first_name + lastName: @user.last_name + email: @user.email + orcidId: null + destination: + brandVariationId: @brand_variation_id + options: + callbackUrl: null + @callback.calledWith(null, expected_export_data) + .should.equal true + + describe "when project is not found", -> + beforeEach (done) -> + @ProjectGetter.getProject = sinon.stub().yields(new Error("project not found")) + @ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) => + @callback(error, export_data) + done() + + it "should return an error", -> + (@callback.args[0][0] instanceof Error) + .should.equal true + + describe "when project has no root doc", -> + beforeEach (done) -> + @ProjectLocator.findRootDoc = sinon.stub().yields(null, [null, null]) + @ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) => + @callback(error, export_data) + done() + + it "should return an error", -> + (@callback.args[0][0] instanceof Error) + .should.equal true + + describe "when user is not found", -> + beforeEach (done) -> + @UserGetter.getUser = sinon.stub().yields(new Error("user not found")) + @ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) => + @callback(error, export_data) + done() + + it "should return an error", -> + (@callback.args[0][0] instanceof Error) + .should.equal true + + describe "when project history request fails", -> + beforeEach (done) -> + @ExportsHandler._requestVersion = sinon.stub().yields(new Error("project history call failed")) + @ExportsHandler._buildExport @project_id, @user_id, @brand_variation_id, (error, export_data) => + @callback(error, export_data) + done() + + it "should return an error", -> + (@callback.args[0][0] instanceof Error) + .should.equal true + + describe '_requestExport', -> + beforeEach (done) -> + @settings.apis = + v1: + url: 'http://localhost:5000' + user: 'overleaf' + pass: 'pass' + @export_data = {iAmAnExport: true} + @export_id = 4096 + @stubPost = sinon.stub().yields(null, {statusCode: 200}, { exportId: @export_id }) + done() + + describe "when all goes well", -> + beforeEach (done) -> + @stubRequest.post = @stubPost + @ExportsHandler._requestExport @export_data, (error, export_v1_id) => + @callback(error, export_v1_id) + done() + + it 'should issue the request', -> + expect(@stubPost.getCall(0).args[0]).to.deep.equal + url: @settings.apis.v1.url + '/api/v1/sharelatex/exports' + auth: + user: @settings.apis.v1.user + pass: @settings.apis.v1.pass + json: @export_data + + it 'should return the v1 export id', -> + @callback.calledWith(null, @export_id) + .should.equal true + + describe "when the request fails", -> + beforeEach (done) -> + @stubRequest.post = sinon.stub().yields(new Error("export request failed")) + @ExportsHandler._requestExport @export_data, (error, export_v1_id) => + @callback(error, export_v1_id) + done() + + it "should return an error", -> + (@callback.args[0][0] instanceof Error) + .should.equal true + + describe "when the request returns an error code", -> + beforeEach (done) -> + @stubRequest.post = sinon.stub().yields(null, {statusCode: 401}, { }) + @ExportsHandler._requestExport @export_data, (error, export_v1_id) => + @callback(error, export_v1_id) + done() + + it "should return the error", -> + (@callback.args[0][0] instanceof Error) + .should.equal true