From 77d34677616fecabaf20de718e76aa7a7aa1e4e7 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 2 Apr 2026 12:33:43 +0200 Subject: [PATCH] Merge pull request #32606 from overleaf/revert-30426-jdt-promisify-institutions-api Revert "Promisify InstitutionsApi" GitOrigin-RevId: 6bf06bf4220833abb3927911ab3079caeb061c25 --- .../Features/Institutions/InstitutionsAPI.mjs | 352 ++++++++++++------ .../src/Institutions/InstitutionsAPI.test.mjs | 124 +++--- 2 files changed, 309 insertions(+), 167 deletions(-) diff --git a/services/web/app/src/Features/Institutions/InstitutionsAPI.mjs b/services/web/app/src/Features/Institutions/InstitutionsAPI.mjs index 5d10d065a4..c938783154 100644 --- a/services/web/app/src/Features/Institutions/InstitutionsAPI.mjs +++ b/services/web/app/src/Features/Institutions/InstitutionsAPI.mjs @@ -1,7 +1,9 @@ +import { callbackify } from 'node:util' import OError from '@overleaf/o-error' import logger from '@overleaf/logger' import settings from '@overleaf/settings' -import { promiseMapWithLimit, callbackifyAll } from '@overleaf/promise-utils' +import request from 'requestretry' +import { promisify, promiseMapWithLimit } from '@overleaf/promise-utils' import NotificationsBuilder from '../Notifications/NotificationsBuilder.mjs' import { V1ConnectionError, @@ -14,7 +16,7 @@ function _makeRequestOptions(options) { const requestOptions = { method: options.method, basicAuth: { user: settings.apis.v1.user, password: settings.apis.v1.pass }, - signal: AbortSignal.timeout(options.timeout ?? settings.apis.v1.timeout), + signal: AbortSignal.timeout(settings.apis.v1.timeout), } if (options.body) { @@ -25,7 +27,7 @@ function _makeRequestOptions(options) { } function _responseErrorHandling(options, error) { - const status = error.response?.status + const status = error.response.status if (status >= 500) { throw new V1ConnectionError({ @@ -98,77 +100,94 @@ async function _affiliationRequestFetchNothing404Ok(options) { } } -async function getInstitutionAffiliations(institutionId) { - const json = await _affiliationRequestFetchJson({ - method: 'GET', - path: `/api/v2/institutions/${institutionId.toString()}/affiliations`, - defaultErrorMessage: "Couldn't get institution affiliations", - }) - return json || [] +function getInstitutionAffiliations(institutionId, callback) { + makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/institutions/${institutionId.toString()}/affiliations`, + defaultErrorMessage: "Couldn't get institution affiliations", + }, + (error, body) => callback(error, body || []) + ) } -async function getConfirmedInstitutionAffiliations(institutionId) { - const json = await _affiliationRequestFetchJson({ - method: 'GET', - path: `/api/v2/institutions/${institutionId.toString()}/confirmed_affiliations`, - defaultErrorMessage: "Couldn't get institution affiliations", - }) - return json || [] +function getConfirmedInstitutionAffiliations(institutionId, callback) { + makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/institutions/${institutionId.toString()}/confirmed_affiliations`, + defaultErrorMessage: "Couldn't get institution affiliations", + }, + (error, body) => callback(error, body || []) + ) } -async function getInstitutionAffiliationsCounts(institutionId) { - const json = await _affiliationRequestFetchJson({ - method: 'GET', - path: `/api/v2/institutions/${institutionId.toString()}/affiliations_counts`, - defaultErrorMessage: "Couldn't get institution counts", - }) - return json || [] +function getInstitutionAffiliationsCounts(institutionId, callback) { + makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/institutions/${institutionId.toString()}/affiliations_counts`, + defaultErrorMessage: "Couldn't get institution counts", + }, + (error, body) => callback(error, body || []) + ) } -async function getLicencesForAnalytics(lag, queryDate) { - const json = await _affiliationRequestFetchJson({ - method: 'GET', - path: `/api/v2/institutions/institutions_licences`, - body: { query_date: queryDate, lag }, - defaultErrorMessage: 'Could not get institutions licences', - timeout: 60_000, - }) - return json +function getLicencesForAnalytics(lag, queryDate, callback) { + makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/institutions/institutions_licences`, + body: { query_date: queryDate, lag }, + defaultErrorMessage: 'Could not get institutions licences', + timeout: 60_000, + }, + callback + ) } -async function getUserAffiliations(userId) { - const json = await _affiliationRequestFetchJson({ - method: 'GET', - path: `/api/v2/users/${userId.toString()}/affiliations`, - defaultErrorMessage: "Couldn't get user affiliations", - }) - - const affiliations = [] - - if (json?.length > 0) { - const concurrencyLimit = 10 - await promiseMapWithLimit(concurrencyLimit, json, async affiliation => { - if (affiliation.institution.confirmed) { - // only check groups if domain is confirmed - const group = ( - await Modules.promises.hooks.fire( - 'getGroupWithDomainCaptureByV1Id', - affiliation.institution.id - ) - )?.[0] - - if (group) { - affiliation.group = { - _id: group._id, - managedUsersEnabled: Boolean(group.managedUsersEnabled), - domainCaptureEnabled: Boolean(group.domainCaptureEnabled), - } - } +function getUserAffiliations(userId, callback) { + makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/users/${userId.toString()}/affiliations`, + defaultErrorMessage: "Couldn't get user affiliations", + }, + async (error, body) => { + if (error) { + return callback(error, []) } - affiliations.push(affiliation) - }) - } - return affiliations + + const affiliations = [] + + if (body?.length > 0) { + const concurrencyLimit = 10 + await promiseMapWithLimit(concurrencyLimit, body, async affiliation => { + if (affiliation.institution.confirmed) { + // only check groups if domain is confirmed + const group = ( + await Modules.promises.hooks.fire( + 'getGroupWithDomainCaptureByV1Id', + affiliation.institution.id + ) + )?.[0] + + if (group) { + affiliation.group = { + _id: group._id, + managedUsersEnabled: Boolean(group.managedUsersEnabled), + domainCaptureEnabled: Boolean(group.domainCaptureEnabled), + } + } + } + + affiliations.push(affiliation) + }) + } + + callback(null, affiliations) + } + ) } async function getUsersNeedingReconfirmationsLapsedProcessed() { @@ -236,50 +255,65 @@ async function removeAffiliation(userId, email) { }) } -async function endorseAffiliation(userId, email, role, department) { - await _affiliationRequestFetchNothing({ - method: 'POST', - path: `/api/v2/users/${userId.toString()}/affiliations/endorse`, - body: { email, role, department }, - defaultErrorMessage: "Couldn't endorse affiliation", - }) +function endorseAffiliation(userId, email, role, department, callback) { + makeAffiliationRequest( + { + method: 'POST', + path: `/api/v2/users/${userId.toString()}/affiliations/endorse`, + body: { email, role, department }, + defaultErrorMessage: "Couldn't endorse affiliation", + }, + callback + ) } -async function deleteAffiliations(userId) { - await _affiliationRequestFetchNothing({ - method: 'DELETE', - path: `/api/v2/users/${userId.toString()}/affiliations`, - defaultErrorMessage: "Couldn't delete affiliations", - }) +function deleteAffiliations(userId, callback) { + makeAffiliationRequest( + { + method: 'DELETE', + path: `/api/v2/users/${userId.toString()}/affiliations`, + defaultErrorMessage: "Couldn't delete affiliations", + }, + callback + ) } -// only used by syncUserEntitlements, safe to remove once that script isnt needed -async function addEntitlement(userId, email) { - const json = await _affiliationRequestFetchJson({ - method: 'POST', - path: `/api/v2/users/${userId}/affiliations/add_entitlement`, - body: { email }, - defaultErrorMessage: "Couldn't add entitlement", - }) - return json +function addEntitlement(userId, email, callback) { + makeAffiliationRequest( + { + method: 'POST', + path: `/api/v2/users/${userId}/affiliations/add_entitlement`, + body: { email }, + defaultErrorMessage: "Couldn't add entitlement", + }, + callback + ) } -async function removeEntitlement(userId, email) { - await _affiliationRequestFetchNothing404Ok({ - method: 'POST', - path: `/api/v2/users/${userId}/affiliations/remove_entitlement`, - body: { email }, - defaultErrorMessage: "Couldn't remove entitlement", - }) +function removeEntitlement(userId, email, callback) { + makeAffiliationRequest( + { + method: 'POST', + path: `/api/v2/users/${userId}/affiliations/remove_entitlement`, + body: { email }, + defaultErrorMessage: "Couldn't remove entitlement", + extraSuccessStatusCodes: [404], + }, + callback + ) } -async function sendUsersWithReconfirmationsLapsedProcessed(users) { - await _affiliationRequestFetchNothing({ - method: 'POST', - path: '/api/v2/institutions/reconfirmation_lapsed_processed', - body: { users }, - defaultErrorMessage: 'Could not update reconfirmation_lapsed_processed_at', - }) +function sendUsersWithReconfirmationsLapsedProcessed(users, callback) { + makeAffiliationRequest( + { + method: 'POST', + path: '/api/v2/institutions/reconfirmation_lapsed_processed', + body: { users }, + defaultErrorMessage: + 'Could not update reconfirmation_lapsed_processed_at', + }, + (error, body) => callback(error, body || []) + ) } async function verifyDomainMatchesDomainMatcher(domain, institutionId) { @@ -293,22 +327,120 @@ async function verifyDomainMatchesDomainMatcher(domain, institutionId) { const InstitutionsAPI = { getInstitutionAffiliations, + getConfirmedInstitutionAffiliations, + getInstitutionAffiliationsCounts, + getLicencesForAnalytics, + getUserAffiliations, + + getUsersNeedingReconfirmationsLapsedProcessed: callbackify( + getUsersNeedingReconfirmationsLapsedProcessed + ), + + addAffiliation: callbackify(addAffiliation), + + removeAffiliation: callbackify(removeAffiliation), + + endorseAffiliation, + + deleteAffiliations, + + addEntitlement, + + removeEntitlement, + + sendUsersWithReconfirmationsLapsedProcessed, +} + +function makeAffiliationRequest(options, callback) { + if (!settings.apis.v1.url) { + return callback(null) + } // service is not configured + if (!options.extraSuccessStatusCodes) { + options.extraSuccessStatusCodes = [] + } + const timeout = options.timeout ? options.timeout : settings.apis.v1.timeout + const requestOptions = { + method: options.method, + url: `${settings.apis.v1.url}${options.path}`, + body: options.body, + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, + json: true, + timeout, + } + if (options.method === 'GET') { + requestOptions.maxAttempts = 3 + requestOptions.retryDelay = 500 + } else { + requestOptions.maxAttempts = 0 + } + request(requestOptions, function (error, response, body) { + if (error) { + return callback( + new V1ConnectionError('error getting affiliations from v1').withCause( + error + ) + ) + } + if (response && response.statusCode >= 500) { + return callback( + new V1ConnectionError({ + message: 'error getting affiliations from v1', + info: { + status: response.statusCode, + body, + }, + }) + ) + } + let isSuccess = response.statusCode >= 200 && response.statusCode < 300 + if (!isSuccess) { + isSuccess = options.extraSuccessStatusCodes.includes(response.statusCode) + } + if (!isSuccess) { + let errorMessage + if (body && body.errors) { + errorMessage = `${response.statusCode}: ${body.errors}` + } else { + errorMessage = `${options.defaultErrorMessage}: ${response.statusCode}` + } + + logger.warn({ path: options.path, body: options.body }, errorMessage) + return callback( + new OError(errorMessage, { statusCode: response.statusCode }) + ) + } + + callback(null, body) + }) +} + +InstitutionsAPI.promises = { + getInstitutionAffiliations: promisify( + InstitutionsAPI.getInstitutionAffiliations + ), + getConfirmedInstitutionAffiliations: promisify( + InstitutionsAPI.getConfirmedInstitutionAffiliations + ), + getInstitutionAffiliationsCounts: promisify( + InstitutionsAPI.getInstitutionAffiliationsCounts + ), + getLicencesForAnalytics: promisify(InstitutionsAPI.getLicencesForAnalytics), + getUserAffiliations: promisify(InstitutionsAPI.getUserAffiliations), getUsersNeedingReconfirmationsLapsedProcessed, addAffiliation, removeAffiliation, - endorseAffiliation, - deleteAffiliations, - addEntitlement, - removeEntitlement, - sendUsersWithReconfirmationsLapsedProcessed, + endorseAffiliation: promisify(InstitutionsAPI.endorseAffiliation), + deleteAffiliations: promisify(InstitutionsAPI.deleteAffiliations), + addEntitlement: promisify(InstitutionsAPI.addEntitlement), + removeEntitlement: promisify(InstitutionsAPI.removeEntitlement), + sendUsersWithReconfirmationsLapsedProcessed: promisify( + InstitutionsAPI.sendUsersWithReconfirmationsLapsedProcessed + ), verifyDomainMatchesDomainMatcher, } -export default { - promises: InstitutionsAPI, - ...callbackifyAll(InstitutionsAPI), -} +export default InstitutionsAPI diff --git a/services/web/test/unit/src/Institutions/InstitutionsAPI.test.mjs b/services/web/test/unit/src/Institutions/InstitutionsAPI.test.mjs index 65d9f06124..689501c96e 100644 --- a/services/web/test/unit/src/Institutions/InstitutionsAPI.test.mjs +++ b/services/web/test/unit/src/Institutions/InstitutionsAPI.test.mjs @@ -18,7 +18,8 @@ describe('InstitutionsAPI', function () { ctx.settings = { apis: { v1: { url: 'v1.url', user: '', pass: '', timeout: 5000 } }, } - + ctx.request = sinon.stub() + ctx.fetchNothing = sinon.stub() ctx.ipMatcherNotification = { read: (ctx.markAsReadIpMatcher = sinon.stub().resolves()), } @@ -27,8 +28,12 @@ describe('InstitutionsAPI', function () { default: ctx.settings, })) + vi.doMock('requestretry', () => ({ + default: ctx.request, + })) + vi.doMock('@overleaf/fetch-utils', () => ({ - fetchNothing: (ctx.fetchNothing = sinon.stub()), + fetchNothing: ctx.fetchNothing, fetchJson: (ctx.fetchJson = sinon.stub()), })) @@ -69,18 +74,21 @@ describe('InstitutionsAPI', function () { it('get affiliations', async function (ctx) { ctx.institutionId = 123 const responseBody = ['123abc', '456def'] - ctx.fetchJson.resolves(responseBody) + ctx.request.yields(null, { statusCode: 200 }, responseBody) const body = await ctx.InstitutionsAPI.promises.getInstitutionAffiliations( ctx.institutionId ) - ctx.fetchJson.calledOnce.should.equal(true) + ctx.request.calledOnce.should.equal(true) + const requestOptions = ctx.request.lastCall.args[0] const expectedUrl = `v1.url/api/v2/institutions/${ctx.institutionId}/affiliations` - ctx.fetchJson.lastCall.args[0].should.equal(expectedUrl) - const requestOptions = ctx.fetchJson.lastCall.args[1] + requestOptions.url.should.equal(expectedUrl) requestOptions.method.should.equal('GET') - expect(requestOptions.json).not.to.exist + requestOptions.maxAttempts.should.exist + requestOptions.maxAttempts.should.not.equal(0) + requestOptions.retryDelay.should.exist + expect(requestOptions.body).not.to.exist body.should.equal(responseBody) }) @@ -109,17 +117,15 @@ describe('InstitutionsAPI', function () { max_confirmation_months: [], }, } - ctx.fetchJson.resolves(v1Result) + ctx.request.callsArgWith(1, null, { statusCode: 201 }, v1Result) await ctx.InstitutionsAPI.promises.getLicencesForAnalytics(lag, queryDate) - const expectedUrl = `v1.url/api/v2/institutions/institutions_licences` - ctx.fetchJson.lastCall.args[0].should.equal(expectedUrl) - const requestOptions = ctx.fetchJson.lastCall.args[1] - expect(requestOptions.json.query_date).to.equal(queryDate) - expect(requestOptions.json.lag).to.equal(lag) + const requestOptions = ctx.request.lastCall.args[0] + expect(requestOptions.body.query_date).to.equal(queryDate) + expect(requestOptions.body.lag).to.equal(lag) requestOptions.method.should.equal('GET') }) it('should handle errors', async function (ctx) { - ctx.fetchJson.throws({ response: { status: 500 } }) + ctx.request.callsArgWith(1, null, { statusCode: 500 }) let error try { @@ -146,17 +152,18 @@ describe('InstitutionsAPI', function () { }, }, ] - ctx.fetchJson.resolves(responseBody) + ctx.request.callsArgWith(1, null, { statusCode: 201 }, responseBody) const body = await ctx.InstitutionsAPI.promises.getUserAffiliations( ctx.stubbedUser._id ) - ctx.fetchJson.calledOnce.should.equal(true) + ctx.request.calledOnce.should.equal(true) + const requestOptions = ctx.request.lastCall.args[0] const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations` - ctx.fetchJson.lastCall.args[0].should.equal(expectedUrl) - const requestOptions = ctx.fetchJson.lastCall.args[1] + requestOptions.url.should.equal(expectedUrl) requestOptions.method.should.equal('GET') + requestOptions.maxAttempts.should.equal(3) ctx.Modules.promises.hooks.fire.should.have.been.called - expect(requestOptions.json).not.to.exist + expect(requestOptions.body).not.to.exist expect(body).to.deep.equal(responseBody) }) @@ -166,13 +173,12 @@ describe('InstitutionsAPI', function () { id: '123abc', foo: 'bar', institution: { - id: 'test-institution-id', commonsAccount: false, confirmed: true, }, }, ] - ctx.fetchJson.resolves(responseBody) + ctx.request.callsArgWith(1, null, { statusCode: 201 }, responseBody) const groupResponse = { _id: new ObjectId(), managedUsersEnabled: false, @@ -187,16 +193,17 @@ describe('InstitutionsAPI', function () { const body = await ctx.InstitutionsAPI.promises.getUserAffiliations( ctx.stubbedUser._id ) - ctx.fetchJson.calledOnce.should.equal(true) + ctx.request.calledOnce.should.equal(true) + const requestOptions = ctx.request.lastCall.args[0] const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations` - ctx.fetchJson.lastCall.args[0].should.equal(expectedUrl) - const requestOptions = ctx.fetchJson.lastCall.args[1] + requestOptions.url.should.equal(expectedUrl) requestOptions.method.should.equal('GET') + requestOptions.maxAttempts.should.equal(3) ctx.Modules.promises.hooks.fire.should.have.been.calledWith( 'getGroupWithDomainCaptureByV1Id', responseBody[0].institution.id ) - expect(requestOptions.json).not.to.exist + expect(requestOptions.body).not.to.exist expect(body).to.deep.equal([ { ...responseBody[0], @@ -218,18 +225,19 @@ describe('InstitutionsAPI', function () { }, }, ] - ctx.fetchJson.resolves(responseBody) + ctx.request.callsArgWith(1, null, { statusCode: 201 }, responseBody) const body = await ctx.InstitutionsAPI.promises.getUserAffiliations( ctx.stubbedUser._id ) - ctx.fetchJson.calledOnce.should.equal(true) + ctx.request.calledOnce.should.equal(true) + const requestOptions = ctx.request.lastCall.args[0] const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations` - ctx.fetchJson.lastCall.args[0].should.equal(expectedUrl) - const requestOptions = ctx.fetchJson.lastCall.args[1] + requestOptions.url.should.equal(expectedUrl) requestOptions.method.should.equal('GET') + requestOptions.maxAttempts.should.equal(3) ctx.Modules.promises.hooks.fire.should.not.have.been.called - expect(requestOptions.json).not.to.exist + expect(requestOptions.body).not.to.exist expect(body).to.deep.equal([ { ...responseBody[0], @@ -238,7 +246,8 @@ describe('InstitutionsAPI', function () { }) it('handle error', async function (ctx) { - ctx.fetchJson.throws({ response: { status: 503 } }) + const body = { errors: 'affiliation error message' } + ctx.request.callsArgWith(1, null, { statusCode: 503 }, body) let error try { @@ -264,17 +273,17 @@ describe('InstitutionsAPI', function () { describe('getUsersNeedingReconfirmationsLapsedProcessed', function () { it('get the list of users', async function (ctx) { - ctx.fetchJson.resolves({}) + ctx.fetchJson.resolves({ statusCode: 200 }) await ctx.InstitutionsAPI.promises.getUsersNeedingReconfirmationsLapsedProcessed() ctx.fetchJson.calledOnce.should.equal(true) + const requestOptions = ctx.fetchJson.lastCall.args[1] const expectedUrl = `v1.url/api/v2/institutions/need_reconfirmation_lapsed_processed` ctx.fetchJson.lastCall.args[0].should.equal(expectedUrl) - const requestOptions = ctx.fetchJson.lastCall.args[1] requestOptions.method.should.equal('GET') }) it('handle error', async function (ctx) { - ctx.fetchJson.throws({ response: { status: 500 } }) + ctx.fetchJson.throws({ info: { statusCode: 500 } }) await expect( ctx.InstitutionsAPI.promises.getUsersNeedingReconfirmationsLapsedProcessed() ).to.be.rejected @@ -283,7 +292,7 @@ describe('InstitutionsAPI', function () { describe('addAffiliation', function () { beforeEach(function (ctx) { - ctx.fetchNothing.resolves() + ctx.fetchNothing.resolves({ status: 201 }) }) it('add affiliation', async function (ctx) { @@ -300,9 +309,9 @@ describe('InstitutionsAPI', function () { affiliationOptions ) ctx.fetchNothing.calledOnce.should.equal(true) + const requestOptions = ctx.fetchNothing.lastCall.args[1] const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations` expect(ctx.fetchNothing.lastCall.args[0]).to.equal(expectedUrl) - const requestOptions = ctx.fetchNothing.lastCall.args[1] requestOptions.method.should.equal('POST') const { json } = requestOptions @@ -406,9 +415,9 @@ describe('InstitutionsAPI', function () { ctx.newEmail ) ctx.fetchNothing.calledOnce.should.equal(true) + const requestOptions = ctx.fetchNothing.lastCall.args[1] const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations/remove` ctx.fetchNothing.lastCall.args[0].should.equal(expectedUrl) - const requestOptions = ctx.fetchNothing.lastCall.args[1] requestOptions.method.should.equal('POST') expect(requestOptions.json).to.deep.equal({ email: ctx.newEmail }) }) @@ -433,17 +442,18 @@ describe('InstitutionsAPI', function () { describe('deleteAffiliations', function () { it('delete affiliations', async function (ctx) { - ctx.fetchNothing.resolves() + ctx.request.callsArgWith(1, null, { statusCode: 200 }) await ctx.InstitutionsAPI.promises.deleteAffiliations(ctx.stubbedUser._id) - ctx.fetchNothing.calledOnce.should.equal(true) + ctx.request.calledOnce.should.equal(true) + const requestOptions = ctx.request.lastCall.args[0] const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations` - ctx.fetchNothing.lastCall.args[0].should.equal(expectedUrl) - const requestOptions = ctx.fetchNothing.lastCall.args[1] + requestOptions.url.should.equal(expectedUrl) requestOptions.method.should.equal('DELETE') }) it('handle error', async function (ctx) { - ctx.fetchNothing.throws({ response: { status: 518 } }) + const body = { errors: 'affiliation error message' } + ctx.request.callsArgWith(1, null, { statusCode: 518 }, body) let error try { @@ -460,7 +470,7 @@ describe('InstitutionsAPI', function () { describe('endorseAffiliation', function () { beforeEach(function (ctx) { - ctx.fetchNothing.resolves() + ctx.request.callsArgWith(1, null, { statusCode: 204 }) }) it('endorse affiliation', async function (ctx) { @@ -470,17 +480,17 @@ describe('InstitutionsAPI', function () { 'Student', 'Physics' ) - ctx.fetchNothing.calledOnce.should.equal(true) + ctx.request.calledOnce.should.equal(true) + const requestOptions = ctx.request.lastCall.args[0] const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations/endorse` - ctx.fetchNothing.lastCall.args[0].should.equal(expectedUrl) - const requestOptions = ctx.fetchNothing.lastCall.args[1] + requestOptions.url.should.equal(expectedUrl) requestOptions.method.should.equal('POST') - const { json } = requestOptions - Object.keys(json).length.should.equal(3) - json.email.should.equal(ctx.newEmail) - json.role.should.equal('Student') - json.department.should.equal('Physics') + const { body } = requestOptions + Object.keys(body).length.should.equal(3) + body.email.should.equal(ctx.newEmail) + body.role.should.equal('Student') + body.department.should.equal('Physics') }) }) @@ -488,21 +498,21 @@ describe('InstitutionsAPI', function () { const users = ['abc123', 'def456'] it('sends the list of users', async function (ctx) { - ctx.fetchNothing.resolves() + ctx.request.callsArgWith(1, null, { statusCode: 200 }) await ctx.InstitutionsAPI.promises.sendUsersWithReconfirmationsLapsedProcessed( users ) - ctx.fetchNothing.calledOnce.should.equal(true) + ctx.request.calledOnce.should.equal(true) + const requestOptions = ctx.request.lastCall.args[0] const expectedUrl = 'v1.url/api/v2/institutions/reconfirmation_lapsed_processed' - ctx.fetchNothing.lastCall.args[0].should.equal(expectedUrl) - const requestOptions = ctx.fetchNothing.lastCall.args[1] + requestOptions.url.should.equal(expectedUrl) requestOptions.method.should.equal('POST') - expect(requestOptions.json).to.deep.equal({ users }) + expect(requestOptions.body).to.deep.equal({ users }) }) it('handle error', async function (ctx) { - ctx.fetchNothing.throws({ response: { status: 500 } }) + ctx.request.callsArgWith(1, null, { statusCode: 500 }) let error try {