mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-03 22:29:01 +02:00
93b7274ea6
GitOrigin-RevId: 03bd4db8cddc548706439edd7f6db0bc3e7ed9d3
252 lines
7.3 KiB
JavaScript
252 lines
7.3 KiB
JavaScript
import { vi, expect } from 'vitest'
|
|
import sinon from 'sinon'
|
|
import OError from '@overleaf/o-error'
|
|
import { ThirdPartyUserNotFoundError } from '../../../../app/src/Features/Errors/Errors.js'
|
|
const modulePath =
|
|
'../../../../app/src/Features/User/ThirdPartyIdentityManager.mjs'
|
|
|
|
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
|
|
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
|
|
)
|
|
|
|
describe('ThirdPartyIdentityManager', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.userId = 'a1b2c3'
|
|
ctx.user = {
|
|
_id: ctx.userId,
|
|
email: 'example@overleaf.com',
|
|
}
|
|
ctx.externalUserId = 'id789'
|
|
ctx.externalData = {}
|
|
ctx.auditLog = { initiatorId: ctx.userId, ipAddress: '0:0:0:0' }
|
|
|
|
vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({
|
|
default: (ctx.UserAuditLogHandler = {
|
|
promises: {
|
|
addEntry: sinon.stub().resolves(),
|
|
},
|
|
}),
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({
|
|
default: (ctx.EmailHandler = {
|
|
promises: {
|
|
sendEmail: sinon.stub().resolves(),
|
|
},
|
|
}),
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/models/User', () => ({
|
|
User: (ctx.User = {
|
|
findOneAndUpdate: sinon
|
|
.stub()
|
|
.returns({ exec: sinon.stub().resolves(ctx.user) }),
|
|
findOne: sinon.stub().returns({
|
|
exec: sinon.stub().resolves(undefined),
|
|
}),
|
|
}),
|
|
}))
|
|
|
|
vi.doMock('@overleaf/settings', () => ({
|
|
default: {
|
|
oauthProviders: {
|
|
google: {
|
|
name: 'Google',
|
|
},
|
|
orcid: {
|
|
name: 'ORCID',
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
ctx.ThirdPartyIdentityManager = (await import(modulePath)).default
|
|
})
|
|
describe('getUser', function () {
|
|
it('should throw an error when missing providerId or externalUserId', async function (ctx) {
|
|
await expect(
|
|
ctx.ThirdPartyIdentityManager.promises.getUser(undefined, undefined)
|
|
).to.be.rejectedWith(OError, `invalid SSO arguments`)
|
|
})
|
|
|
|
describe('when user linked', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.User.findOne.returns({
|
|
exec: sinon.stub().resolves(ctx.user),
|
|
})
|
|
})
|
|
|
|
it('should return the user', async function (ctx) {
|
|
ctx.User.findOne.returns({
|
|
exec: sinon.stub().resolves(ctx.user),
|
|
})
|
|
const user = await ctx.ThirdPartyIdentityManager.promises.getUser(
|
|
'google',
|
|
'an-id-linked'
|
|
)
|
|
expect(user).to.deep.equal(ctx.user)
|
|
})
|
|
})
|
|
it('should throw ThirdPartyUserNotFoundError when no user linked', async function (ctx) {
|
|
await expect(
|
|
ctx.ThirdPartyIdentityManager.promises.getUser(
|
|
'google',
|
|
'an-id-not-linked'
|
|
)
|
|
).to.be.rejectedWith(ThirdPartyUserNotFoundError)
|
|
})
|
|
})
|
|
describe('link', function () {
|
|
it('should send email alert', async function (ctx) {
|
|
await ctx.ThirdPartyIdentityManager.promises.link(
|
|
ctx.userId,
|
|
'google',
|
|
ctx.externalUserId,
|
|
ctx.externalData,
|
|
ctx.auditLog
|
|
)
|
|
const emailCall = ctx.EmailHandler.promises.sendEmail.getCall(0)
|
|
expect(emailCall.args[0]).to.equal('securityAlert')
|
|
expect(emailCall.args[1].actionDescribed).to.contain(
|
|
'a Google account was linked'
|
|
)
|
|
})
|
|
|
|
it('should update user audit log', async function (ctx) {
|
|
await ctx.ThirdPartyIdentityManager.promises.link(
|
|
ctx.userId,
|
|
'google',
|
|
ctx.externalUserId,
|
|
ctx.externalData,
|
|
ctx.auditLog
|
|
)
|
|
expect(
|
|
ctx.UserAuditLogHandler.promises.addEntry
|
|
).to.have.been.calledOnceWith(
|
|
ctx.userId,
|
|
'link-sso',
|
|
ctx.auditLog.initiatorId,
|
|
ctx.auditLog.ipAddress,
|
|
{
|
|
providerId: 'google',
|
|
}
|
|
)
|
|
})
|
|
describe('errors', function () {
|
|
const anError = new Error('oops')
|
|
|
|
it('should not unlink if the UserAuditLogHandler throws an error', async function (ctx) {
|
|
ctx.UserAuditLogHandler.promises.addEntry.throws(anError)
|
|
await expect(
|
|
ctx.ThirdPartyIdentityManager.promises.link(
|
|
ctx.userId,
|
|
'google',
|
|
ctx.externalUserId,
|
|
ctx.externalData,
|
|
ctx.auditLog
|
|
)
|
|
).to.be.rejectedWith(anError)
|
|
expect(ctx.User.findOneAndUpdate).to.not.have.been.called
|
|
})
|
|
|
|
describe('EmailHandler', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.EmailHandler.promises.sendEmail.rejects(anError)
|
|
})
|
|
it('should log but not return the error', async function (ctx) {
|
|
await expect(
|
|
ctx.ThirdPartyIdentityManager.promises.link(
|
|
ctx.userId,
|
|
'google',
|
|
ctx.externalUserId,
|
|
ctx.externalData,
|
|
ctx.auditLog
|
|
)
|
|
).to.be.fulfilled
|
|
expect(ctx.logger.error).toBeCalledWith(
|
|
{
|
|
err: anError,
|
|
userId: ctx.userId,
|
|
},
|
|
'could not send security alert email when new account linked'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('unlink', function () {
|
|
it('should send email alert', async function (ctx) {
|
|
await ctx.ThirdPartyIdentityManager.promises.unlink(
|
|
ctx.userId,
|
|
'orcid',
|
|
ctx.auditLog
|
|
)
|
|
const emailCall = ctx.EmailHandler.promises.sendEmail.getCall(0)
|
|
expect(emailCall.args[0]).to.equal('securityAlert')
|
|
expect(emailCall.args[1].actionDescribed).to.contain(
|
|
'an ORCID account was unlinked from'
|
|
)
|
|
})
|
|
it('should update user audit log', async function (ctx) {
|
|
await ctx.ThirdPartyIdentityManager.promises.unlink(
|
|
ctx.userId,
|
|
'orcid',
|
|
ctx.auditLog
|
|
)
|
|
expect(
|
|
ctx.UserAuditLogHandler.promises.addEntry
|
|
).to.have.been.calledOnceWith(
|
|
ctx.userId,
|
|
'unlink-sso',
|
|
ctx.auditLog.initiatorId,
|
|
ctx.auditLog.ipAddress,
|
|
{
|
|
providerId: 'orcid',
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('errors', function () {
|
|
const anError = new Error('oops')
|
|
|
|
it('should not unlink if the UserAuditLogHandler throws an error', async function (ctx) {
|
|
ctx.UserAuditLogHandler.promises.addEntry.throws(anError)
|
|
|
|
await expect(
|
|
ctx.ThirdPartyIdentityManager.promises.unlink(
|
|
ctx.userId,
|
|
'orcid',
|
|
ctx.auditLog
|
|
)
|
|
).to.be.rejectedWith(anError)
|
|
|
|
expect(ctx.User.findOneAndUpdate).to.not.have.been.called
|
|
})
|
|
|
|
describe('EmailHandler', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.EmailHandler.promises.sendEmail.rejects(anError)
|
|
})
|
|
it('should log but not return the error', async function (ctx) {
|
|
await expect(
|
|
ctx.ThirdPartyIdentityManager.promises.unlink(
|
|
ctx.userId,
|
|
'google',
|
|
ctx.auditLog
|
|
)
|
|
).to.be.fulfilled
|
|
|
|
expect(ctx.logger.error).toBeCalledWith(
|
|
{
|
|
err: anError,
|
|
userId: ctx.userId,
|
|
},
|
|
'could not send security alert email when account no longer linked'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|