From bad0736d5ba93e1a433cb88d25720db49abaf317 Mon Sep 17 00:00:00 2001 From: john-tco Date: Wed, 18 Oct 2023 17:57:13 +0100 Subject: [PATCH 1/8] add fe & service --- pages/unsubscribe/{[jwt].tsx => [id].tsx} | 58 +++++++++++++++-------- src/service/unsubscribe.service.ts | 17 +++++++ 2 files changed, 54 insertions(+), 21 deletions(-) rename pages/unsubscribe/{[jwt].tsx => [id].tsx} (75%) create mode 100644 src/service/unsubscribe.service.ts diff --git a/pages/unsubscribe/[jwt].tsx b/pages/unsubscribe/[id].tsx similarity index 75% rename from pages/unsubscribe/[jwt].tsx rename to pages/unsubscribe/[id].tsx index 0234f41f..3ebdcaaa 100644 --- a/pages/unsubscribe/[jwt].tsx +++ b/pages/unsubscribe/[id].tsx @@ -1,51 +1,60 @@ import Head from 'next/head'; import Link from 'next/link'; import Layout from '../../src/components/partials/Layout'; -import { decryptSignedApiKey } from '../../src/service/api-key-service'; import { SubscriptionService } from '../../src/service/subscription-service'; import { decrypt } from '../../src/utils/encryption'; import { NewsletterSubscriptionService } from '../../src/service/newsletter/newsletter-subscription-service'; import { NewsletterType } from '../../src/types/newsletter'; import { deleteSaveSearch } from '../../src/service/saved_search_service'; import ServiceErrorPage from '../service-error/index.page'; +import { getUnsubscribeReferenceFromId } from '../../src/service/unsubscribe.service'; -export async function getServerSideProps({ query: { jwt = '' } = {} }) { +export async function getServerSideProps({ query: { id = '' } = {} }) { let emailAddress: string, type: keyof typeof UNSUBSCRIBE_HANDLER_MAP, - id: NotificationKey; + notificationId: NotificationKey; try { - const decodedJwt = decryptSignedApiKey(jwt); - type = decodedJwt.type; - id = decodedJwt.id; - emailAddress = await decrypt(decodedJwt.emailAddress); - await handleUnsubscribe(type, id, emailAddress); + const { + user: { encryptedEmailAddress }, + subscriptionId, + newsletterId, + savedSearchId, + type, + } = await getUnsubscribeReferenceFromId(id as string); + + emailAddress = await decrypt(encryptedEmailAddress); + notificationId = subscriptionId ?? newsletterId ?? savedSearchId; + await handleUnsubscribe(type, notificationId, emailAddress); + return { props: { error: false } }; } catch (error: unknown) { - return handleServerSideError(error, { type, emailAddress, id }); + return handleServerSideError(error, { + id, + type, + emailAddress, + notificationId, + }); } } const handleServerSideError = ( error: unknown, - { - type, - id, - emailAddress, - }: { - type: keyof typeof UNSUBSCRIBE_HANDLER_MAP; - id: NotificationKey; - emailAddress: string; - }, + { id, type, notificationId, emailAddress }: HandleServerSideErrorProps, ) => { - if (!type || !id || !emailAddress) { - console.error('Failed to decrypt jwt. Error: ' + JSON.stringify(error)); + if (!type || !notificationId || !emailAddress) { + console.error( + `Failed to get user from unsubscribe reference id: ${id}. Error: ${JSON.stringify( + error, + )}`, + ); } else { console.error( - `Failed to unsubscribe from notification type: ${type}, id: ${id}, with email: ${emailAddress}. Error: ${JSON.stringify( + `Failed to unsubscribe from notification type: ${type}, id: ${notificationId}, with email: ${emailAddress}. Error: ${JSON.stringify( error, )}`, ); } + return { props: { error: true } }; }; @@ -88,6 +97,13 @@ const handleUnsubscribe = async ( type NotificationKey = string | NewsletterType | number; +type HandleServerSideErrorProps = { + id: string; + type: keyof typeof UNSUBSCRIBE_HANDLER_MAP; + notificationId: NotificationKey; + emailAddress: string; +}; + const Unsubscribe = (props: undefined | { error: boolean }) => { if (props.error) { return ; diff --git a/src/service/unsubscribe.service.ts b/src/service/unsubscribe.service.ts new file mode 100644 index 00000000..23c7c124 --- /dev/null +++ b/src/service/unsubscribe.service.ts @@ -0,0 +1,17 @@ +import axios from 'axios'; + +type UnsubscribeReferenceData = { + encryptedEmailAddress: string; + newsletterId?: string; + subscriptionId?: string; + savedSearchId?: string; + user: { encryptedEmailAddress: string }; + type: 'NEWSLETTER' | 'GRANT_SUBSCRIPTION' | 'SAVED_SEARCH'; +}; + +export const getUnsubscribeReferenceFromId = async (id: string) => { + const { data } = await axios.get( + process.env.BACKEND_HOST + '/unsubscribe/' + id, + ); + return data; +}; From 1ed981ae4d52aba2b19dbc4910367757c5035e59 Mon Sep 17 00:00:00 2001 From: john-tco Date: Thu, 19 Oct 2023 11:18:50 +0100 Subject: [PATCH 2/8] amend unit test --- .../{[jwt].test.js => [id].test.js} | 71 ++++++++++++------- pages/unsubscribe/[id].tsx | 12 ++-- 2 files changed, 50 insertions(+), 33 deletions(-) rename __tests__/pages/unsubscribe/{[jwt].test.js => [id].test.js} (71%) diff --git a/__tests__/pages/unsubscribe/[jwt].test.js b/__tests__/pages/unsubscribe/[id].test.js similarity index 71% rename from __tests__/pages/unsubscribe/[jwt].test.js rename to __tests__/pages/unsubscribe/[id].test.js index cc6a7284..df933505 100644 --- a/__tests__/pages/unsubscribe/[jwt].test.js +++ b/__tests__/pages/unsubscribe/[id].test.js @@ -1,20 +1,16 @@ import '@testing-library/jest-dom'; import { AxiosError } from 'axios'; -import { decryptSignedApiKey } from '../../../src/service/api-key-service'; import { NewsletterSubscriptionService } from '../../../src/service/newsletter/newsletter-subscription-service'; -import { getServerSideProps } from '../../../pages/unsubscribe/[jwt]'; -import { TokenExpiredError } from 'jsonwebtoken'; +import { getServerSideProps } from '../../../pages/unsubscribe/[id]'; import { SubscriptionService } from '../../../src/service/subscription-service'; import { deleteSaveSearch } from '../../../src/service/saved_search_service'; +import { getUnsubscribeReferenceFromId } from '../../../src/service/unsubscribe.service'; jest.mock('../../../pages/service-error/index.page', () => ({ default: () =>

ServiceErrorPage

, })); -jest.mock('../../../src/service/api-key-service', () => ({ - decryptSignedApiKey: jest.fn(), -})); jest.mock('../../../src/utils/encryption', () => ({ - decrypt: jest.fn(), + decrypt: jest.fn().mockReturnValue('some-email'), })); jest.mock( '../../../src/service/newsletter/newsletter-subscription-service', @@ -30,6 +26,7 @@ jest.mock('../../../src/service/subscription-service', () => ({ }, })); jest.mock('../../../src/service/saved_search_service'); +jest.mock('../../../src/service/unsubscribe.service'); const newsletterSubscriptionServiceSpy = ({ throwsError }) => NewsletterSubscriptionService.getInstance.mockImplementation(() => ({ @@ -67,20 +64,15 @@ const mockSavedSearch = ({ throwsError }) => { }); }; -const getContext = ({ jwt }) => ({ - query: { - jwt, - }, -}); - describe('getServerSideProps()', () => { beforeEach(jest.clearAllMocks); - it('should return error when jwt has expired', async () => { - decryptSignedApiKey.mockImplementation(() => { - throw new TokenExpiredError(); - }); - const context = getContext({ jwt: 'invalid-jwt' }); + it('should return error when the token is invalid ', async () => { + getUnsubscribeReferenceFromId.mockReturnValue( + new AxiosError('Internal server error'), + ); + + const context = getContext({ id: 'invalid-id' }); const response = await getServerSideProps(context); expect(response).toEqual({ @@ -99,17 +91,14 @@ describe('getServerSideProps()', () => { ${'GRANT_SUBSCRIPTION'} | ${grantSubscriptionSpy} | ${false} ${'SAVED_SEARCH'} | ${mockSavedSearch} | ${false} `( - 'should return correct props when jwt is valid and $type mock service is called with throwsError: $mockServiceThrowsError', + 'should return correct props when id is valid and $type mock service is called with throwsError: $mockServiceThrowsError', async ({ type, mockServiceFunction, mockServiceThrowsError }) => { - decryptSignedApiKey.mockReturnValue({ - id: 'some-id', - type, - emailAddress: 'some-email', - }); + getUnsubscribeReferenceFromId.mockReturnValue( + getMockUnsubscribeReferenceData(type), + ); const context = getContext({ jwt: 'valid.jwt.token' }); mockServiceFunction({ throwsError: mockServiceThrowsError }); const response = await getServerSideProps(context); - expect(response).toEqual({ props: { error: mockServiceThrowsError, @@ -118,3 +107,35 @@ describe('getServerSideProps()', () => { }, ); }); + +const getContext = ({ id }) => ({ + query: { + id, + }, +}); + +const TEST_USER_DATA_MAP = { + NEWSLETTER: { + newsletterId: 'some-newsletter-id', + subscriptionId: null, + savedSearchid: null, + }, + GRANT_SUBSCRIPTION: { + newsletterId: null, + subscriptionId: 'some-subscription-id', + savedSearchid: null, + }, + SAVED_SEARCH: { + newsletterId: null, + subscriptionId: null, + savedSearchid: 'some-saved-search-id', + }, +}; + +const getMockUnsubscribeReferenceData = (type) => ({ + user: { + encryptedEmailAddress: 'some-email', + }, + ...TEST_USER_DATA_MAP[type], + type, +}); diff --git a/pages/unsubscribe/[id].tsx b/pages/unsubscribe/[id].tsx index 3ebdcaaa..643838ae 100644 --- a/pages/unsubscribe/[id].tsx +++ b/pages/unsubscribe/[id].tsx @@ -10,9 +10,7 @@ import ServiceErrorPage from '../service-error/index.page'; import { getUnsubscribeReferenceFromId } from '../../src/service/unsubscribe.service'; export async function getServerSideProps({ query: { id = '' } = {} }) { - let emailAddress: string, - type: keyof typeof UNSUBSCRIBE_HANDLER_MAP, - notificationId: NotificationKey; + let emailAddress: string, notificationId: NotificationKey; try { const { user: { encryptedEmailAddress }, @@ -30,7 +28,6 @@ export async function getServerSideProps({ query: { id = '' } = {} }) { } catch (error: unknown) { return handleServerSideError(error, { id, - type, emailAddress, notificationId, }); @@ -39,9 +36,9 @@ export async function getServerSideProps({ query: { id = '' } = {} }) { const handleServerSideError = ( error: unknown, - { id, type, notificationId, emailAddress }: HandleServerSideErrorProps, + { id, notificationId, emailAddress }: HandleServerSideErrorProps, ) => { - if (!type || !notificationId || !emailAddress) { + if (!notificationId || !emailAddress) { console.error( `Failed to get user from unsubscribe reference id: ${id}. Error: ${JSON.stringify( error, @@ -49,7 +46,7 @@ const handleServerSideError = ( ); } else { console.error( - `Failed to unsubscribe from notification type: ${type}, id: ${notificationId}, with email: ${emailAddress}. Error: ${JSON.stringify( + `Failed to unsubscribe from notification id: ${notificationId}, with email: ${emailAddress}. Error: ${JSON.stringify( error, )}`, ); @@ -99,7 +96,6 @@ type NotificationKey = string | NewsletterType | number; type HandleServerSideErrorProps = { id: string; - type: keyof typeof UNSUBSCRIBE_HANDLER_MAP; notificationId: NotificationKey; emailAddress: string; }; From c2cb0cb1e8198b619db8b2dda6be0310bb133b34 Mon Sep 17 00:00:00 2001 From: john-tco Date: Thu, 19 Oct 2023 11:27:17 +0100 Subject: [PATCH 3/8] log notification type --- pages/unsubscribe/[id].tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pages/unsubscribe/[id].tsx b/pages/unsubscribe/[id].tsx index 643838ae..ffcc38e6 100644 --- a/pages/unsubscribe/[id].tsx +++ b/pages/unsubscribe/[id].tsx @@ -10,7 +10,9 @@ import ServiceErrorPage from '../service-error/index.page'; import { getUnsubscribeReferenceFromId } from '../../src/service/unsubscribe.service'; export async function getServerSideProps({ query: { id = '' } = {} }) { - let emailAddress: string, notificationId: NotificationKey; + let emailAddress: string, + notificationId: NotificationKey, + notificationType: keyof typeof UNSUBSCRIBE_HANDLER_MAP; try { const { user: { encryptedEmailAddress }, @@ -20,6 +22,7 @@ export async function getServerSideProps({ query: { id = '' } = {} }) { type, } = await getUnsubscribeReferenceFromId(id as string); + notificationType = type; emailAddress = await decrypt(encryptedEmailAddress); notificationId = subscriptionId ?? newsletterId ?? savedSearchId; await handleUnsubscribe(type, notificationId, emailAddress); @@ -30,17 +33,23 @@ export async function getServerSideProps({ query: { id = '' } = {} }) { id, emailAddress, notificationId, + notificationType, }); } } const handleServerSideError = ( error: unknown, - { id, notificationId, emailAddress }: HandleServerSideErrorProps, + { + id, + notificationId, + emailAddress, + notificationType, + }: HandleServerSideErrorProps, ) => { if (!notificationId || !emailAddress) { console.error( - `Failed to get user from unsubscribe reference id: ${id}. Error: ${JSON.stringify( + `Failed to get user from notification with type: ${notificationType}, id: ${id}. Error: ${JSON.stringify( error, )}`, ); @@ -97,6 +106,7 @@ type NotificationKey = string | NewsletterType | number; type HandleServerSideErrorProps = { id: string; notificationId: NotificationKey; + notificationType: keyof typeof UNSUBSCRIBE_HANDLER_MAP; emailAddress: string; }; From 627ca1cc052c4eeded5382f51b98fe8fd209f961 Mon Sep 17 00:00:00 2001 From: john-tco Date: Thu, 19 Oct 2023 14:21:42 +0100 Subject: [PATCH 4/8] infer type from data --- __tests__/pages/unsubscribe/[id].test.js | 17 +++++++---- pages/unsubscribe/[id].tsx | 37 ++++++++++++++---------- src/service/unsubscribe.service.ts | 37 ++++++++++++++++++++---- 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/__tests__/pages/unsubscribe/[id].test.js b/__tests__/pages/unsubscribe/[id].test.js index df933505..da969380 100644 --- a/__tests__/pages/unsubscribe/[id].test.js +++ b/__tests__/pages/unsubscribe/[id].test.js @@ -10,7 +10,7 @@ jest.mock('../../../pages/service-error/index.page', () => ({ default: () =>

ServiceErrorPage

, })); jest.mock('../../../src/utils/encryption', () => ({ - decrypt: jest.fn().mockReturnValue('some-email'), + decrypt: jest.fn(), })); jest.mock( '../../../src/service/newsletter/newsletter-subscription-service', @@ -25,8 +25,14 @@ jest.mock('../../../src/service/subscription-service', () => ({ getInstance: jest.fn(), }, })); +jest.mock('../../../src/service/unsubscribe.service', () => ({ + getUnsubscribeReferenceFromId: jest.fn(), + removeUnsubscribeReference: jest.fn(), + getTypeFromNotificationIds: jest.requireActual( + '../../../src/service/unsubscribe.service', + ).getTypeFromNotificationIds, +})); jest.mock('../../../src/service/saved_search_service'); -jest.mock('../../../src/service/unsubscribe.service'); const newsletterSubscriptionServiceSpy = ({ throwsError }) => NewsletterSubscriptionService.getInstance.mockImplementation(() => ({ @@ -118,17 +124,17 @@ const TEST_USER_DATA_MAP = { NEWSLETTER: { newsletterId: 'some-newsletter-id', subscriptionId: null, - savedSearchid: null, + savedSearchId: null, }, GRANT_SUBSCRIPTION: { newsletterId: null, subscriptionId: 'some-subscription-id', - savedSearchid: null, + savedSearchId: null, }, SAVED_SEARCH: { newsletterId: null, subscriptionId: null, - savedSearchid: 'some-saved-search-id', + savedSearchId: 'some-saved-search-id', }, }; @@ -137,5 +143,4 @@ const getMockUnsubscribeReferenceData = (type) => ({ encryptedEmailAddress: 'some-email', }, ...TEST_USER_DATA_MAP[type], - type, }); diff --git a/pages/unsubscribe/[id].tsx b/pages/unsubscribe/[id].tsx index ffcc38e6..c1d7467b 100644 --- a/pages/unsubscribe/[id].tsx +++ b/pages/unsubscribe/[id].tsx @@ -7,7 +7,11 @@ import { NewsletterSubscriptionService } from '../../src/service/newsletter/news import { NewsletterType } from '../../src/types/newsletter'; import { deleteSaveSearch } from '../../src/service/saved_search_service'; import ServiceErrorPage from '../service-error/index.page'; -import { getUnsubscribeReferenceFromId } from '../../src/service/unsubscribe.service'; +import { + getTypeFromNotificationIds, + getUnsubscribeReferenceFromId, + removeUnsubscribeReference, +} from '../../src/service/unsubscribe.service'; export async function getServerSideProps({ query: { id = '' } = {} }) { let emailAddress: string, @@ -19,13 +23,18 @@ export async function getServerSideProps({ query: { id = '' } = {} }) { subscriptionId, newsletterId, savedSearchId, - type, } = await getUnsubscribeReferenceFromId(id as string); - notificationType = type; + notificationType = getTypeFromNotificationIds({ + subscriptionId, + newsletterId, + savedSearchId, + }); emailAddress = await decrypt(encryptedEmailAddress); notificationId = subscriptionId ?? newsletterId ?? savedSearchId; - await handleUnsubscribe(type, notificationId, emailAddress); + + await handleUnsubscribe(notificationType, notificationId, emailAddress); + await removeUnsubscribeReference(id); return { props: { error: false } }; } catch (error: unknown) { @@ -69,7 +78,6 @@ const grantSubscriptionHandler = async ( emailAddress: string, ) => { const subscriptionService = SubscriptionService.getInstance(); - return subscriptionService.deleteSubscriptionByEmailAndGrantId( emailAddress, id as string, @@ -79,7 +87,6 @@ const grantSubscriptionHandler = async ( const newsletterHandler = async (id: NotificationKey, emailAddress: string) => { const newsletterSubscriptionService = NewsletterSubscriptionService.getInstance(); - return newsletterSubscriptionService.unsubscribeFromNewsletter( emailAddress, id as NewsletterType, @@ -101,15 +108,6 @@ const handleUnsubscribe = async ( emailAddress: string, ) => UNSUBSCRIBE_HANDLER_MAP[type](id, emailAddress); -type NotificationKey = string | NewsletterType | number; - -type HandleServerSideErrorProps = { - id: string; - notificationId: NotificationKey; - notificationType: keyof typeof UNSUBSCRIBE_HANDLER_MAP; - emailAddress: string; -}; - const Unsubscribe = (props: undefined | { error: boolean }) => { if (props.error) { return ; @@ -160,4 +158,13 @@ const Unsubscribe = (props: undefined | { error: boolean }) => { ); }; +type NotificationKey = string | NewsletterType | number; + +type HandleServerSideErrorProps = { + id: string; + notificationId: NotificationKey; + notificationType: keyof typeof UNSUBSCRIBE_HANDLER_MAP; + emailAddress: string; +}; + export default Unsubscribe; diff --git a/src/service/unsubscribe.service.ts b/src/service/unsubscribe.service.ts index 23c7c124..11c5362d 100644 --- a/src/service/unsubscribe.service.ts +++ b/src/service/unsubscribe.service.ts @@ -1,13 +1,40 @@ import axios from 'axios'; -type UnsubscribeReferenceData = { - encryptedEmailAddress: string; +const GRANT_SUBSCRIPTION = 'GRANT_SUBSCRIPTION'; +const NEWSLETTER = 'NEWSLETTER'; +const SAVED_SEARCH = 'SAVED_SEARCH'; + +interface NotificationKeys { newsletterId?: string; subscriptionId?: string; savedSearchId?: string; - user: { encryptedEmailAddress: string }; - type: 'NEWSLETTER' | 'GRANT_SUBSCRIPTION' | 'SAVED_SEARCH'; -}; +} + +interface UnsubscribeReferenceData extends NotificationKeys { + encryptedEmailAddress: string; + user: { encryptedEmailAddress: string; id: number }; +} + +const NOTIFICATION_KEY_MAP = { + subscriptionId: GRANT_SUBSCRIPTION, + newsletterId: NEWSLETTER, + savedSearchId: SAVED_SEARCH, +} as const; + +export const getTypeFromNotificationIds = ({ + subscriptionId, + newsletterId, + savedSearchId, +}: NotificationKeys) => + Object.values(NOTIFICATION_KEY_MAP).find( + (value) => + (subscriptionId && value === GRANT_SUBSCRIPTION) || + (newsletterId && value === NEWSLETTER) || + (savedSearchId && value === SAVED_SEARCH), + ) as (typeof NOTIFICATION_KEY_MAP)[keyof typeof NOTIFICATION_KEY_MAP]; + +export const removeUnsubscribeReference = async (id: string) => + axios.delete(`${process.env.BACKEND_HOST}/unsubscribe/${id}`); export const getUnsubscribeReferenceFromId = async (id: string) => { const { data } = await axios.get( From e2f0b2e2a7e0fe5371c4c206cf7003bd340d9c1b Mon Sep 17 00:00:00 2001 From: john-tco Date: Tue, 24 Oct 2023 14:37:21 +0100 Subject: [PATCH 5/8] REFACTOR - delete unsubscribe reference using existing methods --- pages/unsubscribe/[id].tsx | 30 ++++++++++++++----- .../newsletter-subscription-service.ts | 3 +- src/service/saved_search_service.ts | 8 +++-- src/service/subscription-service.ts | 9 ++++-- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/pages/unsubscribe/[id].tsx b/pages/unsubscribe/[id].tsx index c1d7467b..33fdb423 100644 --- a/pages/unsubscribe/[id].tsx +++ b/pages/unsubscribe/[id].tsx @@ -10,7 +10,6 @@ import ServiceErrorPage from '../service-error/index.page'; import { getTypeFromNotificationIds, getUnsubscribeReferenceFromId, - removeUnsubscribeReference, } from '../../src/service/unsubscribe.service'; export async function getServerSideProps({ query: { id = '' } = {} }) { @@ -33,8 +32,7 @@ export async function getServerSideProps({ query: { id = '' } = {} }) { emailAddress = await decrypt(encryptedEmailAddress); notificationId = subscriptionId ?? newsletterId ?? savedSearchId; - await handleUnsubscribe(notificationType, notificationId, emailAddress); - await removeUnsubscribeReference(id); + await handleUnsubscribe(notificationType, notificationId, emailAddress, id); return { props: { error: false } }; } catch (error: unknown) { @@ -76,25 +74,35 @@ const handleServerSideError = ( const grantSubscriptionHandler = async ( id: NotificationKey, emailAddress: string, + unsubscribeReferenceId: string, ) => { const subscriptionService = SubscriptionService.getInstance(); return subscriptionService.deleteSubscriptionByEmailAndGrantId( emailAddress, id as string, + unsubscribeReferenceId, ); }; -const newsletterHandler = async (id: NotificationKey, emailAddress: string) => { +const newsletterHandler = async ( + id: NotificationKey, + emailAddress: string, + unsubscribeReferenceId: string, +) => { const newsletterSubscriptionService = NewsletterSubscriptionService.getInstance(); return newsletterSubscriptionService.unsubscribeFromNewsletter( emailAddress, id as NewsletterType, + unsubscribeReferenceId, ); }; -const savedSearchHandler = async (id: NotificationKey, emailAddress: string) => - deleteSaveSearch(id as number, emailAddress); +const savedSearchHandler = async ( + id: NotificationKey, + emailAddress: string, + unsubscribeReferenceId: string, +) => deleteSaveSearch(id as number, emailAddress, unsubscribeReferenceId); const UNSUBSCRIBE_HANDLER_MAP = { GRANT_SUBSCRIPTION: grantSubscriptionHandler, @@ -104,9 +112,15 @@ const UNSUBSCRIBE_HANDLER_MAP = { const handleUnsubscribe = async ( type: keyof typeof UNSUBSCRIBE_HANDLER_MAP, - id: NotificationKey, + notificationKey: NotificationKey, emailAddress: string, -) => UNSUBSCRIBE_HANDLER_MAP[type](id, emailAddress); + unsubscribeReferenceId: string, +) => + UNSUBSCRIBE_HANDLER_MAP[type]( + notificationKey, + emailAddress, + unsubscribeReferenceId, + ); const Unsubscribe = (props: undefined | { error: boolean }) => { if (props.error) { diff --git a/src/service/newsletter/newsletter-subscription-service.ts b/src/service/newsletter/newsletter-subscription-service.ts index 99a4a42b..01d79abe 100644 --- a/src/service/newsletter/newsletter-subscription-service.ts +++ b/src/service/newsletter/newsletter-subscription-service.ts @@ -33,9 +33,10 @@ export class NewsletterSubscriptionService { async unsubscribeFromNewsletter( plaintextEmail: string, type: NewsletterType, + unsubscribeReferenceId: string, ): Promise { return await NewsletterSubscriptionService.client.delete( - `/users/${plaintextEmail}/types/${type}`, + `/users/${plaintextEmail}/types/${type}?unsubscribeReference=${unsubscribeReferenceId}`, ); } } diff --git a/src/service/saved_search_service.ts b/src/service/saved_search_service.ts index 90a21214..a8dcc43c 100644 --- a/src/service/saved_search_service.ts +++ b/src/service/saved_search_service.ts @@ -81,10 +81,14 @@ export async function updateStatus( return response.data; } -export async function deleteSaveSearch(savedSearchId: number, email: string) { +export async function deleteSaveSearch( + savedSearchId: number, + email: string, + unsubscribeReferenceId: string, +) { const response = await axios({ method: 'post', - url: `${process.env.BACKEND_HOST}/saved-searches/${savedSearchId}/delete`, + url: `${process.env.BACKEND_HOST}/saved-searches/${savedSearchId}/delete?unsubscribeReference=${unsubscribeReferenceId}`, data: { email, }, diff --git a/src/service/subscription-service.ts b/src/service/subscription-service.ts index 15938386..b796de06 100644 --- a/src/service/subscription-service.ts +++ b/src/service/subscription-service.ts @@ -56,11 +56,14 @@ export class SubscriptionService { async deleteSubscriptionByEmailAndGrantId( emailAddress: string, grantId: string, - ): Promise { - const endpoint: string = `${ + unsubscribeReference?: string, + ) { + const endpoint = `${ SubscriptionService.endpoint.emailParam + encodeURIComponent(emailAddress) }/${SubscriptionService.endpoint.grantIdParam + grantId}`; - const result = await SubscriptionService.client.delete(endpoint); + const result = await SubscriptionService.client.delete( + endpoint + '?unsubscribeReference=' + unsubscribeReference, + ); return result.data; } } From 607aaa4295ada16df533288f5dcbca622ece4591 Mon Sep 17 00:00:00 2001 From: john-tco Date: Tue, 24 Oct 2023 15:34:45 +0100 Subject: [PATCH 6/8] fix tests after refactor --- .../service/subscription_service.test.ts | 190 +++++++++--------- .../newsletter-subscription-service.test.js | 2 +- src/service/saved_search_service.test.js | 2 +- 3 files changed, 98 insertions(+), 96 deletions(-) diff --git a/__tests__/service/subscription_service.test.ts b/__tests__/service/subscription_service.test.ts index f04766c1..e16e87c3 100644 --- a/__tests__/service/subscription_service.test.ts +++ b/__tests__/service/subscription_service.test.ts @@ -58,116 +58,118 @@ jest.mock('next/config', () => { const subscriptionService = SubscriptionService.getInstance(); const instance = axios.create(); -describe('subscription manager add subscription', () => { - it('should return true when a correct email and grant id are passed in', async () => { - const result = await subscriptionService.addSubscription( - body.encrypted_email_address, - body.contentful_grant_subscription_id, - ); - expect(result).toBe(true); - expect(instance.post).toHaveBeenNthCalledWith(1, ' ', { - emailAddress: body.encrypted_email_address, - contentfulGrantSubscriptionId: body.contentful_grant_subscription_id, - }); - }); -}); - -describe('subscription manager delete Subscription By ID', () => { - it('should delete a subscription when correct values are passed into the function', async () => { - const result = - await subscriptionService.deleteSubscriptionByEmailAndGrantId( +describe('first', () => { + describe('subscription manager add subscription', () => { + it('should return true when a correct email and grant id are passed in', async () => { + const result = await subscriptionService.addSubscription( body.encrypted_email_address, body.contentful_grant_subscription_id, ); - - expect(instance.delete).toHaveBeenNthCalledWith( - 1, - 'users/fake%40fake.com/grants/12345678', - ); - expect(result).toBe(true); + expect(result).toBe(true); + expect(instance.post).toHaveBeenNthCalledWith(1, ' ', { + emailAddress: body.encrypted_email_address, + contentfulGrantSubscriptionId: body.contentful_grant_subscription_id, + }); + }); }); -}); -describe('subscription manager get Subscription By Email', () => { - beforeAll(() => { - jest.clearAllMocks(); + describe('subscription manager delete Subscription By ID', () => { + it('should delete a subscription when correct values are passed into the function', async () => { + const result = + await subscriptionService.deleteSubscriptionByEmailAndGrantId( + body.encrypted_email_address, + body.contentful_grant_subscription_id, + ); + + expect(instance.delete).toHaveBeenNthCalledWith( + 1, + 'users/fake%40fake.com/grants/12345678?unsubscribeReference=undefined', + ); + expect(result).toBe(true); + }); }); - it('should return records when they are found', async () => { - const example = [ - { - encrypted_email_address: 'test@test.com', - hashed_email_address: 'test@test.com', - contentful_grant_subscription_id: '12345678', - createdAt: 'now', - updatedAt: 'now', - }, - ]; - const result = - await subscriptionService.getSubscriptionsByEmail('test@test.com'); + describe('subscription manager get Subscription By Email', () => { + beforeAll(() => { + jest.clearAllMocks(); + }); + it('should return records when they are found', async () => { + const example = [ + { + encrypted_email_address: 'test@test.com', + hashed_email_address: 'test@test.com', + contentful_grant_subscription_id: '12345678', + createdAt: 'now', + updatedAt: 'now', + }, + ]; - expect(result).toEqual(example); + const result = + await subscriptionService.getSubscriptionsByEmail('test@test.com'); - expect(instance.get).toHaveBeenNthCalledWith(1, 'users/test%40test.com'); - }); + expect(result).toEqual(example); - it('should return an empty object if no records are found', async () => { - (instance as any).get.mockImplementation(() => { - return { - data: {}, - }; + expect(instance.get).toHaveBeenNthCalledWith(1, 'users/test%40test.com'); }); - expect( - await subscriptionService.getSubscriptionsByEmail('test@test.com'), - ).toEqual({}); - expect(instance.get).toHaveBeenNthCalledWith(1, 'users/test%40test.com'); - }); -}); -describe('subscription manager get Subscription By Email and ID', () => { - beforeAll(() => { - jest.clearAllMocks(); - }); - it('should return emails if any are found', async () => { - const example = { - encrypted_email_address: 'test@test.com', - hashed_email_address: 'test@test.com', - contentful_grant_subscription_id: '12345678', - createdAt: 'now', - updatedAt: 'now', - }; - (instance as any).get.mockImplementation(() => { - return { - data: example, - }; + it('should return an empty object if no records are found', async () => { + (instance as any).get.mockImplementation(() => { + return { + data: {}, + }; + }); + expect( + await subscriptionService.getSubscriptionsByEmail('test@test.com'), + ).toEqual({}); + expect(instance.get).toHaveBeenNthCalledWith(1, 'users/test%40test.com'); }); - expect( - await subscriptionService.getSubscriptionByEmailAndGrantId( - 'test@test.com', - '12345678', - ), - ).toEqual(example); - expect(instance.get).toHaveBeenNthCalledWith( - 1, - 'users/test%40test.com/grants/12345678', - ); }); - it('should return an empty object if no records are found', async () => { - (instance as any).get.mockImplementation(() => { - return { - data: {}, + describe('subscription manager get Subscription By Email and ID', () => { + beforeAll(() => { + jest.clearAllMocks(); + }); + it('should return emails if any are found', async () => { + const example = { + encrypted_email_address: 'test@test.com', + hashed_email_address: 'test@test.com', + contentful_grant_subscription_id: '12345678', + createdAt: 'now', + updatedAt: 'now', }; + (instance as any).get.mockImplementation(() => { + return { + data: example, + }; + }); + expect( + await subscriptionService.getSubscriptionByEmailAndGrantId( + 'test@test.com', + '12345678', + ), + ).toEqual(example); + expect(instance.get).toHaveBeenNthCalledWith( + 1, + 'users/test%40test.com/grants/12345678', + ); + }); + + it('should return an empty object if no records are found', async () => { + (instance as any).get.mockImplementation(() => { + return { + data: {}, + }; + }); + expect( + await subscriptionService.getSubscriptionByEmailAndGrantId( + 'test@test.com', + '12345678', + ), + ).toEqual({}); + expect(instance.get).toHaveBeenNthCalledWith( + 1, + 'users/test%40test.com/grants/12345678', + ); }); - expect( - await subscriptionService.getSubscriptionByEmailAndGrantId( - 'test@test.com', - '12345678', - ), - ).toEqual({}); - expect(instance.get).toHaveBeenNthCalledWith( - 1, - 'users/test%40test.com/grants/12345678', - ); }); }); diff --git a/src/service/newsletter/newsletter-subscription-service.test.js b/src/service/newsletter/newsletter-subscription-service.test.js index 15cbb312..0fd6e00f 100644 --- a/src/service/newsletter/newsletter-subscription-service.test.js +++ b/src/service/newsletter/newsletter-subscription-service.test.js @@ -77,7 +77,7 @@ describe('newsletter-subscription-service', () => { ); expect(axiosInstance.delete).toHaveBeenCalledWith( - `/users/${email}/types/${NewsletterType.NEW_GRANTS}`, + `/users/${email}/types/${NewsletterType.NEW_GRANTS}?unsubscribeReference=undefined`, ); }); }); diff --git a/src/service/saved_search_service.test.js b/src/service/saved_search_service.test.js index dbdeea90..f12a6698 100644 --- a/src/service/saved_search_service.test.js +++ b/src/service/saved_search_service.test.js @@ -142,7 +142,7 @@ describe('delete', () => { expect(axios).toHaveBeenCalledWith({ method: 'post', - url: `${process.env.BACKEND_HOST}/saved-searches/${saveSearchId}/delete`, + url: `${process.env.BACKEND_HOST}/saved-searches/${saveSearchId}/delete?unsubscribeReference=undefined`, data: { email, }, From 644c43766316f04fcf6903164ad8e0548ef1ae86 Mon Sep 17 00:00:00 2001 From: john-tco Date: Tue, 24 Oct 2023 15:48:46 +0100 Subject: [PATCH 7/8] make ref optional --- src/service/newsletter/newsletter-subscription-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/newsletter/newsletter-subscription-service.ts b/src/service/newsletter/newsletter-subscription-service.ts index 01d79abe..a0f174b8 100644 --- a/src/service/newsletter/newsletter-subscription-service.ts +++ b/src/service/newsletter/newsletter-subscription-service.ts @@ -33,7 +33,7 @@ export class NewsletterSubscriptionService { async unsubscribeFromNewsletter( plaintextEmail: string, type: NewsletterType, - unsubscribeReferenceId: string, + unsubscribeReferenceId?: string, ): Promise { return await NewsletterSubscriptionService.client.delete( `/users/${plaintextEmail}/types/${type}?unsubscribeReference=${unsubscribeReferenceId}`, From 85b524e196f74516befe378dc5d50770e87d9b5e Mon Sep 17 00:00:00 2001 From: john-tco Date: Tue, 24 Oct 2023 15:58:46 +0100 Subject: [PATCH 8/8] rm --- .../service/subscription_service.test.ts | 190 +++++++++--------- 1 file changed, 94 insertions(+), 96 deletions(-) diff --git a/__tests__/service/subscription_service.test.ts b/__tests__/service/subscription_service.test.ts index e16e87c3..eacaa4a8 100644 --- a/__tests__/service/subscription_service.test.ts +++ b/__tests__/service/subscription_service.test.ts @@ -58,118 +58,116 @@ jest.mock('next/config', () => { const subscriptionService = SubscriptionService.getInstance(); const instance = axios.create(); -describe('first', () => { - describe('subscription manager add subscription', () => { - it('should return true when a correct email and grant id are passed in', async () => { - const result = await subscriptionService.addSubscription( - body.encrypted_email_address, - body.contentful_grant_subscription_id, - ); - expect(result).toBe(true); - expect(instance.post).toHaveBeenNthCalledWith(1, ' ', { - emailAddress: body.encrypted_email_address, - contentfulGrantSubscriptionId: body.contentful_grant_subscription_id, - }); +describe('subscription manager add subscription', () => { + it('should return true when a correct email and grant id are passed in', async () => { + const result = await subscriptionService.addSubscription( + body.encrypted_email_address, + body.contentful_grant_subscription_id, + ); + expect(result).toBe(true); + expect(instance.post).toHaveBeenNthCalledWith(1, ' ', { + emailAddress: body.encrypted_email_address, + contentfulGrantSubscriptionId: body.contentful_grant_subscription_id, }); }); +}); - describe('subscription manager delete Subscription By ID', () => { - it('should delete a subscription when correct values are passed into the function', async () => { - const result = - await subscriptionService.deleteSubscriptionByEmailAndGrantId( - body.encrypted_email_address, - body.contentful_grant_subscription_id, - ); - - expect(instance.delete).toHaveBeenNthCalledWith( - 1, - 'users/fake%40fake.com/grants/12345678?unsubscribeReference=undefined', +describe('subscription manager delete Subscription By ID', () => { + it('should delete a subscription when correct values are passed into the function', async () => { + const result = + await subscriptionService.deleteSubscriptionByEmailAndGrantId( + body.encrypted_email_address, + body.contentful_grant_subscription_id, ); - expect(result).toBe(true); - }); + + expect(instance.delete).toHaveBeenNthCalledWith( + 1, + 'users/fake%40fake.com/grants/12345678?unsubscribeReference=undefined', + ); + expect(result).toBe(true); }); +}); - describe('subscription manager get Subscription By Email', () => { - beforeAll(() => { - jest.clearAllMocks(); - }); - it('should return records when they are found', async () => { - const example = [ - { - encrypted_email_address: 'test@test.com', - hashed_email_address: 'test@test.com', - contentful_grant_subscription_id: '12345678', - createdAt: 'now', - updatedAt: 'now', - }, - ]; +describe('subscription manager get Subscription By Email', () => { + beforeAll(() => { + jest.clearAllMocks(); + }); + it('should return records when they are found', async () => { + const example = [ + { + encrypted_email_address: 'test@test.com', + hashed_email_address: 'test@test.com', + contentful_grant_subscription_id: '12345678', + createdAt: 'now', + updatedAt: 'now', + }, + ]; - const result = - await subscriptionService.getSubscriptionsByEmail('test@test.com'); + const result = + await subscriptionService.getSubscriptionsByEmail('test@test.com'); - expect(result).toEqual(example); + expect(result).toEqual(example); - expect(instance.get).toHaveBeenNthCalledWith(1, 'users/test%40test.com'); - }); + expect(instance.get).toHaveBeenNthCalledWith(1, 'users/test%40test.com'); + }); - it('should return an empty object if no records are found', async () => { - (instance as any).get.mockImplementation(() => { - return { - data: {}, - }; - }); - expect( - await subscriptionService.getSubscriptionsByEmail('test@test.com'), - ).toEqual({}); - expect(instance.get).toHaveBeenNthCalledWith(1, 'users/test%40test.com'); + it('should return an empty object if no records are found', async () => { + (instance as any).get.mockImplementation(() => { + return { + data: {}, + }; }); + expect( + await subscriptionService.getSubscriptionsByEmail('test@test.com'), + ).toEqual({}); + expect(instance.get).toHaveBeenNthCalledWith(1, 'users/test%40test.com'); }); +}); - describe('subscription manager get Subscription By Email and ID', () => { - beforeAll(() => { - jest.clearAllMocks(); - }); - it('should return emails if any are found', async () => { - const example = { - encrypted_email_address: 'test@test.com', - hashed_email_address: 'test@test.com', - contentful_grant_subscription_id: '12345678', - createdAt: 'now', - updatedAt: 'now', +describe('subscription manager get Subscription By Email and ID', () => { + beforeAll(() => { + jest.clearAllMocks(); + }); + it('should return emails if any are found', async () => { + const example = { + encrypted_email_address: 'test@test.com', + hashed_email_address: 'test@test.com', + contentful_grant_subscription_id: '12345678', + createdAt: 'now', + updatedAt: 'now', + }; + (instance as any).get.mockImplementation(() => { + return { + data: example, }; - (instance as any).get.mockImplementation(() => { - return { - data: example, - }; - }); - expect( - await subscriptionService.getSubscriptionByEmailAndGrantId( - 'test@test.com', - '12345678', - ), - ).toEqual(example); - expect(instance.get).toHaveBeenNthCalledWith( - 1, - 'users/test%40test.com/grants/12345678', - ); }); + expect( + await subscriptionService.getSubscriptionByEmailAndGrantId( + 'test@test.com', + '12345678', + ), + ).toEqual(example); + expect(instance.get).toHaveBeenNthCalledWith( + 1, + 'users/test%40test.com/grants/12345678', + ); + }); - it('should return an empty object if no records are found', async () => { - (instance as any).get.mockImplementation(() => { - return { - data: {}, - }; - }); - expect( - await subscriptionService.getSubscriptionByEmailAndGrantId( - 'test@test.com', - '12345678', - ), - ).toEqual({}); - expect(instance.get).toHaveBeenNthCalledWith( - 1, - 'users/test%40test.com/grants/12345678', - ); + it('should return an empty object if no records are found', async () => { + (instance as any).get.mockImplementation(() => { + return { + data: {}, + }; }); + expect( + await subscriptionService.getSubscriptionByEmailAndGrantId( + 'test@test.com', + '12345678', + ), + ).toEqual({}); + expect(instance.get).toHaveBeenNthCalledWith( + 1, + 'users/test%40test.com/grants/12345678', + ); }); });