Add helper functions for creating change events

GitOrigin-RevId: 26a4cbc8e322c52e12cd3eb7f891d9914cefc70d
This commit is contained in:
Andrew Rumble
2025-08-22 15:42:50 +01:00
committed by Copybot
parent ae504e8af5
commit cfbfcbc5db
2 changed files with 390 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
// @ts-check
const UserGetter = require('../User/UserGetter')
const { registerEmailChange } = require('./AnalyticsManager')
/**
* @typedef {object} EmailData
* @property {string} email
* @property {Date} createdAt
* @property {Date} confirmedAt
* @property {boolean} default
*/
/**
* @typedef {object} EventData
* @property {Date} [emailCreatedAt] ISO string of when the email was created
* @property {Date} [emailConfirmedAt] ISO string of when the email was confirmed
* @property {Date} [emailDeletedAt] ISO string of when the email was deleted
* @property {boolean} [isPrimary] Whether the email is the primary email
*/
/**
* @typedef {import('./types').EmailChangePayload} EmailChangePayload
*/
/**
*
* @param {string} userId
* @param {string} email
* @param {EventData} eventData
* @returns {Promise<void>}
*/
async function registerEmailUpdate(userId, email, eventData = {}) {
const emailChangeEvent = await makeEmailChangeEvent(userId, email, eventData)
registerEmailChange({
...emailChangeEvent,
action: 'updated',
})
}
/**
*
* @param {string} userId
* @param {string} email
* @param {EventData} eventData
* @returns {Promise<Omit<EmailChangePayload, 'action'>>}
*/
async function makeEmailChangeEvent(userId, email, eventData) {
const userEmails = await UserGetter.promises.getUserFullEmails(userId)
const emailData = userEmails.find(userEmail => userEmail.email === email)
const filledEventData = fillMissingEventData(eventData, emailData)
return {
userId,
email,
createdAt: new Date().toISOString(),
emailCreatedAt: filledEventData?.emailCreatedAt?.toISOString() ?? null,
emailConfirmedAt: filledEventData?.emailConfirmedAt?.toISOString() ?? null,
emailDeletedAt: filledEventData?.emailDeletedAt?.toISOString() ?? null,
isPrimary: filledEventData?.isPrimary ?? false,
}
}
/**
*
* @param {string} userId
* @param {string} email
* @param {EventData} eventData
* @returns {Promise<void>}
*/
async function registerEmailCreation(userId, email, eventData = {}) {
const emailChangeEvent = await makeEmailChangeEvent(userId, email, eventData)
registerEmailChange({
...emailChangeEvent,
action: 'created',
})
}
/**
*
* @param {string} userId
* @param {string} email
* @param {EventData} eventData
* @returns {Promise<void>}
*/
async function registerEmailDeletion(userId, email, eventData = {}) {
const emailChangeEvent = await makeEmailChangeEvent(userId, email, eventData)
registerEmailChange({
...emailChangeEvent,
action: 'deleted',
})
}
/**
*
* @param {EventData} eventData
* @param {EmailData | null} emailData
* @return {EventData}
*/
function fillMissingEventData(eventData, emailData) {
if (emailData) {
if (!eventData.emailCreatedAt) {
eventData.emailCreatedAt = emailData.createdAt
}
if (!eventData.emailConfirmedAt && emailData.confirmedAt) {
eventData.emailConfirmedAt = emailData.confirmedAt
}
if (eventData.isPrimary === undefined) {
eventData.isPrimary = emailData.default
}
}
return eventData
}
module.exports = {
registerEmailUpdate,
registerEmailCreation,
registerEmailDeletion,
}

View File

@@ -0,0 +1,268 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
describe('EmailChangeHelper', function () {
let AnalyticsManager
let UserGetter
let EmailChangeHelpers
const email = 'test@example.com'
const userId = '507f1f77bcf86cd799439011'
beforeEach(function () {
UserGetter = {
promises: {
getUserFullEmails: sinon.stub().resolves([]),
},
}
AnalyticsManager = {
registerEmailChange: sinon.stub(),
}
EmailChangeHelpers = SandboxedModule.require(
'../../../../app/src/Features/Analytics/EmailChangeHelper',
{
requires: {
'../User/UserGetter': UserGetter,
'./AnalyticsManager': AnalyticsManager,
},
}
)
})
describe('registerEmailUpdate', function () {
describe('when the email cannot be matched', function () {
beforeEach(function () {
UserGetter.promises.getUserFullEmails.resolves([
{
email: 'test2@example.com',
reversedHostname: 'moc.elpmaxe',
createdAt: new Date('2023-01-01T00:00:00'),
confirmedAt: new Date('2023-02-01T00:00:00'),
default: false,
},
])
})
it('calls registerEmailChange with the passed event data', async function () {
const eventData = {
emailCreatedAt: new Date('2024-01-01T00:00:00'),
isPrimary: true,
}
await EmailChangeHelpers.registerEmailUpdate(userId, email, eventData)
expect(AnalyticsManager.registerEmailChange).to.have.been.calledOnce
const callArgs = AnalyticsManager.registerEmailChange.getCall(0).args[0]
expect(callArgs).to.include({
userId,
email,
action: 'updated',
isPrimary: true,
})
expect(callArgs.emailCreatedAt).to.eql('2024-01-01T00:00:00.000Z')
expect(callArgs.emailConfirmedAt).to.be.null
})
})
describe('when the email can be matched', function () {
beforeEach(function () {
UserGetter.promises.getUserFullEmails.resolves([
{
email,
reversedHostname: 'moc.elpmaxe',
createdAt: new Date('2023-01-01T00:00:00'),
confirmedAt: new Date('2023-02-01T00:00:00'),
default: false,
},
])
})
it('calls registerEmailChange with the email data', async function () {
await EmailChangeHelpers.registerEmailUpdate(userId, email)
expect(AnalyticsManager.registerEmailChange).to.have.been.calledOnce
const callArgs = AnalyticsManager.registerEmailChange.getCall(0).args[0]
expect(callArgs).to.include({
userId,
email,
action: 'updated',
isPrimary: false,
})
expect(callArgs.emailCreatedAt).to.eql('2023-01-01T00:00:00.000Z')
expect(callArgs.emailConfirmedAt).to.eql('2023-02-01T00:00:00.000Z')
expect(callArgs.emailDeletedAt).to.be.null
})
it('prefers supplied event data over fetched email data', async function () {
const eventData = {
emailCreatedAt: new Date('2024-01-01T00:00:00'),
emailConfirmedAt: new Date('2024-02-01T00:00:00'),
isPrimary: true,
}
await EmailChangeHelpers.registerEmailUpdate(userId, email, eventData)
expect(AnalyticsManager.registerEmailChange).to.have.been.calledOnce
const callArgs = AnalyticsManager.registerEmailChange.getCall(0).args[0]
expect(callArgs).to.include({
userId,
email,
action: 'updated',
isPrimary: true,
})
expect(callArgs.emailCreatedAt).to.eql('2024-01-01T00:00:00.000Z')
expect(callArgs.emailConfirmedAt).to.eql('2024-02-01T00:00:00.000Z')
expect(callArgs.emailDeletedAt).to.be.null
})
})
describe('when the user is not found', function () {
beforeEach(function () {
UserGetter.promises.getUserFullEmails.rejects(
new Error('User not found')
)
})
it('throws the error', async function () {
await expect(
EmailChangeHelpers.registerEmailUpdate(userId, email)
).to.eventually.be.rejectedWith('User not found')
})
})
})
describe('registerEmailCreation', function () {
describe('when the email cannot be matched', function () {
beforeEach(function () {
UserGetter.promises.getUserFullEmails.resolves([
{
email: 'test2@example.com',
reversedHostname: 'moc.elpmaxe',
createdAt: new Date('2023-01-01T00:00:00'),
confirmedAt: new Date('2023-02-01T00:00:00'),
default: false,
},
])
})
it('calls registerEmailChange with the passed event data', async function () {
const eventData = {
emailCreatedAt: new Date('2024-01-01T00:00:00'),
isPrimary: true,
}
await EmailChangeHelpers.registerEmailCreation(userId, email, eventData)
expect(AnalyticsManager.registerEmailChange).to.have.been.calledOnce
const callArgs = AnalyticsManager.registerEmailChange.getCall(0).args[0]
expect(callArgs).to.include({
userId,
email,
action: 'created',
isPrimary: true,
})
expect(callArgs.emailCreatedAt).to.eql('2024-01-01T00:00:00.000Z')
expect(callArgs.emailConfirmedAt).to.be.null
expect(callArgs.emailDeletedAt).to.be.null
})
})
describe('when the email can be matched', function () {
beforeEach(function () {
UserGetter.promises.getUserFullEmails.resolves([
{
email,
reversedHostname: 'moc.elpmaxe',
createdAt: new Date('2023-01-01T00:00:00'),
confirmedAt: new Date('2023-02-01T00:00:00'),
default: false,
},
])
})
it('calls registerEmailChange with the email data', async function () {
await EmailChangeHelpers.registerEmailCreation(userId, email)
expect(AnalyticsManager.registerEmailChange).to.have.been.calledOnce
const callArgs = AnalyticsManager.registerEmailChange.getCall(0).args[0]
expect(callArgs).to.include({
userId,
email,
action: 'created',
isPrimary: false,
})
expect(callArgs.emailCreatedAt).to.eql('2023-01-01T00:00:00.000Z')
expect(callArgs.emailConfirmedAt).to.eql('2023-02-01T00:00:00.000Z')
expect(callArgs.emailDeletedAt).to.be.null
})
it('prefers supplied event data over fetched email data', async function () {
const eventData = {
emailCreatedAt: new Date('2024-01-01T00:00:00'),
emailConfirmedAt: new Date('2024-02-01T00:00:00'),
isPrimary: true,
}
await EmailChangeHelpers.registerEmailCreation(userId, email, eventData)
expect(AnalyticsManager.registerEmailChange).to.have.been.calledOnce
const callArgs = AnalyticsManager.registerEmailChange.getCall(0).args[0]
expect(callArgs).to.include({
userId,
email,
action: 'created',
isPrimary: true,
})
expect(callArgs.emailCreatedAt).to.eql('2024-01-01T00:00:00.000Z')
expect(callArgs.emailConfirmedAt).to.eql('2024-02-01T00:00:00.000Z')
expect(callArgs.emailDeletedAt).to.be.null
})
})
describe('when the user is not found', function () {
beforeEach(function () {
UserGetter.promises.getUserFullEmails.rejects(
new Error('User not found')
)
})
it('throws the error', async function () {
await expect(
EmailChangeHelpers.registerEmailCreation(userId, email)
).to.eventually.be.rejectedWith('User not found')
})
})
})
describe('registerEmailDeletion', function () {
describe('when the email cannot be matched', function () {
beforeEach(function () {
UserGetter.promises.getUserFullEmails.resolves([
{
email: 'test2@example.com',
reversedHostname: 'moc.elpmaxe',
createdAt: new Date('2023-01-01T00:00:00'),
confirmedAt: new Date('2023-02-01T00:00:00'),
default: false,
},
])
})
it('calls registerEmailChange with the passed event data', async function () {
const eventData = {
emailCreatedAt: new Date('2024-01-01T00:00:00'),
emailDeletedAt: new Date('2025-02-01T00:00:00'),
isPrimary: true,
}
await EmailChangeHelpers.registerEmailDeletion(userId, email, eventData)
expect(AnalyticsManager.registerEmailChange).to.have.been.calledOnce
const callArgs = AnalyticsManager.registerEmailChange.getCall(0).args[0]
expect(callArgs).to.include({
userId,
email,
action: 'deleted',
isPrimary: true,
})
expect(callArgs.emailCreatedAt).to.eql('2024-01-01T00:00:00.000Z')
expect(callArgs.emailDeletedAt).to.eql('2025-02-01T00:00:00.000Z')
})
})
describe('when the user is not found', function () {
beforeEach(function () {
UserGetter.promises.getUserFullEmails.rejects(
new Error('User not found')
)
})
it('throws the error', async function () {
await expect(
EmailChangeHelpers.registerEmailDeletion(userId, email)
).to.eventually.be.rejectedWith('User not found')
})
})
})
})