diff --git a/services/web/app/src/Features/Institutions/InstitutionsAPI.mjs b/services/web/app/src/Features/Institutions/InstitutionsAPI.mjs index c938783154..5d10d065a4 100644 --- a/services/web/app/src/Features/Institutions/InstitutionsAPI.mjs +++ b/services/web/app/src/Features/Institutions/InstitutionsAPI.mjs @@ -1,9 +1,7 @@ -import { callbackify } from 'node:util' import OError from '@overleaf/o-error' import logger from '@overleaf/logger' import settings from '@overleaf/settings' -import request from 'requestretry' -import { promisify, promiseMapWithLimit } from '@overleaf/promise-utils' +import { promiseMapWithLimit, callbackifyAll } from '@overleaf/promise-utils' import NotificationsBuilder from '../Notifications/NotificationsBuilder.mjs' import { V1ConnectionError, @@ -16,7 +14,7 @@ function _makeRequestOptions(options) { const requestOptions = { method: options.method, basicAuth: { user: settings.apis.v1.user, password: settings.apis.v1.pass }, - signal: AbortSignal.timeout(settings.apis.v1.timeout), + signal: AbortSignal.timeout(options.timeout ?? settings.apis.v1.timeout), } if (options.body) { @@ -27,7 +25,7 @@ function _makeRequestOptions(options) { } function _responseErrorHandling(options, error) { - const status = error.response.status + const status = error.response?.status if (status >= 500) { throw new V1ConnectionError({ @@ -100,94 +98,77 @@ async function _affiliationRequestFetchNothing404Ok(options) { } } -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 getInstitutionAffiliations(institutionId) { + const json = await _affiliationRequestFetchJson({ + method: 'GET', + path: `/api/v2/institutions/${institutionId.toString()}/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 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 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 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 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 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 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, []) - } +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 = [] + 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 (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), - } - } + if (group) { + affiliation.group = { + _id: group._id, + managedUsersEnabled: Boolean(group.managedUsersEnabled), + domainCaptureEnabled: Boolean(group.domainCaptureEnabled), } - - affiliations.push(affiliation) - }) + } } - - callback(null, affiliations) - } - ) + affiliations.push(affiliation) + }) + } + return affiliations } async function getUsersNeedingReconfirmationsLapsedProcessed() { @@ -255,65 +236,50 @@ async function removeAffiliation(userId, email) { }) } -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 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 deleteAffiliations(userId, callback) { - makeAffiliationRequest( - { - method: 'DELETE', - path: `/api/v2/users/${userId.toString()}/affiliations`, - defaultErrorMessage: "Couldn't delete affiliations", - }, - callback - ) +async function deleteAffiliations(userId) { + await _affiliationRequestFetchNothing({ + method: 'DELETE', + path: `/api/v2/users/${userId.toString()}/affiliations`, + defaultErrorMessage: "Couldn't delete affiliations", + }) } -function addEntitlement(userId, email, callback) { - makeAffiliationRequest( - { - method: 'POST', - path: `/api/v2/users/${userId}/affiliations/add_entitlement`, - body: { email }, - defaultErrorMessage: "Couldn't add entitlement", - }, - 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 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 removeEntitlement(userId, email) { + await _affiliationRequestFetchNothing404Ok({ + method: 'POST', + path: `/api/v2/users/${userId}/affiliations/remove_entitlement`, + body: { email }, + defaultErrorMessage: "Couldn't remove entitlement", + }) } -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 sendUsersWithReconfirmationsLapsedProcessed(users) { + await _affiliationRequestFetchNothing({ + method: 'POST', + path: '/api/v2/institutions/reconfirmation_lapsed_processed', + body: { users }, + defaultErrorMessage: 'Could not update reconfirmation_lapsed_processed_at', + }) } async function verifyDomainMatchesDomainMatcher(domain, institutionId) { @@ -327,120 +293,22 @@ 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: promisify(InstitutionsAPI.endorseAffiliation), - deleteAffiliations: promisify(InstitutionsAPI.deleteAffiliations), - addEntitlement: promisify(InstitutionsAPI.addEntitlement), - removeEntitlement: promisify(InstitutionsAPI.removeEntitlement), - sendUsersWithReconfirmationsLapsedProcessed: promisify( - InstitutionsAPI.sendUsersWithReconfirmationsLapsedProcessed - ), + endorseAffiliation, + deleteAffiliations, + addEntitlement, + removeEntitlement, + sendUsersWithReconfirmationsLapsedProcessed, verifyDomainMatchesDomainMatcher, } -export default InstitutionsAPI +export default { + promises: InstitutionsAPI, + ...callbackifyAll(InstitutionsAPI), +} diff --git a/services/web/test/unit/src/Institutions/InstitutionsAPI.test.mjs b/services/web/test/unit/src/Institutions/InstitutionsAPI.test.mjs index 689501c96e..65d9f06124 100644 --- a/services/web/test/unit/src/Institutions/InstitutionsAPI.test.mjs +++ b/services/web/test/unit/src/Institutions/InstitutionsAPI.test.mjs @@ -18,8 +18,7 @@ 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()), } @@ -28,12 +27,8 @@ describe('InstitutionsAPI', function () { default: ctx.settings, })) - vi.doMock('requestretry', () => ({ - default: ctx.request, - })) - vi.doMock('@overleaf/fetch-utils', () => ({ - fetchNothing: ctx.fetchNothing, + fetchNothing: (ctx.fetchNothing = sinon.stub()), fetchJson: (ctx.fetchJson = sinon.stub()), })) @@ -74,21 +69,18 @@ describe('InstitutionsAPI', function () { it('get affiliations', async function (ctx) { ctx.institutionId = 123 const responseBody = ['123abc', '456def'] - ctx.request.yields(null, { statusCode: 200 }, responseBody) + ctx.fetchJson.resolves(responseBody) const body = await ctx.InstitutionsAPI.promises.getInstitutionAffiliations( ctx.institutionId ) - ctx.request.calledOnce.should.equal(true) - const requestOptions = ctx.request.lastCall.args[0] + ctx.fetchJson.calledOnce.should.equal(true) const expectedUrl = `v1.url/api/v2/institutions/${ctx.institutionId}/affiliations` - requestOptions.url.should.equal(expectedUrl) + ctx.fetchJson.lastCall.args[0].should.equal(expectedUrl) + const requestOptions = ctx.fetchJson.lastCall.args[1] requestOptions.method.should.equal('GET') - requestOptions.maxAttempts.should.exist - requestOptions.maxAttempts.should.not.equal(0) - requestOptions.retryDelay.should.exist - expect(requestOptions.body).not.to.exist + expect(requestOptions.json).not.to.exist body.should.equal(responseBody) }) @@ -117,15 +109,17 @@ describe('InstitutionsAPI', function () { max_confirmation_months: [], }, } - ctx.request.callsArgWith(1, null, { statusCode: 201 }, v1Result) + ctx.fetchJson.resolves(v1Result) await ctx.InstitutionsAPI.promises.getLicencesForAnalytics(lag, queryDate) - const requestOptions = ctx.request.lastCall.args[0] - expect(requestOptions.body.query_date).to.equal(queryDate) - expect(requestOptions.body.lag).to.equal(lag) + 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) requestOptions.method.should.equal('GET') }) it('should handle errors', async function (ctx) { - ctx.request.callsArgWith(1, null, { statusCode: 500 }) + ctx.fetchJson.throws({ response: { status: 500 } }) let error try { @@ -152,18 +146,17 @@ describe('InstitutionsAPI', function () { }, }, ] - ctx.request.callsArgWith(1, null, { statusCode: 201 }, responseBody) + ctx.fetchJson.resolves(responseBody) const body = await ctx.InstitutionsAPI.promises.getUserAffiliations( ctx.stubbedUser._id ) - ctx.request.calledOnce.should.equal(true) - const requestOptions = ctx.request.lastCall.args[0] + ctx.fetchJson.calledOnce.should.equal(true) const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations` - requestOptions.url.should.equal(expectedUrl) + ctx.fetchJson.lastCall.args[0].should.equal(expectedUrl) + const requestOptions = ctx.fetchJson.lastCall.args[1] requestOptions.method.should.equal('GET') - requestOptions.maxAttempts.should.equal(3) ctx.Modules.promises.hooks.fire.should.have.been.called - expect(requestOptions.body).not.to.exist + expect(requestOptions.json).not.to.exist expect(body).to.deep.equal(responseBody) }) @@ -173,12 +166,13 @@ describe('InstitutionsAPI', function () { id: '123abc', foo: 'bar', institution: { + id: 'test-institution-id', commonsAccount: false, confirmed: true, }, }, ] - ctx.request.callsArgWith(1, null, { statusCode: 201 }, responseBody) + ctx.fetchJson.resolves(responseBody) const groupResponse = { _id: new ObjectId(), managedUsersEnabled: false, @@ -193,17 +187,16 @@ describe('InstitutionsAPI', function () { const body = await ctx.InstitutionsAPI.promises.getUserAffiliations( ctx.stubbedUser._id ) - ctx.request.calledOnce.should.equal(true) - const requestOptions = ctx.request.lastCall.args[0] + ctx.fetchJson.calledOnce.should.equal(true) const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations` - requestOptions.url.should.equal(expectedUrl) + ctx.fetchJson.lastCall.args[0].should.equal(expectedUrl) + const requestOptions = ctx.fetchJson.lastCall.args[1] 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.body).not.to.exist + expect(requestOptions.json).not.to.exist expect(body).to.deep.equal([ { ...responseBody[0], @@ -225,19 +218,18 @@ describe('InstitutionsAPI', function () { }, }, ] - ctx.request.callsArgWith(1, null, { statusCode: 201 }, responseBody) + ctx.fetchJson.resolves(responseBody) const body = await ctx.InstitutionsAPI.promises.getUserAffiliations( ctx.stubbedUser._id ) - ctx.request.calledOnce.should.equal(true) - const requestOptions = ctx.request.lastCall.args[0] + ctx.fetchJson.calledOnce.should.equal(true) const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations` - requestOptions.url.should.equal(expectedUrl) + ctx.fetchJson.lastCall.args[0].should.equal(expectedUrl) + const requestOptions = ctx.fetchJson.lastCall.args[1] requestOptions.method.should.equal('GET') - requestOptions.maxAttempts.should.equal(3) ctx.Modules.promises.hooks.fire.should.not.have.been.called - expect(requestOptions.body).not.to.exist + expect(requestOptions.json).not.to.exist expect(body).to.deep.equal([ { ...responseBody[0], @@ -246,8 +238,7 @@ describe('InstitutionsAPI', function () { }) it('handle error', async function (ctx) { - const body = { errors: 'affiliation error message' } - ctx.request.callsArgWith(1, null, { statusCode: 503 }, body) + ctx.fetchJson.throws({ response: { status: 503 } }) let error try { @@ -273,17 +264,17 @@ describe('InstitutionsAPI', function () { describe('getUsersNeedingReconfirmationsLapsedProcessed', function () { it('get the list of users', async function (ctx) { - ctx.fetchJson.resolves({ statusCode: 200 }) + ctx.fetchJson.resolves({}) 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({ info: { statusCode: 500 } }) + ctx.fetchJson.throws({ response: { status: 500 } }) await expect( ctx.InstitutionsAPI.promises.getUsersNeedingReconfirmationsLapsedProcessed() ).to.be.rejected @@ -292,7 +283,7 @@ describe('InstitutionsAPI', function () { describe('addAffiliation', function () { beforeEach(function (ctx) { - ctx.fetchNothing.resolves({ status: 201 }) + ctx.fetchNothing.resolves() }) it('add affiliation', async function (ctx) { @@ -309,9 +300,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 @@ -415,9 +406,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 }) }) @@ -442,18 +433,17 @@ describe('InstitutionsAPI', function () { describe('deleteAffiliations', function () { it('delete affiliations', async function (ctx) { - ctx.request.callsArgWith(1, null, { statusCode: 200 }) + ctx.fetchNothing.resolves() await ctx.InstitutionsAPI.promises.deleteAffiliations(ctx.stubbedUser._id) - ctx.request.calledOnce.should.equal(true) - const requestOptions = ctx.request.lastCall.args[0] + ctx.fetchNothing.calledOnce.should.equal(true) const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations` - requestOptions.url.should.equal(expectedUrl) + ctx.fetchNothing.lastCall.args[0].should.equal(expectedUrl) + const requestOptions = ctx.fetchNothing.lastCall.args[1] requestOptions.method.should.equal('DELETE') }) it('handle error', async function (ctx) { - const body = { errors: 'affiliation error message' } - ctx.request.callsArgWith(1, null, { statusCode: 518 }, body) + ctx.fetchNothing.throws({ response: { status: 518 } }) let error try { @@ -470,7 +460,7 @@ describe('InstitutionsAPI', function () { describe('endorseAffiliation', function () { beforeEach(function (ctx) { - ctx.request.callsArgWith(1, null, { statusCode: 204 }) + ctx.fetchNothing.resolves() }) it('endorse affiliation', async function (ctx) { @@ -480,17 +470,17 @@ describe('InstitutionsAPI', function () { 'Student', 'Physics' ) - ctx.request.calledOnce.should.equal(true) - const requestOptions = ctx.request.lastCall.args[0] + ctx.fetchNothing.calledOnce.should.equal(true) const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations/endorse` - requestOptions.url.should.equal(expectedUrl) + ctx.fetchNothing.lastCall.args[0].should.equal(expectedUrl) + const requestOptions = ctx.fetchNothing.lastCall.args[1] requestOptions.method.should.equal('POST') - 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') + 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') }) }) @@ -498,21 +488,21 @@ describe('InstitutionsAPI', function () { const users = ['abc123', 'def456'] it('sends the list of users', async function (ctx) { - ctx.request.callsArgWith(1, null, { statusCode: 200 }) + ctx.fetchNothing.resolves() await ctx.InstitutionsAPI.promises.sendUsersWithReconfirmationsLapsedProcessed( users ) - ctx.request.calledOnce.should.equal(true) - const requestOptions = ctx.request.lastCall.args[0] + ctx.fetchNothing.calledOnce.should.equal(true) const expectedUrl = 'v1.url/api/v2/institutions/reconfirmation_lapsed_processed' - requestOptions.url.should.equal(expectedUrl) + ctx.fetchNothing.lastCall.args[0].should.equal(expectedUrl) + const requestOptions = ctx.fetchNothing.lastCall.args[1] requestOptions.method.should.equal('POST') - expect(requestOptions.body).to.deep.equal({ users }) + expect(requestOptions.json).to.deep.equal({ users }) }) it('handle error', async function (ctx) { - ctx.request.callsArgWith(1, null, { statusCode: 500 }) + ctx.fetchNothing.throws({ response: { status: 500 } }) let error try {