Explorar el Código

Add helper functions for creating change events

GitOrigin-RevId: 26a4cbc8e322c52e12cd3eb7f891d9914cefc70d
Andrew Rumble hace 1 año
padre
commit
cfbfcbc5db

+ 122 - 0
services/web/app/src/Features/Analytics/EmailChangeHelper.js

@@ -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,
+}

+ 268 - 0
services/web/test/unit/src/Analytics/EmailChangeHelpersTests.js

@@ -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')
+      })
+    })
+  })
+})