Files
Verso/services/web/test/unit/src/User/UserEmailsController.test.mjs
T
Andrew Rumble 0ca1888f76 Merge pull request #29374 from overleaf/renovate-major-vitest-monorepo
chore(deps): update vitest monorepo to v4 from ^3.2.4 (major)

GitOrigin-RevId: 553a8fefadd82598a9ff4089ce468b59791e2d43
2025-12-18 09:06:39 +00:00

970 lines
29 KiB
JavaScript

import { vi, assert, expect } from 'vitest'
import { setTimeout } from 'node:timers/promises'
import MockRequest from '../helpers/MockRequest.mjs'
import MockResponse from '../helpers/MockResponse.mjs'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
const modulePath = '../../../../app/src/Features/User/UserEmailsController.mjs'
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
)
describe('UserEmailsController', function () {
beforeEach(async function (ctx) {
ctx.req = new MockRequest(vi)
ctx.req.sessionID = Math.random().toString()
ctx.res = new MockResponse(vi)
ctx.next = vi.fn()
ctx.user = {
_id: 'mock-user-id',
email: 'example@overleaf.com',
emails: [],
}
ctx.UserGetter = {
getUser: vi.fn().mockImplementation((userId, projection, callback) => {
callback?.(null, ctx.user)
}),
getUserFullEmails: vi.fn(),
promises: {
ensureUniqueEmailAddress: vi.fn().mockResolvedValue(undefined),
getUser: vi.fn().mockResolvedValue(ctx.user),
getUserByAnyEmail: vi.fn(),
},
}
ctx.SessionManager = {
getSessionUser: vi.fn().mockReturnValue(ctx.user),
getLoggedInUserId: vi.fn().mockReturnValue(ctx.user._id),
setInSessionUser: vi.fn(),
}
ctx.Features = {
hasFeature: vi.fn(),
}
ctx.UserSessionsManager = {
promises: {
removeSessionsFromRedis: vi.fn().mockResolvedValue(undefined),
},
}
ctx.UserUpdater = {
addEmailAddress: vi.fn(),
updateV1AndSetDefaultEmailAddress: vi.fn(),
promises: {
addEmailAddress: vi.fn().mockResolvedValue(undefined),
confirmEmail: vi.fn().mockResolvedValue(undefined),
removeEmailAddress: vi.fn(),
setDefaultEmailAddress: vi.fn().mockResolvedValue(undefined),
},
}
ctx.EmailHelper = { parseEmail: vi.fn() }
ctx.endorseAffiliation = vi.fn((userId, email, role, dept, callback) =>
callback()
)
ctx.InstitutionsAPI = {
endorseAffiliation: ctx.endorseAffiliation,
}
ctx.HttpErrorHandler = { conflict: vi.fn() }
ctx.AnalyticsManager = {
recordEventForUserInBackground: vi.fn(),
}
ctx.UserAuditLogHandler = {
addEntry: vi.fn((userId, op, initiatorId, ip, info, callback) =>
callback()
),
promises: {
addEntry: vi.fn().mockResolvedValue(undefined),
},
}
ctx.rateLimiter = {
consume: vi.fn().mockResolvedValue(undefined),
}
ctx.RateLimiter = {
RateLimiter: vi.fn().mockImplementation(function RateLimiter() {
this.consume = ctx.rateLimiter.consume
}),
}
ctx.AuthenticationController = {
getRedirectFromSession: vi.fn().mockReturnValue(null),
}
vi.doMock(
'../../../../app/src/Features/Authentication/AuthenticationController',
() => ({
default: ctx.AuthenticationController,
})
)
vi.doMock(
'../../../../app/src/Features/Authentication/SessionManager',
() => ({
default: ctx.SessionManager,
})
)
vi.doMock('../../../../app/src/infrastructure/Features', () => ({
default: ctx.Features,
}))
vi.doMock('../../../../app/src/Features/User/UserSessionsManager', () => ({
default: ctx.UserSessionsManager,
}))
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({
default: ctx.UserUpdater,
}))
vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({
default: (ctx.EmailHandler = {
promises: {
sendEmail: vi.fn().mockResolvedValue(undefined),
},
}),
}))
vi.doMock('../../../../app/src/Features/Helpers/EmailHelper', () => ({
default: ctx.EmailHelper,
}))
vi.doMock(
'../../../../app/src/Features/User/UserEmailsConfirmationHandler',
() => ({
default: (ctx.UserEmailsConfirmationHandler = {
promises: {
sendConfirmationEmail: vi.fn().mockResolvedValue(undefined),
},
}),
})
)
vi.doMock(
'../../../../app/src/Features/Institutions/InstitutionsAPI',
() => ({
default: ctx.InstitutionsAPI,
})
)
vi.doMock('../../../../app/src/Features/Errors/HttpErrorHandler', () => ({
default: ctx.HttpErrorHandler,
}))
vi.doMock(
'../../../../app/src/Features/Analytics/AnalyticsManager',
() => ({
default: ctx.AnalyticsManager,
})
)
vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({
default: ctx.UserAuditLogHandler,
}))
vi.doMock(
'../../../../app/src/infrastructure/RateLimiter',
() => ctx.RateLimiter
)
ctx.UserEmailsController = (await import(modulePath)).default
})
describe('List', function () {
beforeEach(function () {})
it('lists emails', async function (ctx) {
expect.assertions(1)
const fullEmails = [{ some: 'data' }]
ctx.UserGetter.getUserFullEmails.mockImplementation(
(userId, callback) => {
callback(null, fullEmails)
}
)
await ctx.UserEmailsController.list(ctx.req, {
json: response => {
assert.deepEqual(response, fullEmails)
expect(ctx.UserGetter.getUserFullEmails).toHaveBeenCalledWith(
ctx.user._id,
expect.any(Function)
)
},
})
})
})
describe('addWithConfirmationCode', function () {
beforeEach(function (ctx) {
ctx.newEmail = 'new_email@baz.com'
ctx.req.body = {
email: ctx.newEmail,
}
ctx.EmailHelper.parseEmail.mockReturnValue(ctx.newEmail)
ctx.UserEmailsConfirmationHandler.promises.sendConfirmationCode = vi
.fn()
.mockResolvedValue({
confirmCode: '123456',
confirmCodeExpiresTimestamp: new Date(),
})
})
it('sends an email confirmation', async function (ctx) {
expect.assertions(2)
await ctx.UserEmailsController.addWithConfirmationCode(ctx.req, {
sendStatus: code => {
expect(code).to.equal(200)
expect(
ctx.UserEmailsConfirmationHandler.promises.sendConfirmationCode
).toHaveBeenCalledWith(ctx.newEmail, false)
},
})
})
it('handles email parse error', async function (ctx) {
expect.assertions(1)
ctx.EmailHelper.parseEmail.mockReturnValue(null)
await ctx.UserEmailsController.addWithConfirmationCode(ctx.req, {
sendStatus: code => {
expect(code).to.equal(422)
},
})
})
it('handles when the email already exists', async function (ctx) {
expect.assertions(1)
ctx.UserGetter.promises.ensureUniqueEmailAddress.mockRejectedValue(
new Errors.EmailExistsError()
)
await ctx.UserEmailsController.addWithConfirmationCode(ctx.req, {
status: code => {
expect(code).to.equal(409)
return { json: () => {} }
},
})
})
it('should fail to add new emails when the limit has been reached', async function (ctx) {
expect.assertions(2)
ctx.user.emails = []
for (let i = 0; i < 10; i++) {
ctx.user.emails.push({ email: `example${i}@overleaf.com` })
}
await ctx.UserEmailsController.addWithConfirmationCode(ctx.req, {
status: code => {
expect(code).to.equal(422)
return {
json: error => {
expect(error.message).to.equal('secondary email limit exceeded')
},
}
},
})
})
})
describe('checkNewSecondaryEmailConfirmationCode', function () {
beforeEach(function (ctx) {
ctx.newEmail = 'new_email@baz.com'
ctx.req.session.pendingSecondaryEmail = {
confirmCode: '123456',
email: ctx.newEmail,
confirmCodeExpiresTimestamp: new Date(Math.max),
}
})
describe('with a valid confirmation code', function () {
beforeEach(function (ctx) {
ctx.req.body = {
code: '123456',
}
})
it('adds the email', async function (ctx) {
expect.assertions(2)
await ctx.UserEmailsController.checkNewSecondaryEmailConfirmationCode(
ctx.req,
{
json: () => {
expect(
ctx.UserUpdater.promises.addEmailAddress
).toHaveBeenCalledWith(ctx.user._id, ctx.newEmail, undefined, {
initiatorId: 'mock-user-id',
ipAddress: '42.42.42.42',
})
expect(
ctx.UserUpdater.promises.confirmEmail
).toHaveBeenCalledWith(ctx.user._id, ctx.newEmail, undefined)
},
}
)
})
it('redirects to /project', async function (ctx) {
expect.assertions(1)
await ctx.UserEmailsController.checkNewSecondaryEmailConfirmationCode(
ctx.req,
{
json: ({ redir }) => {
expect(redir).to.equal('/project')
},
}
)
})
it('sends a security alert email', async function (ctx) {
expect.assertions(4)
ctx.req.session.pendingSecondaryEmail = {
confirmCode: '123456',
email: ctx.newEmail,
confirmCodeExpiresTimestamp: new Date(Math.max),
affiliationOptions: {},
}
ctx.req.body.code = '123456'
await ctx.UserEmailsController.checkNewSecondaryEmailConfirmationCode(
ctx.req,
{
json: vi.fn().mockResolvedValue(undefined),
}
)
const emailCall = ctx.EmailHandler.promises.sendEmail.mock.calls[0]
expect(emailCall[0]).to.equal('securityAlert')
expect(emailCall[1].to).to.equal(ctx.user.email)
expect(emailCall[1].actionDescribed).to.contain(
'a secondary email address'
)
expect(emailCall[1].message[0]).to.contain(ctx.newEmail)
})
})
describe('with an invalid confirmation code', function () {
beforeEach(function (ctx) {
ctx.req.body = {
code: '999999',
}
})
it('does not add the email', async function (ctx) {
expect.assertions(2)
await ctx.UserEmailsController.checkNewSecondaryEmailConfirmationCode(
ctx.req,
{
status: () => {
expect(
ctx.UserUpdater.promises.addEmailAddress
).not.toHaveBeenCalled()
expect(
ctx.UserUpdater.promises.confirmEmail
).not.toHaveBeenCalled()
return { json: ctx.next }
},
}
)
})
it('responds with a 403', async function (ctx) {
expect.assertions(1)
await ctx.UserEmailsController.checkNewSecondaryEmailConfirmationCode(
ctx.req,
{
status: code => {
expect(code).to.equal(403)
return { json: ctx.next }
},
}
)
})
})
})
describe('resendNewSecondaryEmailConfirmationCode', function () {
beforeEach(function (ctx) {
ctx.newEmail = 'new_email@baz.com'
ctx.req.session.pendingSecondaryEmail = {
confirmCode: '123456',
email: ctx.newEmail,
confirmCodeExpiresTimestamp: new Date(Math.max),
}
ctx.UserEmailsConfirmationHandler.promises.sendConfirmationCode = vi
.fn()
.mockResolvedValue({
confirmCode: '123456',
confirmCodeExpiresTimestamp: new Date(),
})
})
it('should send the email', async function (ctx) {
expect.assertions(2)
await ctx.UserEmailsController.resendNewSecondaryEmailConfirmationCode(
ctx.req,
{
status: code => {
expect(code).to.equal(200)
expect(
ctx.UserEmailsConfirmationHandler.promises.sendConfirmationCode
).toHaveBeenCalledWith(ctx.newEmail, false)
return { json: ctx.next }
},
}
)
})
})
describe('remove', function () {
beforeEach(function (ctx) {
ctx.email = 'email_to_remove@bar.com'
ctx.req.body.email = ctx.email
ctx.EmailHelper.parseEmail.mockReturnValue(ctx.email)
})
it('removes email', async function (ctx) {
expect.assertions(3)
const auditLog = {
initiatorId: ctx.user._id,
ipAddress: ctx.req.ip,
}
ctx.UserUpdater.promises.removeEmailAddress.mockResolvedValue(undefined)
await ctx.UserEmailsController.remove(ctx.req, {
sendStatus: code => {
expect(code).to.equal(200)
expect(ctx.EmailHelper.parseEmail).toHaveBeenCalledWith(ctx.email)
expect(
ctx.UserUpdater.promises.removeEmailAddress
).toHaveBeenCalledWith(ctx.user._id, ctx.email, auditLog)
},
})
})
it('handles email parse error', async function (ctx) {
expect.assertions(2)
ctx.EmailHelper.parseEmail.mockReturnValue(null)
await ctx.UserEmailsController.remove(ctx.req, {
sendStatus: code => {
expect(code).to.equal(422)
expect(
ctx.UserUpdater.promises.removeEmailAddress
).not.toHaveBeenCalled()
},
})
})
})
describe('setDefault', function () {
beforeEach(function (ctx) {
ctx.email = 'email_to_set_default@bar.com'
ctx.req.body.email = ctx.email
ctx.EmailHelper.parseEmail.mockReturnValue(ctx.email)
ctx.SessionManager.setInSessionUser.mockReturnValue(null)
})
it('sets default email', async function (ctx) {
expect.assertions(4)
await ctx.UserEmailsController.setDefault(ctx.req, {
sendStatus: code => {
expect(code).to.equal(200)
expect(ctx.EmailHelper.parseEmail).toHaveBeenCalledWith(ctx.email)
expect(ctx.SessionManager.setInSessionUser).toHaveBeenCalledWith(
ctx.req.session,
{
email: ctx.email,
}
)
expect(
ctx.UserUpdater.promises.setDefaultEmailAddress
).toHaveBeenCalledWith(
ctx.user._id,
ctx.email,
false,
{ initiatorId: 'mock-user-id', ipAddress: '42.42.42.42' },
true,
false
)
},
})
})
it('deletes unconfirmed primary if delete-unconfirmed-primary is set', async function (ctx) {
expect.assertions(1)
ctx.user.emails = [{ email: 'example@overleaf.com' }]
ctx.req.query['delete-unconfirmed-primary'] = ''
await ctx.UserEmailsController.setDefault(ctx.req, {
sendStatus: () => {
expect(
ctx.UserUpdater.promises.removeEmailAddress
).toHaveBeenCalledWith(ctx.user._id, 'example@overleaf.com', {
initiatorId: ctx.user._id,
ipAddress: ctx.req.ip,
extraInfo: {
info: 'removed unconfirmed email after setting new primary',
},
})
},
})
})
it('doesnt delete a confirmed primary', async function (ctx) {
expect.assertions(1)
ctx.user.emails = [
{ email: 'example@overleaf.com', confirmedAt: '2000-01-01' },
]
ctx.req.query['delete-unconfirmed-primary'] = ''
await ctx.UserEmailsController.setDefault(ctx.req, {
sendStatus: () => {
expect(
ctx.UserUpdater.promises.removeEmailAddress
).not.toHaveBeenCalled()
},
})
})
it('doesnt delete primary if delete-unconfirmed-primary is not set', async function (ctx) {
await ctx.UserEmailsController.setDefault(ctx.req, {
sendStatus: () => {
expect(
ctx.UserUpdater.promises.removeEmailAddress
).not.toHaveBeenCalled()
},
})
})
it('handles email parse error', async function (ctx) {
expect.assertions(2)
ctx.EmailHelper.parseEmail.mockReturnValue(null)
await ctx.UserEmailsController.setDefault(ctx.req, {
sendStatus: code => {
expect(code).to.equal(422)
expect(
ctx.UserUpdater.promises.setDefaultEmailAddress
).not.toHaveBeenCalled()
},
})
})
it('should reset the users other sessions', async function (ctx) {
await ctx.UserEmailsController.setDefault(ctx.req, ctx.res)
expect(
ctx.UserSessionsManager.promises.removeSessionsFromRedis
).toHaveBeenCalledWith(ctx.user, ctx.req.sessionID)
})
it('handles error from revoking sessions and returns 200', async function (ctx) {
const redisError = new Error('redis error')
ctx.UserSessionsManager.promises.removeSessionsFromRedis = vi
.fn()
.mockRejectedValue(redisError)
await ctx.UserEmailsController.setDefault(ctx.req, ctx.res)
expect(ctx.res.statusCode).to.equal(200)
// give revoke process time to run
await setTimeout(0)
expect(ctx.logger.warn).toHaveBeenCalledWith(
expect.objectContaining({ err: redisError }),
'failed revoking secondary sessions after changing default email'
)
})
})
describe('endorse', function () {
beforeEach(function (ctx) {
ctx.email = 'email_to_endorse@bar.com'
ctx.req.body.email = ctx.email
ctx.EmailHelper.parseEmail.mockReturnValue(ctx.email)
})
it('endorses affiliation', async function (ctx) {
expect.assertions(2)
ctx.req.body.role = 'Role'
ctx.req.body.department = 'Department'
await ctx.UserEmailsController.endorse(ctx.req, {
sendStatus: code => {
expect(code).to.equal(204)
expect(ctx.endorseAffiliation).toHaveBeenCalledWith(
ctx.user._id,
ctx.email,
'Role',
'Department',
expect.any(Function)
)
},
})
})
})
describe('confirm', function () {
beforeEach(function (ctx) {
ctx.UserEmailsConfirmationHandler.confirmEmailFromToken = vi
.fn()
.mockImplementation((req, token, callback) => {
callback(null, { userId: ctx.user._id, email: ctx.user.email })
})
ctx.token = 'mock-token'
ctx.req.body = { token: ctx.token }
ctx.req.ip = '0.0.0.0'
ctx.next = vi.fn()
ctx.res = new MockResponse(vi)
})
describe('successfully', function () {
beforeEach(function (ctx) {
ctx.UserEmailsController.confirm(ctx.req, ctx.res, ctx.next)
})
it('should confirm the email from the token', function (ctx) {
expect(
ctx.UserEmailsConfirmationHandler.confirmEmailFromToken
).toHaveBeenCalledWith(ctx.req, ctx.token, expect.any(Function))
})
it('should return a 200 status', function (ctx) {
expect(ctx.res.sendStatus).toHaveBeenCalledWith(200)
})
it('should log the confirmation to the audit log', function (ctx) {
expect(ctx.UserAuditLogHandler.addEntry).toHaveBeenCalledWith(
ctx.user._id,
'confirm-email',
ctx.user._id,
ctx.req.ip,
{
token: ctx.token.substring(0, 10),
email: ctx.user.email,
},
expect.any(Function)
)
})
})
describe('without a token', function () {
beforeEach(function (ctx) {
ctx.req.body.token = null
ctx.UserEmailsController.confirm(ctx.req, ctx.res, ctx.next)
})
it('should return a 422 status', function (ctx) {
expect(ctx.res.status).toHaveBeenCalledWith(422)
})
})
describe('when confirming fails', function () {
beforeEach(function (ctx) {
ctx.UserEmailsConfirmationHandler.confirmEmailFromToken = vi
.fn()
.mockImplementation((req, token, callback) => {
callback(new Errors.NotFoundError('not found'))
})
})
it('should return a 404 error code with a message', function (ctx) {
ctx.UserEmailsController.confirm(ctx.req, ctx.res, ctx.next)
expect(ctx.res.status).toHaveBeenCalledWith(404)
expect(ctx.res.json).toHaveBeenCalledWith({
message: ctx.req.i18n.translate('confirmation_token_invalid'),
})
})
})
})
describe('sendExistingEmailConfirmationCode', function () {
beforeEach(function (ctx) {
ctx.email = 'existing-email@example.com'
ctx.req.body.email = ctx.email
ctx.EmailHelper.parseEmail.mockReturnValue(ctx.email)
ctx.UserGetter.promises.getUserByAnyEmail.mockResolvedValue({
_id: ctx.user._id,
email: ctx.email,
})
ctx.UserEmailsConfirmationHandler.promises.sendConfirmationCode = vi
.fn()
.mockResolvedValue({
confirmCode: '123456',
confirmCodeExpiresTimestamp: new Date(),
})
})
it('should send confirmation code for existing email', async function (ctx) {
expect.assertions(2)
await ctx.UserEmailsController.sendExistingEmailConfirmationCode(
ctx.req,
{
sendStatus: code => {
expect(code).to.equal(204)
expect(
ctx.UserEmailsConfirmationHandler.promises.sendConfirmationCode
).toHaveBeenCalledWith(ctx.email, false)
},
}
)
})
it('should store confirmation code in session', async function (ctx) {
const confirmCode = '123456'
const confirmCodeExpiresTimestamp = new Date()
ctx.UserEmailsConfirmationHandler.promises.sendConfirmationCode.mockResolvedValue(
{
confirmCode,
confirmCodeExpiresTimestamp,
}
)
await ctx.UserEmailsController.sendExistingEmailConfirmationCode(
ctx.req,
{ sendStatus: vi.fn() }
)
expect(ctx.req.session.pendingExistingEmail).to.deep.equal({
email: ctx.email,
confirmCode,
confirmCodeExpiresTimestamp,
affiliationOptions: undefined,
})
})
it('should handle invalid email', async function (ctx) {
expect.assertions(2)
ctx.EmailHelper.parseEmail.mockReturnValue(null)
await ctx.UserEmailsController.sendExistingEmailConfirmationCode(
ctx.req,
{
sendStatus: code => {
expect(code).to.equal(400)
expect(
ctx.UserEmailsConfirmationHandler.promises.sendConfirmationCode
).not.toHaveBeenCalled()
},
}
)
})
it('should handle email not belonging to user', async function (ctx) {
expect.assertions(2)
ctx.UserGetter.promises.getUserByAnyEmail.mockResolvedValue({
_id: 'another-user-id',
})
await ctx.UserEmailsController.sendExistingEmailConfirmationCode(
ctx.req,
{
sendStatus: code => {
expect(code).to.equal(422)
expect(
ctx.UserEmailsConfirmationHandler.promises.sendConfirmationCode
).not.toHaveBeenCalled()
},
}
)
})
})
describe('checkExistingEmailConfirmationCode', function () {
beforeEach(function (ctx) {
ctx.email = 'existing-email@example.com'
ctx.req.session.pendingExistingEmail = {
confirmCode: '123456',
email: ctx.email,
confirmCodeExpiresTimestamp: new Date(Math.max),
}
ctx.UserUpdater.promises.confirmEmail.mockResolvedValue(undefined)
ctx.res = new MockResponse(vi)
})
describe('with a valid confirmation code', function () {
beforeEach(function (ctx) {
ctx.req.body = { code: '123456' }
})
it('confirms the email', async function (ctx) {
const mockRes = new MockResponse(vi)
await ctx.UserEmailsController.checkExistingEmailConfirmationCode(
ctx.req,
mockRes
)
expect(ctx.UserUpdater.promises.confirmEmail).toHaveBeenCalledWith(
ctx.user._id,
ctx.email,
undefined
)
})
it('adds audit log entry', async function (ctx) {
await ctx.UserEmailsController.checkExistingEmailConfirmationCode(
ctx.req,
{ json: vi.fn() }
)
expect(ctx.UserAuditLogHandler.promises.addEntry).toHaveBeenCalledWith(
ctx.user._id,
'confirm-email-via-code',
ctx.user._id,
ctx.req.ip,
{ email: ctx.email }
)
})
it('records analytics event', async function (ctx) {
await ctx.UserEmailsController.checkExistingEmailConfirmationCode(
ctx.req,
{ json: vi.fn() }
)
expect(
ctx.AnalyticsManager.recordEventForUserInBackground
).toHaveBeenCalledWith(ctx.user._id, 'email-verified', {
provider: 'email',
verification_type: 'token',
isPrimary: ctx.user.email === ctx.email,
})
})
it('removes pendingExistingEmail from session', async function (ctx) {
await ctx.UserEmailsController.checkExistingEmailConfirmationCode(
ctx.req,
{ json: vi.fn() }
)
expect(ctx.req.session.pendingExistingEmail).to.be.undefined
})
})
describe('with an invalid confirmation code', function () {
beforeEach(function (ctx) {
ctx.req.body = { code: '999999' }
})
it('does not confirm the email', async function (ctx) {
expect.assertions(1)
await ctx.UserEmailsController.checkExistingEmailConfirmationCode(
ctx.req,
{
status: () => {
expect(
ctx.UserUpdater.promises.confirmEmail
).not.toHaveBeenCalled()
return { json: ctx.next }
},
}
)
})
it('responds with a 403', async function (ctx) {
expect.assertions(1)
await ctx.UserEmailsController.checkExistingEmailConfirmationCode(
ctx.req,
{
status: code => {
expect(code).to.equal(403)
return { json: ctx.next }
},
}
)
})
})
describe('with an expired confirmation code', function () {
beforeEach(function (ctx) {
ctx.req.session.pendingExistingEmail.confirmCodeExpiresTimestamp =
new Date(0)
ctx.req.body = { code: '123456' }
})
it('responds with a 403', async function (ctx) {
expect.assertions(1)
await ctx.UserEmailsController.checkExistingEmailConfirmationCode(
ctx.req,
{
status: code => {
expect(code).to.equal(403)
return { json: ctx.next }
},
}
)
})
})
})
describe('resendExistingSecondaryEmailConfirmationCode', function () {
beforeEach(function (ctx) {
ctx.email = 'existing-email@example.com'
ctx.req.session.pendingExistingEmail = {
confirmCode: '123456',
email: ctx.email,
confirmCodeExpiresTimestamp: new Date(Math.max),
}
ctx.res.status = vi.fn().mockReturnValue({ json: vi.fn() })
ctx.UserEmailsConfirmationHandler.promises.sendConfirmationCode = vi
.fn()
.mockResolvedValue({
confirmCode: '654321',
confirmCodeExpiresTimestamp: new Date(),
})
})
it('should resend confirmation code', async function (ctx) {
expect.assertions(2)
await ctx.UserEmailsController.resendExistingSecondaryEmailConfirmationCode(
ctx.req,
{
status: code => {
expect(code).to.equal(200)
expect(
ctx.UserEmailsConfirmationHandler.promises.sendConfirmationCode
).toHaveBeenCalledWith(ctx.email, false)
return { json: vi.fn() }
},
}
)
})
it('should update session with new code', async function (ctx) {
const newCode = '654321'
const newExpiryTime = new Date()
ctx.UserEmailsConfirmationHandler.promises.sendConfirmationCode.mockResolvedValue(
{
confirmCode: newCode,
confirmCodeExpiresTimestamp: newExpiryTime,
}
)
await ctx.UserEmailsController.resendExistingSecondaryEmailConfirmationCode(
ctx.req,
{ status: () => ({ json: vi.fn() }) }
)
expect(ctx.req.session.pendingExistingEmail.confirmCode).to.equal(newCode)
expect(
ctx.req.session.pendingExistingEmail.confirmCodeExpiresTimestamp
).to.equal(newExpiryTime)
})
it('should add audit log entry', async function (ctx) {
await ctx.UserEmailsController.resendExistingSecondaryEmailConfirmationCode(
ctx.req,
{ status: () => ({ json: vi.fn() }) }
)
expect(ctx.UserAuditLogHandler.promises.addEntry).toHaveBeenCalledWith(
ctx.user._id,
'resend-confirm-email-code',
ctx.user._id,
ctx.req.ip,
{ email: ctx.email }
)
})
it('should handle rate limiting', async function (ctx) {
expect.assertions(1)
ctx.rateLimiter.consume.mockRejectedValue({ remainingPoints: 0 })
await ctx.UserEmailsController.resendExistingSecondaryEmailConfirmationCode(
ctx.req,
{
status: code => {
expect(code).to.equal(429)
return { json: vi.fn() }
},
}
)
})
})
})