| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- import { vi, expect } from 'vitest'
- import path from 'node:path'
- import sinon from 'sinon'
- import mongodb from 'mongodb-legacy'
- import Errors from '../../../../app/src/Features/Errors/Errors.js'
- const modulePath = path.join(
- import.meta.dirname,
- '../../../../app/src/Features/Institutions/InstitutionsAPI'
- )
- const { ObjectId } = mongodb
- vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
- vi.importActual('../../../../app/src/Features/Errors/Errors.js')
- )
- describe('InstitutionsAPI', function () {
- beforeEach(async function (ctx) {
- 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()),
- }
- vi.doMock('@overleaf/settings', () => ({
- default: ctx.settings,
- }))
- vi.doMock('requestretry', () => ({
- default: ctx.request,
- }))
- vi.doMock('@overleaf/fetch-utils', () => ({
- fetchNothing: ctx.fetchNothing,
- fetchJson: (ctx.fetchJson = sinon.stub()),
- }))
- vi.doMock(
- '../../../../app/src/Features/Notifications/NotificationsBuilder',
- () => ({
- default: {
- promises: {
- ipMatcherAffiliation: sinon
- .stub()
- .returns(ctx.ipMatcherNotification),
- },
- },
- })
- )
- vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
- default: (ctx.Modules = {
- promises: {
- hooks: {
- fire: sinon.stub(),
- },
- },
- }),
- }))
- ctx.InstitutionsAPI = (await import(modulePath)).default
- ctx.stubbedUser = {
- _id: '3131231',
- name: 'bob',
- email: 'hello@world.com',
- }
- ctx.newEmail = 'bob@bob.com'
- })
- describe('getInstitutionAffiliations', function () {
- it('get affiliations', async function (ctx) {
- ctx.institutionId = 123
- const responseBody = ['123abc', '456def']
- ctx.request.yields(null, { statusCode: 200 }, responseBody)
- const body =
- await ctx.InstitutionsAPI.promises.getInstitutionAffiliations(
- ctx.institutionId
- )
- ctx.request.calledOnce.should.equal(true)
- const requestOptions = ctx.request.lastCall.args[0]
- const expectedUrl = `v1.url/api/v2/institutions/${ctx.institutionId}/affiliations`
- requestOptions.url.should.equal(expectedUrl)
- 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
- body.should.equal(responseBody)
- })
- it('handle empty response', async function (ctx) {
- ctx.institutionId = 123
- ctx.settings.apis.v1.url = ''
- const body =
- await ctx.InstitutionsAPI.promises.getInstitutionAffiliations(
- ctx.institutionId
- )
- expect(body).to.be.a('Array')
- body.length.should.equal(0)
- })
- })
- describe('getLicencesForAnalytics', function () {
- const lag = 'daily'
- const queryDate = '2017-01-07:00:00.000Z'
- it('should send the request to v1', async function (ctx) {
- const v1Result = {
- lag: 'daily',
- date: queryDate,
- data: {
- user_counts: { total: [], new: [] },
- max_confirmation_months: [],
- },
- }
- ctx.request.callsArgWith(1, null, { statusCode: 201 }, 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)
- requestOptions.method.should.equal('GET')
- })
- it('should handle errors', async function (ctx) {
- ctx.request.callsArgWith(1, null, { statusCode: 500 })
- let error
- try {
- await ctx.InstitutionsAPI.promises.getLicencesForAnalytics(
- lag,
- queryDate
- )
- } catch (err) {
- error = err
- }
- expect(error).to.be.instanceOf(Errors.V1ConnectionError)
- })
- })
- describe('getUserAffiliations', function () {
- it('get affiliations with commons', async function (ctx) {
- const responseBody = [
- {
- foo: 'bar',
- institution: {
- commonsAccount: true,
- confirmed: true,
- },
- },
- ]
- ctx.request.callsArgWith(1, null, { statusCode: 201 }, responseBody)
- const body = await ctx.InstitutionsAPI.promises.getUserAffiliations(
- ctx.stubbedUser._id
- )
- ctx.request.calledOnce.should.equal(true)
- const requestOptions = ctx.request.lastCall.args[0]
- const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations`
- 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.body).not.to.exist
- expect(body).to.deep.equal(responseBody)
- })
- it('get affiliations with domain capture for groups', async function (ctx) {
- const responseBody = [
- {
- id: '123abc',
- foo: 'bar',
- institution: {
- commonsAccount: false,
- confirmed: true,
- },
- },
- ]
- ctx.request.callsArgWith(1, null, { statusCode: 201 }, responseBody)
- const groupResponse = {
- _id: new ObjectId(),
- managedUsersEnabled: false,
- domainCaptureEnabled: true,
- }
- ctx.Modules.promises.hooks.fire
- .withArgs(
- 'getGroupWithDomainCaptureByV1Id',
- responseBody[0].institution.id
- )
- .resolves([groupResponse])
- const body = await ctx.InstitutionsAPI.promises.getUserAffiliations(
- ctx.stubbedUser._id
- )
- ctx.request.calledOnce.should.equal(true)
- const requestOptions = ctx.request.lastCall.args[0]
- const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations`
- 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.body).not.to.exist
- expect(body).to.deep.equal([
- {
- ...responseBody[0],
- group: {
- ...groupResponse,
- },
- },
- ])
- })
- it('does not query for group with domain capture if domain is not confirmed', async function (ctx) {
- const responseBody = [
- {
- id: '123abc',
- foo: 'bar',
- institution: {
- commonsAccount: false,
- confirmed: false,
- },
- },
- ]
- ctx.request.callsArgWith(1, null, { statusCode: 201 }, responseBody)
- const body = await ctx.InstitutionsAPI.promises.getUserAffiliations(
- ctx.stubbedUser._id
- )
- ctx.request.calledOnce.should.equal(true)
- const requestOptions = ctx.request.lastCall.args[0]
- const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations`
- 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.body).not.to.exist
- expect(body).to.deep.equal([
- {
- ...responseBody[0],
- },
- ])
- })
- it('handle error', async function (ctx) {
- const body = { errors: 'affiliation error message' }
- ctx.request.callsArgWith(1, null, { statusCode: 503 }, body)
- let error
- try {
- await ctx.InstitutionsAPI.promises.getUserAffiliations(
- ctx.stubbedUser._id
- )
- } catch (err) {
- error = err
- }
- expect(error).to.be.instanceOf(Errors.V1ConnectionError)
- })
- it('handle empty response', async function (ctx) {
- ctx.settings.apis.v1.url = ''
- const body = await ctx.InstitutionsAPI.promises.getUserAffiliations(
- ctx.stubbedUser._id
- )
- expect(body).to.be.a('Array')
- body.length.should.equal(0)
- })
- })
- describe('getUsersNeedingReconfirmationsLapsedProcessed', function () {
- it('get the list of users', async function (ctx) {
- 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)
- requestOptions.method.should.equal('GET')
- })
- it('handle error', async function (ctx) {
- ctx.fetchJson.throws({ info: { statusCode: 500 } })
- await expect(
- ctx.InstitutionsAPI.promises.getUsersNeedingReconfirmationsLapsedProcessed()
- ).to.be.rejected
- })
- })
- describe('addAffiliation', function () {
- beforeEach(function (ctx) {
- ctx.fetchNothing.resolves({ status: 201 })
- })
- it('add affiliation', async function (ctx) {
- const affiliationOptions = {
- university: { id: 1 },
- department: 'Math',
- role: 'Prof',
- confirmedAt: new Date(),
- entitlement: true,
- }
- await ctx.InstitutionsAPI.promises.addAffiliation(
- ctx.stubbedUser._id,
- ctx.newEmail,
- 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)
- requestOptions.method.should.equal('POST')
- const { json } = requestOptions
- Object.keys(json).length.should.equal(7)
- expect(json).to.deep.equal(
- Object.assign(
- { email: ctx.newEmail, rejectIfBlocklisted: undefined },
- affiliationOptions
- )
- )
- ctx.markAsReadIpMatcher.calledOnce.should.equal(true)
- })
- it('handles 422 error', async function (ctx) {
- const messageFromApi = 'affiliation error message'
- const body = JSON.stringify({ errors: messageFromApi })
- ctx.fetchNothing.throws({ response: { status: 422 }, body })
- let error
- try {
- await ctx.InstitutionsAPI.promises.addAffiliation(
- ctx.stubbedUser._id,
- ctx.newEmail,
- {}
- )
- } catch (err) {
- error = err
- }
- expect(error).to.be.instanceOf(Errors.InvalidInstitutionalEmailError)
- expect(error).to.have.property('message', `422: ${messageFromApi}`)
- })
- it('handles 500 error', async function (ctx) {
- const body = { errors: 'affiliation error message' }
- ctx.fetchNothing.throws({ response: { status: 500 }, body })
- let error
- try {
- await ctx.InstitutionsAPI.promises.addAffiliation(
- ctx.stubbedUser._id,
- ctx.newEmail,
- {}
- )
- } catch (err) {
- error = err
- }
- expect(error).to.be.instanceOf(Errors.V1ConnectionError)
- expect(error.message).to.equal('error getting affiliations from v1')
- expect(error.info).to.eql({
- status: 500,
- body: { errors: 'affiliation error message' },
- })
- })
- it('uses default error message when no error body in response', async function (ctx) {
- ctx.fetchNothing.throws({ response: { status: 429 } })
- let error
- try {
- await ctx.InstitutionsAPI.promises.addAffiliation(
- ctx.stubbedUser._id,
- ctx.newEmail,
- {}
- )
- } catch (err) {
- error = err
- }
- expect(error).to.be.instanceOf(Error)
- expect(error).to.have.property(
- 'message',
- "Couldn't create affiliation: 429"
- )
- })
- it('does not try to mark IP matcher notifications as read if no university passed', async function (ctx) {
- const affiliationOptions = {
- confirmedAt: new Date(),
- }
- await ctx.InstitutionsAPI.promises.addAffiliation(
- ctx.stubbedUser._id,
- ctx.newEmail,
- affiliationOptions
- )
- expect(ctx.markAsReadIpMatcher.callCount).to.equal(0)
- })
- })
- describe('removeAffiliation', function () {
- beforeEach(function (ctx) {
- ctx.fetchNothing.throws({ response: { status: 404 } })
- })
- it('remove affiliation', async function (ctx) {
- await ctx.InstitutionsAPI.promises.removeAffiliation(
- ctx.stubbedUser._id,
- 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)
- requestOptions.method.should.equal('POST')
- expect(requestOptions.json).to.deep.equal({ email: ctx.newEmail })
- })
- it('handle error', async function (ctx) {
- ctx.fetchNothing.throws({ response: { status: 500 } })
- let error
- try {
- await ctx.InstitutionsAPI.promises.removeAffiliation(
- ctx.stubbedUser._id,
- ctx.newEmail
- )
- } catch (err) {
- error = err
- }
- expect(error).to.exist
- expect(error).to.have.property('message')
- })
- })
- describe('deleteAffiliations', function () {
- it('delete affiliations', async function (ctx) {
- ctx.request.callsArgWith(1, null, { statusCode: 200 })
- await ctx.InstitutionsAPI.promises.deleteAffiliations(ctx.stubbedUser._id)
- ctx.request.calledOnce.should.equal(true)
- const requestOptions = ctx.request.lastCall.args[0]
- const expectedUrl = `v1.url/api/v2/users/${ctx.stubbedUser._id}/affiliations`
- requestOptions.url.should.equal(expectedUrl)
- 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)
- let error
- try {
- await ctx.InstitutionsAPI.promises.deleteAffiliations(
- ctx.stubbedUser._id
- )
- } catch (err) {
- error = err
- }
- expect(error).to.be.instanceOf(Errors.V1ConnectionError)
- })
- })
- describe('endorseAffiliation', function () {
- beforeEach(function (ctx) {
- ctx.request.callsArgWith(1, null, { statusCode: 204 })
- })
- it('endorse affiliation', async function (ctx) {
- await ctx.InstitutionsAPI.promises.endorseAffiliation(
- ctx.stubbedUser._id,
- ctx.newEmail,
- 'Student',
- 'Physics'
- )
- 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`
- requestOptions.url.should.equal(expectedUrl)
- 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')
- })
- })
- describe('sendUsersWithReconfirmationsLapsedProcessed', function () {
- const users = ['abc123', 'def456']
- it('sends the list of users', async function (ctx) {
- ctx.request.callsArgWith(1, null, { statusCode: 200 })
- await ctx.InstitutionsAPI.promises.sendUsersWithReconfirmationsLapsedProcessed(
- users
- )
- ctx.request.calledOnce.should.equal(true)
- const requestOptions = ctx.request.lastCall.args[0]
- const expectedUrl =
- 'v1.url/api/v2/institutions/reconfirmation_lapsed_processed'
- requestOptions.url.should.equal(expectedUrl)
- requestOptions.method.should.equal('POST')
- expect(requestOptions.body).to.deep.equal({ users })
- })
- it('handle error', async function (ctx) {
- ctx.request.callsArgWith(1, null, { statusCode: 500 })
- let error
- try {
- await ctx.InstitutionsAPI.promises.sendUsersWithReconfirmationsLapsedProcessed(
- users
- )
- } catch (err) {
- error = err
- }
- expect(error).to.exist
- })
- })
- })
|