Skip to content

Commit

Permalink
[DEV-2045] Refactor index and add queue event parser helper (#1259)
Browse files Browse the repository at this point in the history
* Add function to subscribe and upsubscribe contact from active campaign list

* Update packages/active-campaign-client/src/__tests__/handlers/manageListSubscription.test.ts

Co-authored-by: marcobottaro <[email protected]>

* Update packages/active-campaign-client/src/__tests__/handlers/manageListSubscription.test.ts

Co-authored-by: marcobottaro <[email protected]>

* Refactor index and add queue event parser helper

* Update packages/active-campaign-client/.env.example

Co-authored-by: Marco Ponchia <[email protected]>

* pr comments

* Connect webinar events to active campaign endpoint

* Fix console log

---------

Co-authored-by: t <[email protected]>
Co-authored-by: tommaso1 <[email protected]>
Co-authored-by: marcobottaro <[email protected]>
  • Loading branch information
4 people authored Dec 5, 2024
1 parent d1dae75 commit b9003b6
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const listUsersCommandOutput: ListUsersCommandOutput = {
],
};

describe('addContact handler', () => {
describe('Helpers: listUsersCommandOutputToUser', () => {
it('should properly convert ListUsersCommandOutput to User', async () => {
const user = listUsersCommandOutputToUser(listUsersCommandOutput);
const expectedUser: User = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { queueEventParser } from '../../helpers/queueEventParser';
import { QueueEvent, QueueEventType } from '../../types/queueEvent';

const MOCK_WEBINAR_ID = 'webinar-id';
const MOCK_COGNITO_ID = 'c67ec280-799a-40d6-b398-2a2b31aefbbd';

const generateMockBody = (eventName?: QueueEventType, webinarId?: string) => {
const webinarData = webinarId ? { webinarId } : {};
return {
...webinarData,
version: '0',
id: 'c67ec280-799a-40d6-b398-2a2b31aefbbd',
'detail-type': 'AWS API Call via CloudTrail',
source: 'aws.cognito-idp',
account: '99999999999',
time: '2024-11-25T13:34:12Z',
region: 'region',
resources: [],
detail: {
eventVersion: '1.08',
userIdentity: {
type: 'Unknown',
principalId: 'Anonymous',
},
eventTime: '2024-11-25T13:34:12Z',
eventSource: 'cognito-idp.amazonaws.com',
eventName: `${eventName || 'Unknown'}`,
awsRegion: 'region',
sourceIPAddress: '1.1.1.1',
userAgent:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
requestParameters: {
userAttributes: 'HIDDEN_DUE_TO_SECURITY_REASONS',
accessToken: 'HIDDEN_DUE_TO_SECURITY_REASONS',
},
responseElements: null,
additionalEventData: {
sub: MOCK_COGNITO_ID,
},
requestID: 'c67ec280-799a-40d6-b398-2a2b31aefbbd',
eventID: '1b231015-853a-4042-a157-4127a9ec5530',
readOnly: false,
eventType: 'AwsApiCall',
managementEvent: true,
recipientAccountId: '999999999',
eventCategory: 'Management',
tlsDetails: {
tlsVersion: 'TLSv1.3',
cipherSuite: 'TLS_AES_128_GCM_SHA256',
clientProvidedHostHeader: 'clientProvidedHostHeader',
},
},
};
};

const generateSQSMockEvent = (params?: {
readonly eventType?: QueueEventType;
readonly webinarId?: string;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
readonly customBody?: Record<string, unknown>;
}) => ({
Records: [
{
messageId: '983ad1cf-e06a-4393-8382-e51af60c4f58',
receiptHandle: 'receiptHandle',
body: JSON.stringify(
params?.customBody ||
generateMockBody(params?.eventType, params?.webinarId)
),
attributes: {
ApproximateReceiveCount: '1',
SentTimestamp: '99999999',
SequenceNumber: '1245',
MessageGroupId: 'userEvents',
SenderId: 'SenderId',
MessageDeduplicationId: 'MessageDeduplicationId',
ApproximateFirstReceiveTimestamp: '99999999',
},
messageAttributes: {},
md5OfBody: 'sdf1df457fg71d5sf1dfsd7',
eventSource: 'aws:sqs',
eventSourceARN: 'eventSourceARN',
awsRegion: 'awsRegion',
},
],
});

describe('Helpers: queueEventParser', () => {
it('should rise an error if event is different from QueueEventType', async () => {
const sqsEvent = generateSQSMockEvent();
expect(() => {
queueEventParser(sqsEvent);
}).toThrow('Event missing required fields');
});

it('should rise an error if body is not a valid JSON', async () => {
const sqsEvent = generateSQSMockEvent();
expect(() => {
queueEventParser(sqsEvent);
}).toThrow('Event missing required fields');
});

it('should properly convert UpdateUserAttributes event', async () => {
const sqsEvent = generateSQSMockEvent({
eventType: 'UpdateUserAttributes',
});
const parsedQueueEvent = queueEventParser(sqsEvent);
const queueEvent: QueueEvent = {
detail: {
eventName: 'UpdateUserAttributes',
additionalEventData: {
sub: MOCK_COGNITO_ID,
},
},
};
expect(parsedQueueEvent.detail.eventName).toEqual(
queueEvent.detail.eventName
);
expect(parsedQueueEvent.detail.additionalEventData.sub).toEqual(
queueEvent.detail.additionalEventData.sub
);
});

it('should properly convert DeleteUser event', async () => {
const sqsEvent = generateSQSMockEvent({ eventType: 'DeleteUser' });
const parsedQueueEvent = queueEventParser(sqsEvent);
const queueEvent: QueueEvent = {
detail: {
eventName: 'DeleteUser',
additionalEventData: {
sub: MOCK_COGNITO_ID,
},
},
};
expect(parsedQueueEvent.detail.eventName).toEqual(
queueEvent.detail.eventName
);
expect(parsedQueueEvent.detail.additionalEventData.sub).toEqual(
queueEvent.detail.additionalEventData.sub
);
});

it('should properly convert ConfirmSignUp event', async () => {
const sqsEvent = generateSQSMockEvent({ eventType: 'ConfirmSignUp' });
const parsedQueueEvent = queueEventParser(sqsEvent);
const queueEvent: QueueEvent = {
detail: {
eventName: 'ConfirmSignUp',
additionalEventData: {
sub: MOCK_COGNITO_ID,
},
},
};
expect(parsedQueueEvent.detail.eventName).toEqual(
queueEvent.detail.eventName
);
expect(parsedQueueEvent.detail.additionalEventData.sub).toEqual(
queueEvent.detail.additionalEventData.sub
);
});

it('should properly convert DynamoINSERT event', async () => {
const sqsEvent = generateSQSMockEvent({
eventType: 'DynamoINSERT',
webinarId: MOCK_WEBINAR_ID,
});
const parsedQueueEvent = queueEventParser(sqsEvent);
const queueEvent: QueueEvent = {
detail: {
eventName: 'DynamoINSERT',
additionalEventData: {
sub: MOCK_COGNITO_ID,
},
},
webinarId: MOCK_WEBINAR_ID,
};
expect(parsedQueueEvent.detail.eventName).toEqual(
queueEvent.detail.eventName
);
expect(parsedQueueEvent.detail.additionalEventData.sub).toEqual(
queueEvent.detail.additionalEventData.sub
);
expect(parsedQueueEvent.webinarId).toEqual(queueEvent.webinarId);
});

it('should properly convert DynamoREMOVE event', async () => {
const sqsEvent = generateSQSMockEvent({
eventType: 'DynamoREMOVE',
webinarId: MOCK_WEBINAR_ID,
});
const parsedQueueEvent = queueEventParser(sqsEvent);
const queueEvent: QueueEvent = {
detail: {
eventName: 'DynamoREMOVE',
additionalEventData: {
sub: MOCK_COGNITO_ID,
},
},
webinarId: MOCK_WEBINAR_ID,
};
expect(parsedQueueEvent.detail.eventName).toEqual(
queueEvent.detail.eventName
);
expect(parsedQueueEvent.detail.additionalEventData.sub).toEqual(
queueEvent.detail.additionalEventData.sub
);
expect(parsedQueueEvent.webinarId).toEqual(queueEvent.webinarId);
});

it('should rise an error if webinar id is missing for Dynamo event', async () => {
const sqsEvent = generateSQSMockEvent({ eventType: 'DynamoREMOVE' });
expect(() => {
queueEventParser(sqsEvent);
}).toThrow('Event missing required fields');
});
});
46 changes: 46 additions & 0 deletions packages/active-campaign-client/src/handlers/sqsQueueHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { APIGatewayProxyResult, SQSEvent } from 'aws-lambda';
import { getUserFromCognito } from '../helpers/getUserFromCognito';
import { addContact } from '../helpers/addContact';
import { updateContact } from '../helpers/updateContact';
import { deleteContact } from '../helpers/deleteContact';
import { queueEventParser } from '../helpers/queueEventParser';
import {
addContactToList,
removeContactToList,
} from '../helpers/manageListSubscription';

export async function sqsQueueHandler(event: {
readonly Records: SQSEvent['Records'];
}): Promise<APIGatewayProxyResult> {
try {
console.log('Event:', event); // TODO: Remove after testing
const queueEvent = queueEventParser(event);
switch (queueEvent.detail.eventName) {
case 'ConfirmSignUp':
return await addContact(await getUserFromCognito(queueEvent));
case 'UpdateUserAttributes':
return await updateContact(await getUserFromCognito(queueEvent));
case 'DeleteUser':
return await deleteContact(queueEvent.detail.additionalEventData.sub);
case 'DynamoINSERT':
return await addContactToList(
queueEvent.detail.additionalEventData.sub,
queueEvent.webinarId || ''
);
case 'DynamoREMOVE':
return await removeContactToList(
queueEvent.detail.additionalEventData.sub,
queueEvent.webinarId || ''
);
default:
// eslint-disable-next-line functional/no-throw-statements
throw new Error('Unknown event');
}
} catch (error) {
console.error('Error:', error); // TODO: Remove after testing
return {
statusCode: 500,
body: JSON.stringify({ message: 'Internal server error' }),
};
}
}
33 changes: 33 additions & 0 deletions packages/active-campaign-client/src/helpers/queueEventParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { SQSEvent } from 'aws-lambda';
import { QueueEvent, QueueEventType } from '../types/queueEvent';

const queueEvents: readonly QueueEventType[] = [
'ConfirmSignUp',
'UpdateUserAttributes',
'DeleteUser',
'DynamoINSERT',
'DynamoREMOVE',
];

const dynamoEvents: readonly QueueEventType[] = [
'DynamoINSERT',
'DynamoREMOVE',
];

export function queueEventParser(event: {
readonly Records: SQSEvent['Records'];
}): QueueEvent {
const queueEvent = JSON.parse(event.Records[0].body) as unknown as QueueEvent;

if (
!queueEvents.includes(queueEvent.detail.eventName) ||
!queueEvent.detail.additionalEventData.sub ||
(dynamoEvents.includes(queueEvent.detail.eventName) &&
!queueEvent.webinarId)
) {
// eslint-disable-next-line functional/no-throw-statements
throw new Error('Event missing required fields');
}

return queueEvent;
}
40 changes: 5 additions & 35 deletions packages/active-campaign-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,8 @@
import { APIGatewayProxyResult, SQSEvent } from 'aws-lambda';
import { getUserFromCognito } from './helpers/getUserFromCognito';
import { QueueEvent } from './types/queueEvent';
import { addContact } from './helpers/addContact';
import { updateContact } from './helpers/updateContact';
import { deleteContact } from './helpers/deleteContact';
import { SQSEvent } from 'aws-lambda';
import { sqsQueueHandler } from './handlers/sqsQueueHandler';

export async function handler(event: {
export async function sqsQueue(event: {
readonly Records: SQSEvent['Records'];
}): Promise<APIGatewayProxyResult> {
try {
console.log('Event:', event); // TODO: Remove after testing
const queueEvent = JSON.parse(
event.Records[0].body
) as unknown as QueueEvent;
switch (queueEvent.detail.eventName) {
case 'ConfirmSignUp':
return await addContact(await getUserFromCognito(queueEvent));
case 'UpdateUserAttributes':
return await updateContact(await getUserFromCognito(queueEvent));
case 'DeleteUser':
return await deleteContact(queueEvent.detail.additionalEventData.sub);
default:
console.log('Unknown event:', queueEvent.detail.eventName);
break;
}
return {
statusCode: 200,
body: JSON.stringify(event),
};
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
body: JSON.stringify({ message: 'Internal server error' }),
};
}
}) {
return await sqsQueueHandler(event);
}
5 changes: 4 additions & 1 deletion packages/active-campaign-client/src/types/queueEvent.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export type QueueEventType =
| 'UpdateUserAttributes'
| 'DeleteUser'
| 'ConfirmSignUp';
| 'ConfirmSignUp'
| 'DynamoINSERT'
| 'DynamoREMOVE';

export type QueueEvent = {
readonly detail: {
Expand All @@ -10,4 +12,5 @@ export type QueueEvent = {
readonly sub: string;
};
};
readonly webinarId?: string;
};

0 comments on commit b9003b6

Please sign in to comment.