From 87269f30841568cfa134b22b9d6fbc64990a5c75 Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Tue, 24 Feb 2026 14:28:10 +0000 Subject: [PATCH] Merge pull request #31663 from overleaf/ar-convert-exportshandler-to-fetch-utils [web] convert ExportsHandler to fetch utils GitOrigin-RevId: 96e74a1254e456b4ac8a6d3a2bd6330a806c8693 --- .../Features/Exports/ExportsController.mjs | 225 +++---- .../src/Features/Exports/ExportsHandler.mjs | 425 +++++------- .../src/Exports/ExportsController.test.mjs | 200 +++--- .../unit/src/Exports/ExportsHandler.test.mjs | 622 ++++++++---------- 4 files changed, 640 insertions(+), 832 deletions(-) diff --git a/services/web/app/src/Features/Exports/ExportsController.mjs b/services/web/app/src/Features/Exports/ExportsController.mjs index d80166a8d5..f38b161da6 100644 --- a/services/web/app/src/Features/Exports/ExportsController.mjs +++ b/services/web/app/src/Features/Exports/ExportsController.mjs @@ -1,128 +1,109 @@ -/* eslint-disable - max-len, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ import ExportsHandler from './ExportsHandler.mjs' - +import { expressify } from '@overleaf/promise-utils' import SessionManager from '../Authentication/SessionManager.mjs' import logger from '@overleaf/logger' +import OError from '@overleaf/o-error' + +async function exportProject(req, res, next) { + const { project_id: projectId, brand_variation_id: brandVariationId } = + req.params + const userId = SessionManager.getLoggedInUserId(req.session) + const exportParams = { + project_id: projectId, + brand_variation_id: brandVariationId, + user_id: userId, + } + + if (req.body) { + if (req.body.firstName) { + exportParams.first_name = req.body.firstName.trim() + } + if (req.body.lastName) { + exportParams.last_name = req.body.lastName.trim() + } + // additional parameters for gallery exports + if (req.body.title) { + exportParams.title = req.body.title.trim() + } + if (req.body.description) { + exportParams.description = req.body.description.trim() + } + if (req.body.author) { + exportParams.author = req.body.author.trim() + } + if (req.body.license) { + exportParams.license = req.body.license.trim() + } + if (req.body.showSource != null) { + exportParams.show_source = req.body.showSource + } + } + + try { + const exportData = await ExportsHandler.exportProject(exportParams) + logger.debug( + { + userId, + projectId, + brandVariationId, + exportV1Id: exportData.v1_id, + }, + 'exported project' + ) + return res.json({ + export_v1_id: exportData.v1_id, + message: exportData.message, + }) + } catch (err) { + const info = OError.getFullInfo(err) + if (info?.forwardResponse) { + logger.debug( + { responseError: info.forwardResponse }, + 'forwarding response' + ) + const statusCode = info.forwardResponse.status || 500 + return res.status(statusCode).json(info.forwardResponse) + } + throw err + } +} + +async function exportStatus(req, res) { + const { export_id: exportId } = req.params + let exportJson + try { + exportJson = await ExportsHandler.fetchExport(exportId) + } catch (err) { + const json = { + status_summary: 'failed', + status_detail: String(err), + } + return res.json({ export_json: json }) + } + const parsedExport = JSON.parse(exportJson) + const json = { + status_summary: parsedExport.status_summary, + status_detail: parsedExport.status_detail, + partner_submission_id: parsedExport.partner_submission_id, + v2_user_email: parsedExport.v2_user_email, + v2_user_first_name: parsedExport.v2_user_first_name, + v2_user_last_name: parsedExport.v2_user_last_name, + title: parsedExport.title, + token: parsedExport.token, + } + return res.json({ export_json: json }) +} + +async function exportDownload(req, res, next) { + const { type, export_id: exportId } = req.params + + SessionManager.getLoggedInUserId(req.session) + const exportFileUrl = await ExportsHandler.fetchDownload(exportId, type) + return res.redirect(exportFileUrl) +} export default { - exportProject(req, res, next) { - const { project_id: projectId, brand_variation_id: brandVariationId } = - req.params - const userId = SessionManager.getLoggedInUserId(req.session) - const exportParams = { - project_id: projectId, - brand_variation_id: brandVariationId, - user_id: userId, - } - - if (req.body) { - if (req.body.firstName) { - exportParams.first_name = req.body.firstName.trim() - } - if (req.body.lastName) { - exportParams.last_name = req.body.lastName.trim() - } - // additional parameters for gallery exports - if (req.body.title) { - exportParams.title = req.body.title.trim() - } - if (req.body.description) { - exportParams.description = req.body.description.trim() - } - if (req.body.author) { - exportParams.author = req.body.author.trim() - } - if (req.body.license) { - exportParams.license = req.body.license.trim() - } - if (req.body.showSource != null) { - exportParams.show_source = req.body.showSource - } - } - - return ExportsHandler.exportProject( - exportParams, - function (err, exportData) { - if (err != null) { - if (err.forwardResponse != null) { - logger.debug( - { responseError: err.forwardResponse }, - 'forwarding response' - ) - const statusCode = err.forwardResponse.status || 500 - return res.status(statusCode).json(err.forwardResponse) - } else { - return next(err) - } - } - logger.debug( - { - userId, - projectId, - brandVariationId, - exportV1Id: exportData.v1_id, - }, - 'exported project' - ) - return res.json({ - export_v1_id: exportData.v1_id, - message: exportData.message, - }) - } - ) - }, - - exportStatus(req, res) { - const { export_id: exportId } = req.params - return ExportsHandler.fetchExport(exportId, function (err, exportJson) { - let json - if (err != null) { - json = { - status_summary: 'failed', - status_detail: err.toString, - } - res.json({ export_json: json }) - return err - } - const parsedExport = JSON.parse(exportJson) - json = { - status_summary: parsedExport.status_summary, - status_detail: parsedExport.status_detail, - partner_submission_id: parsedExport.partner_submission_id, - v2_user_email: parsedExport.v2_user_email, - v2_user_first_name: parsedExport.v2_user_first_name, - v2_user_last_name: parsedExport.v2_user_last_name, - title: parsedExport.title, - token: parsedExport.token, - } - return res.json({ export_json: json }) - }) - }, - - exportDownload(req, res, next) { - const { type, export_id: exportId } = req.params - - SessionManager.getLoggedInUserId(req.session) - return ExportsHandler.fetchDownload( - exportId, - type, - function (err, exportFileUrl) { - if (err != null) { - return next(err) - } - - return res.redirect(exportFileUrl) - } - ) - }, + exportDownload: expressify(exportDownload), + exportProject: expressify(exportProject), + exportStatus: expressify(exportStatus), } diff --git a/services/web/app/src/Features/Exports/ExportsHandler.mjs b/services/web/app/src/Features/Exports/ExportsHandler.mjs index 238c866528..b398701429 100644 --- a/services/web/app/src/Features/Exports/ExportsHandler.mjs +++ b/services/web/app/src/Features/Exports/ExportsHandler.mjs @@ -1,16 +1,3 @@ -/* eslint-disable - n/handle-callback-err, - max-len, - no-unused-vars, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ import OError from '@overleaf/o-error' import ProjectGetter from '../Project/ProjectGetter.mjs' import ProjectHistoryHandler from '../Project/ProjectHistoryHandler.mjs' @@ -19,39 +6,25 @@ import ProjectRootDocManager from '../Project/ProjectRootDocManager.mjs' import UserGetter from '../User/UserGetter.mjs' import logger from '@overleaf/logger' import settings from '@overleaf/settings' -import async from 'async' -import Request from 'request' +import { + fetchString, + fetchJson, + RequestFailedError, +} from '@overleaf/fetch-utils' let ExportsHandler -const request = Request.defaults() export default ExportsHandler = { - exportProject(exportParams, callback) { - if (callback == null) { - callback = function () {} - } - return ExportsHandler._buildExport( - exportParams, - function (err, exportData) { - if (err != null) { - return callback(err) - } - return ExportsHandler._requestExport(exportData, function (err, body) { - if (err != null) { - return callback(err) - } - exportData.v1_id = body.exportId - exportData.message = body.message - // TODO: possibly store the export data in Mongo - return callback(null, exportData) - }) - } - ) + async exportProject(exportParams) { + const exportData = await ExportsHandler._buildExport(exportParams) + const body = await ExportsHandler._requestExport(exportData) + + exportData.v1_id = body.exportId + exportData.message = body.message + // TODO: possibly store the export data in Mongo + return exportData }, - _buildExport(exportParams, callback) { - if (callback == null) { - callback = function () {} - } + async _buildExport(exportParams) { const { project_id: projectId, user_id: userId, @@ -62,221 +35,179 @@ export default ExportsHandler = { license, show_source: showSource, } = exportParams - const jobs = { - project(cb) { - return ProjectGetter.getProject(projectId, cb) - }, - rootDoc: [ - 'project', - (results, cb) => - ProjectRootDocManager.ensureRootDocumentIsValid( - projectId, - function (error) { - if (error != null) { - return callback(error) - } - return ProjectLocator.findRootDoc( - { project: results.project, project_id: projectId }, - cb - ) - } - ), - ], - user(cb) { - return UserGetter.getUser( - userId, - { first_name: 1, last_name: 1, email: 1, overleaf: 1 }, - cb - ) - }, - historyVersion(cb) { - return ProjectHistoryHandler.ensureHistoryExistsForProject( - projectId, - function (error) { - if (error != null) { - return callback(error) - } - return ExportsHandler._requestVersion(projectId, cb) - } - ) - }, + + let project, rootDoc, user, historyVersion + + try { + project = await ProjectGetter.promises.getProject(projectId) + + await ProjectRootDocManager.promises.ensureRootDocumentIsValid(projectId) + + rootDoc = await ProjectLocator.promises.findRootDoc({ + project, + project_id: projectId, + }) + + user = await UserGetter.promises.getUser(userId, { + first_name: 1, + last_name: 1, + email: 1, + overleaf: 1, + }) + await ProjectHistoryHandler.promises.ensureHistoryExistsForProject( + projectId + ) + historyVersion = await ExportsHandler._requestVersion(projectId) + } catch (err) { + throw OError.tag(err, 'error building project export', { + project_id: projectId, + user_id: userId, + brand_variation_id: brandVariationId, + }) } - return async.auto(jobs, function (err, results) { - if (err != null) { - OError.tag(err, 'error building project export', { - project_id: projectId, - user_id: userId, - brand_variation_id: brandVariationId, - }) - return callback(err) - } + if (!rootDoc || !rootDoc.path) { + throw new OError('cannot export project without root doc', { + project_id: projectId, + }) + } - const { project, rootDoc, user, historyVersion } = results - if (!rootDoc || rootDoc[1] == null) { - err = new OError('cannot export project without root doc', { - project_id: projectId, - }) - return callback(err) - } + if (exportParams.first_name && exportParams.last_name) { + user.first_name = exportParams.first_name + user.last_name = exportParams.last_name + } - if (exportParams.first_name && exportParams.last_name) { - user.first_name = exportParams.first_name - user.last_name = exportParams.last_name - } + return { + project: { + id: projectId, + rootDocPath: rootDoc.path ? rootDoc.path.fileSystem : undefined, + historyId: project.overleaf?.history?.id, + historyVersion, + v1ProjectId: project.overleaf != null ? project.overleaf.id : undefined, + metadata: { + compiler: project.compiler, + imageName: project.imageName, + title, + description, + author, + license, + showSource, + }, + }, + user: { + id: userId, + firstName: user.first_name, + lastName: user.last_name, + email: user.email, + orcidId: null, // until v2 gets ORCID + v1UserId: user.overleaf != null ? user.overleaf.id : undefined, + }, + destination: { + brandVariationId, + }, + options: { + callbackUrl: null, + }, // for now, until we want v1 to call us back + } + }, - const exportData = { - project: { - id: projectId, - rootDocPath: rootDoc[1] != null ? rootDoc[1].fileSystem : undefined, - historyId: project.overleaf?.history?.id, - historyVersion, - v1ProjectId: - project.overleaf != null ? project.overleaf.id : undefined, - metadata: { - compiler: project.compiler, - imageName: project.imageName, - title, - description, - author, - license, - showSource, + async _requestExport(exportData) { + try { + return await fetchJson( + new URL('/api/v1/overleaf/exports', settings.apis.v1.url), + { + basicAuth: { + user: settings.apis.v1.user, + password: settings.apis.v1.pass, }, + json: exportData, + signal: AbortSignal.timeout(settings.apis.v1.timeout), + method: 'POST', + } + ) + } catch (err) { + if (err instanceof RequestFailedError) { + logger.warn( + { export: exportData }, + `v1 export returned failure; forwarding: ${err.body}` + ) + // pass the v1 error along for the publish modal to handle + throw OError.tag(err, 'v1 export returned failure', { + forwardResponse: err.body, + }) + } + throw OError.tag(err, 'error making request to v1 export', { + export: exportData, + }) + } + }, + + async _requestVersion(projectId) { + const url = new URL(settings.apis.project_history.url) + url.pathname = `/project/${projectId}/version` + try { + const body = await fetchJson(url) + return body.version + } catch (err) { + if (err instanceof RequestFailedError) { + throw new OError( + 'project history version returned a failure status code', + { project_id: projectId, statusCode: err.response.status } + ) + } + throw OError.tag(err, 'error making request to project history', { + project_id: projectId, + }) + } + }, + + async fetchExport(exportId) { + const url = new URL(settings.apis.v1.url) + url.pathname = `/api/v1/overleaf/exports/${exportId}` + + try { + return await fetchString(url, { + basicAuth: { + user: settings.apis.v1.user, + password: settings.apis.v1.pass, }, - user: { - id: userId, - firstName: user.first_name, - lastName: user.last_name, - email: user.email, - orcidId: null, // until v2 gets ORCID - v1UserId: user.overleaf != null ? user.overleaf.id : undefined, + signal: AbortSignal.timeout(settings.apis.v1.timeout), + }) + } catch (err) { + if (err instanceof RequestFailedError) { + throw new OError('v1 export returned a failure status code', { + export: exportId, + statusCode: err.response.status, + }) + } + throw OError.tag(err, 'error making request to v1 export', { + export: exportId, + }) + } + }, + + async fetchDownload(exportId, type) { + const url = new URL(settings.apis.v1.url) + url.pathname = `/api/v1/overleaf/exports/${exportId}/${type}_url` + + try { + return await fetchString(url, { + basicAuth: { + user: settings.apis.v1.user, + password: settings.apis.v1.pass, }, - destination: { - brandVariationId, - }, - options: { - callbackUrl: null, - }, // for now, until we want v1 to call us back + signal: AbortSignal.timeout(settings.apis.v1.timeout), + }) + } catch (err) { + if (err instanceof RequestFailedError) { + throw new OError('v1 export returned a failure status code', { + export: exportId, + statusCode: err.response.status, + }) } - return callback(null, exportData) - }) - }, - - _requestExport(exportData, callback) { - if (callback == null) { - callback = function () {} + throw OError.tag(err, 'error making request to v1 export', { + export: exportId, + }) } - return request.post( - { - url: `${settings.apis.v1.url}/api/v1/overleaf/exports`, - auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, - json: exportData, - timeout: settings.apis.v1.timeout, - }, - function (err, res, body) { - if (err != null) { - OError.tag(err, 'error making request to v1 export', { - export: exportData, - }) - return callback(err) - } else if (res.statusCode >= 200 && res.statusCode < 300) { - return callback(null, body) - } else { - logger.warn( - { export: exportData }, - `v1 export returned failure; forwarding: ${body}` - ) - // pass the v1 error along for the publish modal to handle - const err = { forwardResponse: body } - return callback(err) - } - } - ) - }, - - _requestVersion(projectId, callback) { - if (callback == null) { - callback = function () {} - } - return request.get( - { - url: `${settings.apis.project_history.url}/project/${projectId}/version`, - json: true, - }, - function (err, res, body) { - if (err != null) { - OError.tag(err, 'error making request to project history', { - project_id: projectId, - }) - return callback(err) - } else if (res.statusCode >= 200 && res.statusCode < 300) { - return callback(null, body.version) - } else { - err = new OError( - 'project history version returned a failure status code', - { project_id: projectId, statusCode: res.statusCode } - ) - return callback(err) - } - } - ) - }, - - fetchExport(exportId, callback) { - if (callback == null) { - callback = function () {} - } - return request.get( - { - url: `${settings.apis.v1.url}/api/v1/overleaf/exports/${exportId}`, - auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, - timeout: settings.apis.v1.timeout, - }, - function (err, res, body) { - if (err != null) { - OError.tag(err, 'error making request to v1 export', { - export: exportId, - }) - return callback(err) - } else if (res.statusCode >= 200 && res.statusCode < 300) { - return callback(null, body) - } else { - err = new OError('v1 export returned a failure status code', { - export: exportId, - statusCode: res.statusCode, - }) - return callback(err) - } - } - ) - }, - - fetchDownload(exportId, type, callback) { - if (callback == null) { - callback = function () {} - } - return request.get( - { - url: `${settings.apis.v1.url}/api/v1/overleaf/exports/${exportId}/${type}_url`, - auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, - timeout: settings.apis.v1.timeout, - }, - function (err, res, body) { - if (err != null) { - OError.tag(err, 'error making request to v1 export', { - export: exportId, - }) - return callback(err) - } else if (res.statusCode >= 200 && res.statusCode < 300) { - return callback(null, body) - } else { - err = new OError('v1 export returned a failure status code', { - export: exportId, - statusCode: res.statusCode, - }) - return callback(err) - } - } - ) }, } diff --git a/services/web/test/unit/src/Exports/ExportsController.test.mjs b/services/web/test/unit/src/Exports/ExportsController.test.mjs index ce2a1a168d..99e744d0f4 100644 --- a/services/web/test/unit/src/Exports/ExportsController.test.mjs +++ b/services/web/test/unit/src/Exports/ExportsController.test.mjs @@ -1,5 +1,6 @@ import { expect, vi } from 'vitest' import sinon from 'sinon' +import OError from '@overleaf/o-error' const modulePath = new URL( '../../../../app/src/Features/Exports/ExportsController.mjs', import.meta.url @@ -66,56 +67,54 @@ describe('ExportsController', function () { describe('without gallery fields', function () { it('should ask the handler to perform the export', async function (ctx) { - await new Promise(resolve => { - ctx.handler.exportProject = sinon - .stub() - .yields(null, { iAmAnExport: true, v1_id: 897 }) - const expected = { - project_id: projectId, - user_id: userId, - brand_variation_id: brandVariationId, - first_name: firstName, - last_name: lastName, - } - return ctx.controller.exportProject(ctx.req, { - json: body => { - expect(ctx.handler.exportProject.args[0][0]).to.deep.equal(expected) - expect(body).to.deep.equal({ - export_v1_id: 897, - message: undefined, - }) - return resolve() - }, - }) + ctx.handler.exportProject = sinon + .stub() + .resolves({ iAmAnExport: true, v1_id: 897 }) + const expected = { + project_id: projectId, + user_id: userId, + brand_variation_id: brandVariationId, + first_name: firstName, + last_name: lastName, + } + const res = { + json: sinon.stub(), + } + + await ctx.controller.exportProject(ctx.req, res) + expect(ctx.handler.exportProject.args[0][0]).to.deep.equal(expected) + expect(res.json.args[0][0]).to.deep.equal({ + export_v1_id: 897, + message: undefined, }) }) }) describe('with a message from v1', function () { it('should ask the handler to perform the export', async function (ctx) { - await new Promise(resolve => { - ctx.handler.exportProject = sinon.stub().yields(null, { - iAmAnExport: true, - v1_id: 897, - message: 'RESUBMISSION', - }) - const expected = { - project_id: projectId, - user_id: userId, - brand_variation_id: brandVariationId, - first_name: firstName, - last_name: lastName, - } - return ctx.controller.exportProject(ctx.req, { - json: body => { - expect(ctx.handler.exportProject.args[0][0]).to.deep.equal(expected) - expect(body).to.deep.equal({ - export_v1_id: 897, - message: 'RESUBMISSION', - }) - return resolve() - }, - }) + ctx.handler.exportProject = sinon.stub().resolves({ + iAmAnExport: true, + v1_id: 897, + message: 'RESUBMISSION', + }) + const expected = { + project_id: projectId, + user_id: userId, + brand_variation_id: brandVariationId, + first_name: firstName, + last_name: lastName, + } + + const res = { + json: sinon.stub(), + } + + await ctx.controller.exportProject(ctx.req, res) + + expect(ctx.handler.exportProject.args[0][0]).to.deep.equal(expected) + expect(res.json.args[0][0]).to.deep.equal({ + export_v1_id: 897, + message: 'RESUBMISSION', }) }) }) @@ -130,56 +129,52 @@ describe('ExportsController', function () { }) it('should ask the handler to perform the export', async function (ctx) { - await new Promise(resolve => { - ctx.handler.exportProject = sinon - .stub() - .yields(null, { iAmAnExport: true, v1_id: 897 }) - const expected = { - project_id: projectId, - user_id: userId, - brand_variation_id: brandVariationId, - first_name: firstName, - last_name: lastName, - title, - description, - author, - license, - show_source: showSource, - } - return ctx.controller.exportProject(ctx.req, { - json: body => { - expect(ctx.handler.exportProject.args[0][0]).to.deep.equal(expected) - expect(body).to.deep.equal({ - export_v1_id: 897, - message: undefined, - }) - return resolve() - }, - }) + ctx.handler.exportProject = sinon + .stub() + .resolves({ iAmAnExport: true, v1_id: 897 }) + const expected = { + project_id: projectId, + user_id: userId, + brand_variation_id: brandVariationId, + first_name: firstName, + last_name: lastName, + title, + description, + author, + license, + show_source: showSource, + } + + const res = { + json: sinon.stub(), + } + + await ctx.controller.exportProject(ctx.req, res) + expect(ctx.handler.exportProject.args[0][0]).to.deep.equal(expected) + expect(res.json.args[0][0]).to.deep.equal({ + export_v1_id: 897, + message: undefined, }) }) }) describe('with an error return from v1 to forward to the publish modal', function () { it('should forward the response onward', async function (ctx) { - await new Promise(resolve => { - ctx.error_json = { status: 422, message: 'nope' } - ctx.handler.exportProject = sinon - .stub() - .yields({ forwardResponse: ctx.error_json }) - ctx.controller.exportProject(ctx.req, ctx.res, ctx.next) - expect(ctx.res.json.args[0][0]).to.deep.equal(ctx.error_json) - expect(ctx.res.status.args[0][0]).to.equal(ctx.error_json.status) - return resolve() - }) + ctx.error_json = { status: 422, message: 'nope' } + ctx.handler.exportProject = sinon.stub().rejects( + OError.tag(new Error('original error'), 'v1 error', { + forwardResponse: ctx.error_json, + }) + ) + await ctx.controller.exportProject(ctx.req, ctx.res, ctx.next) + expect(ctx.res.json.args[0][0]).to.deep.equal(ctx.error_json) + expect(ctx.res.status.args[0][0]).to.equal(ctx.error_json.status) }) }) it('should ask the handler to return the status of an export', async function (ctx) { - await new Promise(resolve => { - ctx.handler.fetchExport = sinon.stub().yields( - null, - `{ + ctx.handler.fetchExport = sinon.stub().resolves( + `{ "id":897, "status_summary":"completed", "status_detail":"all done", @@ -190,26 +185,25 @@ describe('ExportsController', function () { "title":"my project", "token":"token" }` - ) + ) - ctx.req.params = { project_id: projectId, export_id: 897 } - return ctx.controller.exportStatus(ctx.req, { - json: body => { - expect(body).to.deep.equal({ - export_json: { - status_summary: 'completed', - status_detail: 'all done', - partner_submission_id: 'abc123', - v2_user_email: 'la@tex.com', - v2_user_first_name: 'Arthur', - v2_user_last_name: 'Author', - title: 'my project', - token: 'token', - }, - }) - return resolve() - }, - }) + const res = { + json: sinon.stub(), + } + + ctx.req.params = { project_id: projectId, export_id: 897 } + await ctx.controller.exportStatus(ctx.req, res) + expect(res.json.args[0][0]).to.deep.equal({ + export_json: { + status_summary: 'completed', + status_detail: 'all done', + partner_submission_id: 'abc123', + v2_user_email: 'la@tex.com', + v2_user_first_name: 'Arthur', + v2_user_last_name: 'Author', + title: 'my project', + token: 'token', + }, }) }) }) diff --git a/services/web/test/unit/src/Exports/ExportsHandler.test.mjs b/services/web/test/unit/src/Exports/ExportsHandler.test.mjs index 05523834a6..a059b9718d 100644 --- a/services/web/test/unit/src/Exports/ExportsHandler.test.mjs +++ b/services/web/test/unit/src/Exports/ExportsHandler.test.mjs @@ -1,50 +1,53 @@ import { expect, vi } from 'vitest' import sinon from 'sinon' +import { RequestFailedError } from '@overleaf/fetch-utils' +import OError from '@overleaf/o-error' const modulePath = '../../../../app/src/Features/Exports/ExportsHandler.mjs' describe('ExportsHandler', function () { beforeEach(async function (ctx) { - ctx.stubRequest = {} - ctx.request = { - defaults: () => { - return ctx.stubRequest - }, - } - vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ - default: (ctx.ProjectGetter = {}), + default: (ctx.ProjectGetter = { promises: {} }), })) vi.doMock( '../../../../app/src/Features/Project/ProjectHistoryHandler', () => ({ - default: (ctx.ProjectHistoryHandler = {}), + default: (ctx.ProjectHistoryHandler = { + promises: {}, + }), }) ) + ctx.fetchUtils = { + fetchJson: sinon.stub().resolves(), + fetchString: sinon.stub().resolves(), + RequestFailedError, + } + + vi.doMock('@overleaf/fetch-utils', () => ctx.fetchUtils) + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ - default: (ctx.ProjectLocator = {}), + default: (ctx.ProjectLocator = { + promises: {}, + }), })) vi.doMock( '../../../../app/src/Features/Project/ProjectRootDocManager', () => ({ - default: (ctx.ProjectRootDocManager = {}), + default: (ctx.ProjectRootDocManager = { promises: {} }), }) ) vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ - default: (ctx.UserGetter = {}), + default: (ctx.UserGetter = { promises: {} }), })) vi.doMock('@overleaf/settings', () => ({ default: (ctx.settings = {}), })) - vi.doMock('request', () => ({ - default: ctx.request, - })) - ctx.ExportsHandler = (await import(modulePath)).default ctx.project_id = 'project-id-123' ctx.project_history_id = 987 @@ -65,32 +68,22 @@ describe('ExportsHandler', function () { license: ctx.license, show_source: ctx.show_source, } - ctx.callback = sinon.stub() }) describe('exportProject', function () { beforeEach(function (ctx) { ctx.export_data = { iAmAnExport: true } ctx.response_body = { iAmAResponseBody: true } - ctx.ExportsHandler._buildExport = sinon - .stub() - .yields(null, ctx.export_data) + ctx.ExportsHandler._buildExport = sinon.stub().resolves(ctx.export_data) ctx.ExportsHandler._requestExport = sinon .stub() - .yields(null, ctx.response_body) + .resolves(ctx.response_body) }) describe('when all goes well', function () { + let exportData beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.ExportsHandler.exportProject( - ctx.export_params, - (error, exportData) => { - ctx.callback(error, exportData) - resolve() - } - ) - }) + exportData = await ctx.ExportsHandler.exportProject(ctx.export_params) }) it('should build the export', function (ctx) { @@ -106,51 +99,46 @@ describe('ExportsHandler', function () { }) it('should return the export', function (ctx) { - ctx.callback.calledWith(null, ctx.export_data).should.equal(true) + expect(exportData).to.equal(ctx.export_data) }) }) describe("when request can't be built", function () { beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.ExportsHandler._buildExport = sinon - .stub() - .yields(new Error('cannot export project without root doc')) - ctx.ExportsHandler.exportProject( - ctx.export_params, - (error, exportData) => { - ctx.callback(error, exportData) - resolve() - } - ) - }) + ctx.ExportsHandler._buildExport = sinon + .stub() + .rejects(new Error('cannot export project without root doc')) }) - it('should return an error', function (ctx) { - expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) + it('should reject with an error', async function (ctx) { + await expect( + ctx.ExportsHandler.exportProject(ctx.export_params) + ).to.be.rejectedWith(Error) }) }) describe('when export request returns an error to forward to the user', function () { + let exportData + let exportError + beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.error_json = { status: 422, message: 'nope' } - ctx.ExportsHandler._requestExport = sinon - .stub() - .yields(null, { forwardResponse: ctx.error_json }) - ctx.ExportsHandler.exportProject( - ctx.export_params, - (error, exportData) => { - ctx.callback(error, exportData) - resolve() - } - ) - }) + ctx.error_json = { status: 422, message: 'nope' } + const error = new Error('error message') + ctx.ExportsHandler._requestExport = sinon.stub().rejects( + OError.tag(error, 'v1 error', { + forwardResponse: ctx.error_json, + }) + ) + try { + exportData = await ctx.ExportsHandler.exportProject(ctx.export_params) + } catch (error) { + exportError = error + } }) - it('should return success and the response to forward', function (ctx) { - expect(ctx.callback.args[0][0]).not.to.be.instanceOf(Error) - ctx.callback.calledWith(null, { + it('should throw with an error containing a response to forward', function (ctx) { + expect(exportData).to.be.undefined + expect(OError.getFullInfo(exportError)).to.eql({ forwardResponse: ctx.error_json, }) }) @@ -159,63 +147,53 @@ describe('ExportsHandler', function () { describe('_buildExport', function () { beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.project = { - id: ctx.project_id, - rootDoc_id: 'doc1_id', - compiler: 'pdflatex', - imageName: 'mock-image-name', - overleaf: { - id: ctx.project_history_id, // for projects imported from v1 - history: { - id: ctx.project_history_id, - }, + ctx.project = { + id: ctx.project_id, + rootDoc_id: 'doc1_id', + compiler: 'pdflatex', + imageName: 'mock-image-name', + overleaf: { + id: ctx.project_history_id, // for projects imported from v1 + history: { + id: ctx.project_history_id, }, - } - ctx.user = { - id: ctx.user_id, - first_name: 'Arthur', - last_name: 'Author', - email: 'arthur.author@arthurauthoring.org', - overleaf: { - id: 876, - }, - } - ctx.rootDocPath = 'main.tex' - ctx.historyVersion = 777 - ctx.ProjectGetter.getProject = sinon.stub().yields(null, ctx.project) - ctx.ProjectHistoryHandler.ensureHistoryExistsForProject = sinon - .stub() - .yields(null) - ctx.ProjectLocator.findRootDoc = sinon - .stub() - .yields(null, [null, { fileSystem: 'main.tex' }]) - ctx.ProjectRootDocManager.ensureRootDocumentIsValid = sinon - .stub() - .callsArgWith(1, null) - ctx.UserGetter.getUser = sinon.stub().yields(null, ctx.user) - ctx.ExportsHandler._requestVersion = sinon - .stub() - .yields(null, ctx.historyVersion) - resolve() - }) + }, + } + ctx.user = { + id: ctx.user_id, + first_name: 'Arthur', + last_name: 'Author', + email: 'arthur.author@arthurauthoring.org', + overleaf: { + id: 876, + }, + } + ctx.rootDocPath = 'main.tex' + ctx.historyVersion = 777 + ctx.ProjectGetter.promises.getProject = sinon.stub().resolves(ctx.project) + ctx.ProjectHistoryHandler.promises.ensureHistoryExistsForProject = sinon + .stub() + .resolves() + ctx.ProjectLocator.promises.findRootDoc = sinon + .stub() + .resolves({ element: null, path: { fileSystem: 'main.tex' } }) + ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid = sinon + .stub() + .resolves() + ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.user) + ctx.ExportsHandler._requestVersion = sinon + .stub() + .resolves(ctx.historyVersion) }) describe('when all goes well', function () { + let exportData beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.ExportsHandler._buildExport( - ctx.export_params, - (error, exportData) => { - ctx.callback(error, exportData) - resolve() - } - ) - }) + exportData = await ctx.ExportsHandler._buildExport(ctx.export_params) }) it('should ensure the project has history', function (ctx) { - ctx.ProjectHistoryHandler.ensureHistoryExistsForProject.called.should.equal( + ctx.ProjectHistoryHandler.promises.ensureHistoryExistsForProject.called.should.equal( true ) }) @@ -257,25 +235,18 @@ describe('ExportsHandler', function () { callbackUrl: null, }, } - ctx.callback.calledWith(null, expectedExportData).should.equal(true) + expect(exportData).to.eql(expectedExportData) }) }) describe('when we send replacement user first and last name', function () { + let exportData beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.custom_first_name = 'FIRST' - ctx.custom_last_name = 'LAST' - ctx.export_params.first_name = ctx.custom_first_name - ctx.export_params.last_name = ctx.custom_last_name - ctx.ExportsHandler._buildExport( - ctx.export_params, - (error, exportData) => { - ctx.callback(error, exportData) - resolve() - } - ) - }) + ctx.custom_first_name = 'FIRST' + ctx.custom_last_name = 'LAST' + ctx.export_params.first_name = ctx.custom_first_name + ctx.export_params.last_name = ctx.custom_last_name + exportData = await ctx.ExportsHandler._buildExport(ctx.export_params) }) it('should send the data from the user input', function (ctx) { @@ -311,51 +282,37 @@ describe('ExportsHandler', function () { callbackUrl: null, }, } - ctx.callback.calledWith(null, expectedExportData).should.equal(true) + expect(exportData).to.eql(expectedExportData) }) }) describe('when project is not found', function () { beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.ProjectGetter.getProject = sinon - .stub() - .yields(new Error('project not found')) - ctx.ExportsHandler._buildExport( - ctx.export_params, - (error, exportData) => { - ctx.callback(error, exportData) - resolve() - } - ) - }) + ctx.ProjectGetter.promises.getProject = sinon + .stub() + .rejects(new Error('project not found')) }) - it('should return an error', function (ctx) { - expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) + it('should return an error', async function (ctx) { + await expect( + ctx.ExportsHandler._buildExport(ctx.export_params) + ).to.be.rejectedWith(Error) }) }) describe('when project has no root doc', function () { describe('when a root doc can be set automatically', function () { + let exportData beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.project.rootDoc_id = null - ctx.ProjectLocator.findRootDoc = sinon - .stub() - .yields(null, [null, { fileSystem: 'other.tex' }]) - ctx.ExportsHandler._buildExport( - ctx.export_params, - (error, exportData) => { - ctx.callback(error, exportData) - resolve() - } - ) - }) + ctx.project.rootDoc_id = null + ctx.ProjectLocator.promises.findRootDoc = sinon + .stub() + .resolves({ path: { fileSystem: 'other.tex' } }) + exportData = await ctx.ExportsHandler._buildExport(ctx.export_params) }) it('should set a root doc', function (ctx) { - ctx.ProjectRootDocManager.ensureRootDocumentIsValid.called.should.equal( + ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid.called.should.equal( true ) }) @@ -393,32 +350,25 @@ describe('ExportsHandler', function () { callbackUrl: null, }, } - ctx.callback.calledWith(null, expectedExportData).should.equal(true) + expect(exportData).to.eql(expectedExportData) }) }) }) describe('when project has an invalid root doc', function () { describe('when a new root doc can be set automatically', function () { + let exportData beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.fakeDoc_id = '1a2b3c4d5e6f' - ctx.project.rootDoc_id = ctx.fakeDoc_id - ctx.ProjectLocator.findRootDoc = sinon - .stub() - .yields(null, [null, { fileSystem: 'other.tex' }]) - ctx.ExportsHandler._buildExport( - ctx.export_params, - (error, exportData) => { - ctx.callback(error, exportData) - resolve() - } - ) - }) + ctx.fakeDoc_id = '1a2b3c4d5e6f' + ctx.project.rootDoc_id = ctx.fakeDoc_id + ctx.ProjectLocator.promises.findRootDoc = sinon + .stub() + .resolves({ path: { fileSystem: 'other.tex' } }) + exportData = await ctx.ExportsHandler._buildExport(ctx.export_params) }) it('should set a valid root doc', function (ctx) { - ctx.ProjectRootDocManager.ensureRootDocumentIsValid.called.should.equal( + ctx.ProjectRootDocManager.promises.ensureRootDocumentIsValid.called.should.equal( true ) }) @@ -456,281 +406,233 @@ describe('ExportsHandler', function () { callbackUrl: null, }, } - ctx.callback.calledWith(null, expectedExportData).should.equal(true) + expect(exportData).to.eql(expectedExportData) }) }) describe('when no root doc can be identified', function () { - beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.ProjectLocator.findRootDoc = sinon - .stub() - .yields(null, [null, null]) - ctx.ExportsHandler._buildExport( - ctx.export_params, - (error, exportData) => { - ctx.callback(error, exportData) - resolve() - } - ) - }) + beforeEach(function (ctx) { + ctx.ProjectLocator.promises.findRootDoc = sinon + .stub() + .resolves({ element: null, path: null }) }) - it('should return an error', function (ctx) { - expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) + it('should return an error', async function (ctx) { + await expect( + ctx.ExportsHandler._buildExport(ctx.export_params) + ).to.be.rejectedWith(Error) }) }) }) describe('when user is not found', function () { beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.UserGetter.getUser = sinon - .stub() - .yields(new Error('user not found')) - ctx.ExportsHandler._buildExport( - ctx.export_params, - (error, exportData) => { - ctx.callback(error, exportData) - resolve() - } - ) - }) + ctx.UserGetter.promises.getUser = sinon + .stub() + .rejects(new Error('user not found')) }) - it('should return an error', function (ctx) { - expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) + it('should return an error', async function (ctx) { + await expect( + ctx.ExportsHandler._buildExport(ctx.export_params) + ).to.be.rejectedWith(Error) }) }) describe('when project history request fails', function () { - beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.ExportsHandler._requestVersion = sinon - .stub() - .yields(new Error('project history call failed')) - ctx.ExportsHandler._buildExport( - ctx.export_params, - (error, exportData) => { - ctx.callback(error, exportData) - resolve() - } - ) - }) + beforeEach(function (ctx) { + ctx.ExportsHandler._requestVersion = sinon + .stub() + .rejects(new Error('project history call failed')) }) - it('should return an error', function (ctx) { - expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) + it('should return an error', async function (ctx) { + await expect( + ctx.ExportsHandler._buildExport(ctx.export_params) + ).to.be.rejectedWith(Error) }) }) }) describe('_requestExport', function () { - beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.settings.apis = { - v1: { - url: 'http://127.0.0.1:5000', - user: 'overleaf', - pass: 'pass', - timeout: 15000, - }, - } - ctx.export_data = { iAmAnExport: true } - ctx.export_id = 4096 - ctx.stubPost = sinon - .stub() - .yields(null, { statusCode: 200 }, { exportId: ctx.export_id }) - resolve() - }) + beforeEach(function (ctx) { + ctx.settings.apis = { + v1: { + url: 'http://127.0.0.1:5000', + user: 'overleaf', + pass: 'pass', + timeout: 15000, + }, + } + ctx.export_data = { iAmAnExport: true } + ctx.export_id = 4096 + ctx.fetchUtils.fetchJson.resolves({ exportId: ctx.export_id }) }) describe('when all goes well', function () { + let response beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.stubRequest.post = ctx.stubPost - ctx.ExportsHandler._requestExport( - ctx.export_data, - (error, exportV1Id) => { - ctx.callback(error, exportV1Id) - resolve() - } - ) - }) + response = await ctx.ExportsHandler._requestExport(ctx.export_data) }) it('should issue the request', function (ctx) { - expect(ctx.stubPost.getCall(0).args[0]).to.deep.equal({ - url: ctx.settings.apis.v1.url + '/api/v1/overleaf/exports', - auth: { - user: ctx.settings.apis.v1.user, - pass: ctx.settings.apis.v1.pass, - }, - json: ctx.export_data, - timeout: 15000, - }) + expect(ctx.fetchUtils.fetchJson).to.have.been.calledWith( + new URL('/api/v1/overleaf/exports', ctx.settings.apis.v1.url), + { + basicAuth: { + user: ctx.settings.apis.v1.user, + password: ctx.settings.apis.v1.pass, + }, + json: ctx.export_data, + method: 'POST', + signal: sinon.match.instanceOf(AbortSignal), + } + ) }) it('should return the body with v1 export id', function (ctx) { - ctx.callback - .calledWith(null, { exportId: ctx.export_id }) - .should.equal(true) + expect(response).to.eql({ exportId: ctx.export_id }) }) }) describe('when the request fails', function () { - beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.stubRequest.post = sinon - .stub() - .yields(new Error('export request failed')) - ctx.ExportsHandler._requestExport( - ctx.export_data, - (error, exportV1Id) => { - ctx.callback(error, exportV1Id) - resolve() - } - ) - }) + beforeEach(function (ctx) { + ctx.fetchUtils.fetchJson = sinon + .stub() + .rejects(new Error('export request failed')) }) - it('should return an error', function (ctx) { - expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) + it('should return an error', async function (ctx) { + await expect( + ctx.ExportsHandler._requestExport(ctx.export_data) + ).to.be.rejectedWith(Error) }) }) describe('when the request returns an error response to forward', function () { + let response + let error beforeEach(async function (ctx) { ctx.error_code = 422 ctx.error_json = { status: ctx.error_code, message: 'nope' } - ctx.stubRequest.post = sinon + ctx.fetchUtils.fetchJson = sinon .stub() - .yields(null, { statusCode: ctx.error_code }, ctx.error_json) - - await new Promise(resolve => { - ctx.ExportsHandler._requestExport( - ctx.export_data, - (error, exportV1Id) => { - ctx.callback(error, exportV1Id) - resolve() - } + .rejects( + new RequestFailedError( + new URL('/api/v1/overleaf/exports', ctx.settings.apis.v1.url), + {}, + { status: ctx.error_code }, + ctx.error_json + ) ) - }) + + try { + response = await ctx.ExportsHandler._requestExport(ctx.export_data) + } catch (err) { + error = err + } }) - it('should return success and the response to forward', function (ctx) { - expect(ctx.callback.args[0][0]).not.to.be.instanceOf(Error) - ctx.callback.calledWith(null, { - forwardResponse: ctx.error_json, - }) + it('should throw with an error containing the response to forward', function (ctx) { + expect(response).to.be.undefined + expect(OError.getFullInfo(error)).to.have.property( + 'forwardResponse', + ctx.error_json + ) }) }) }) describe('fetchExport', function () { - beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.settings.apis = { - v1: { - url: 'http://127.0.0.1:5000', - user: 'overleaf', - pass: 'pass', - timeout: 15000, - }, - } - ctx.export_id = 897 - ctx.body = '{"id":897, "status_summary":"completed"}' - ctx.stubGet = sinon - .stub() - .yields(null, { statusCode: 200 }, { body: ctx.body }) - resolve() - }) + beforeEach(function (ctx) { + ctx.settings.apis = { + v1: { + url: 'http://127.0.0.1:5000', + user: 'overleaf', + pass: 'pass', + timeout: 15000, + }, + } + ctx.export_id = 897 + ctx.body = '{"id":897, "status_summary":"completed"}' + ctx.fetchUtils.fetchString = sinon + .stub() + .resolves(JSON.stringify({ body: ctx.body })) }) describe('when all goes well', function () { + let exportResponse beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.stubRequest.get = ctx.stubGet - ctx.ExportsHandler.fetchExport(ctx.export_id, (error, body) => { - ctx.callback(error, body) - resolve() - }) - }) + exportResponse = await ctx.ExportsHandler.fetchExport(ctx.export_id) }) it('should issue the request', function (ctx) { - expect(ctx.stubGet.getCall(0).args[0]).to.deep.equal({ - url: - ctx.settings.apis.v1.url + - '/api/v1/overleaf/exports/' + - ctx.export_id, - auth: { - user: ctx.settings.apis.v1.user, - pass: ctx.settings.apis.v1.pass, - }, - timeout: 15000, - }) + expect(ctx.fetchUtils.fetchString).to.have.been.calledWith( + new URL( + '/api/v1/overleaf/exports/' + ctx.export_id, + ctx.settings.apis.v1.url + ), + { + basicAuth: { + user: ctx.settings.apis.v1.user, + password: ctx.settings.apis.v1.pass, + }, + signal: sinon.match.instanceOf(AbortSignal), + } + ) }) it('should return the v1 export id', function (ctx) { - ctx.callback.calledWith(null, { body: ctx.body }).should.equal(true) + expect(exportResponse).to.eql(JSON.stringify({ body: ctx.body })) }) }) }) describe('fetchDownload', function () { beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.settings.apis = { - v1: { - url: 'http://127.0.0.1:5000', - user: 'overleaf', - pass: 'pass', - timeout: 15000, - }, - } - ctx.export_id = 897 - ctx.body = - 'https://writelatex-conversions-dev.s3.amazonaws.com/exports/ieee_latexqc/tnb/2912/xggmprcrpfwbsnqzqqmvktddnrbqkqkr.zip?X-Amz-Expires=14400&X-Amz-Date=20180730T181003Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJDGDIJFGLNVGZH6A/20180730/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=dec990336913cef9933f0e269afe99722d7ab2830ebf2c618a75673ee7159fee' - ctx.stubGet = sinon - .stub() - .yields(null, { statusCode: 200 }, { body: ctx.body }) - resolve() - }) + ctx.settings.apis = { + v1: { + url: 'http://127.0.0.1:5000', + user: 'overleaf', + pass: 'pass', + timeout: 15000, + }, + } + ctx.export_id = 897 + ctx.body = + 'https://writelatex-conversions-dev.s3.amazonaws.com/exports/ieee_latexqc/tnb/2912/xggmprcrpfwbsnqzqqmvktddnrbqkqkr.zip?X-Amz-Expires=14400&X-Amz-Date=20180730T181003Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJDGDIJFGLNVGZH6A/20180730/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=dec990336913cef9933f0e269afe99722d7ab2830ebf2c618a75673ee7159fee' + ctx.fetchUtils.fetchString = sinon.stub().resolves(ctx.body) }) describe('when all goes well', function () { + let downloadResponse beforeEach(async function (ctx) { - await new Promise(resolve => { - ctx.stubRequest.get = ctx.stubGet - ctx.ExportsHandler.fetchDownload( - ctx.export_id, - 'zip', - (error, body) => { - ctx.callback(error, body) - resolve() - } - ) - }) + downloadResponse = await ctx.ExportsHandler.fetchDownload( + ctx.export_id, + 'zip' + ) }) it('should issue the request', function (ctx) { - expect(ctx.stubGet.getCall(0).args[0]).to.deep.equal({ - url: + expect(ctx.fetchUtils.fetchString).to.have.been.calledWith( + new URL( ctx.settings.apis.v1.url + - '/api/v1/overleaf/exports/' + - ctx.export_id + - '/zip_url', - auth: { - user: ctx.settings.apis.v1.user, - pass: ctx.settings.apis.v1.pass, - }, - timeout: 15000, - }) + '/api/v1/overleaf/exports/' + + ctx.export_id + + '/zip_url' + ), + { + basicAuth: { + user: ctx.settings.apis.v1.user, + password: ctx.settings.apis.v1.pass, + }, + signal: sinon.match.instanceOf(AbortSignal), + } + ) }) it('should return the v1 export id', function (ctx) { - ctx.callback.calledWith(null, { body: ctx.body }).should.equal(true) + expect(downloadResponse).to.eql(ctx.body) }) }) })