Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DEV-2045] Refactor index and add queue event parser helper #1259

Merged
merged 14 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
});
});
52 changes: 52 additions & 0 deletions packages/active-campaign-client/src/handlers/sqsQueueHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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';

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':
// TODO: implement pending from DEV-1983 and DEV-1986
console.log(
'Dynamo event:',
queueEvent.detail.eventName,
queueEvent.webinarId
);
break;
case 'DynamoREMOVE':
// TODO: implement pending from DEV-1983 and DEV-1986
console.log(
'Dynamo event:',
queueEvent.detail.eventName,
queueEvent.webinarId
);
break;
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' }),
};
}
}
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;
};
Loading