Skip to content

Commit

Permalink
feat(lambda): limited CustomMessage lambda support
Browse files Browse the repository at this point in the history
Only supports the ForgotPassword flow so far
  • Loading branch information
jagregory committed Jul 22, 2021
1 parent ca56796 commit 6880a90
Show file tree
Hide file tree
Showing 16 changed files with 498 additions and 42 deletions.
2 changes: 1 addition & 1 deletion integration-tests/aws-sdk/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const withCognitoSdk = (
{
cognitoClient,
messageDelivery: mockCodeDelivery,
messages: new MessagesService(),
messages: new MessagesService(triggers),
otp,
triggers,
},
Expand Down
11 changes: 11 additions & 0 deletions src/__tests__/testDataBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { User } from "../services/userPoolClient";

export const user = (): User => ({
Attributes: [],
Enabled: true,
Password: "Password123!",
UserCreateDate: new Date().getTime(),
UserLastModifiedDate: new Date().getTime(),
Username: "Username",
UserStatus: "CONFIRMED",
});
2 changes: 1 addition & 1 deletion src/server/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const createDefaultServer = async (logger: Logger): Promise<Server> => {
messageDelivery: new MessageDeliveryService(
new ConsoleMessageSender(logger)
),
messages: new MessagesService(),
messages: new MessagesService(triggers),
otp,
triggers,
},
Expand Down
115 changes: 85 additions & 30 deletions src/services/lambda.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ describe("Lambda function invoker", () => {
).rejects.toEqual(new Error("UserMigration trigger not configured"));
});

describe("UserMigration_Authentication", () => {
it("invokes the lambda", async () => {
describe("when lambda successful", () => {
it("returns string payload as json", async () => {
const response = Promise.resolve({
StatusCode: 200,
Payload: '{ "some": "json" }',
Payload: '{ "response": "value" }',
});
mockLambdaClient.invoke.mockReturnValue({
promise: () => response,
Expand All @@ -64,7 +64,7 @@ describe("Lambda function invoker", () => {
MockLogger
);

await lambda.invoke("UserMigration", {
const result = await lambda.invoke("UserMigration", {
clientId: "clientId",
password: "password",
triggerSource: "UserMigration_Authentication",
Expand All @@ -73,32 +73,13 @@ describe("Lambda function invoker", () => {
userAttributes: {},
});

expect(mockLambdaClient.invoke).toHaveBeenCalledWith({
FunctionName: "MyLambdaName",
InvocationType: "RequestResponse",
Payload: JSON.stringify({
version: 0,
userName: "username",
callerContext: { awsSdkVersion: "2.656.0", clientId: "clientId" },
region: "local",
userPoolId: "userPoolId",
triggerSource: "UserMigration_Authentication",
request: {
userAttributes: {},
password: "password",
validationData: {},
},
response: {},
}),
});
expect(result).toEqual("value");
});
});

describe("when lambda successful", () => {
it("returns string payload as json", async () => {
it("returns Buffer payload as json", async () => {
const response = Promise.resolve({
StatusCode: 200,
Payload: '{ "response": "value" }',
Payload: Buffer.from('{ "response": "value" }'),
});
mockLambdaClient.invoke.mockReturnValue({
promise: () => response,
Expand All @@ -122,11 +103,13 @@ describe("Lambda function invoker", () => {

expect(result).toEqual("value");
});
});

it("returns Buffer payload as json", async () => {
describe("UserMigration_Authentication", () => {
it("invokes the lambda", async () => {
const response = Promise.resolve({
StatusCode: 200,
Payload: Buffer.from('{ "response": "value" }'),
Payload: '{ "some": "json" }',
});
mockLambdaClient.invoke.mockReturnValue({
promise: () => response,
Expand All @@ -139,7 +122,7 @@ describe("Lambda function invoker", () => {
MockLogger
);

const result = await lambda.invoke("UserMigration", {
await lambda.invoke("UserMigration", {
clientId: "clientId",
password: "password",
triggerSource: "UserMigration_Authentication",
Expand All @@ -148,7 +131,79 @@ describe("Lambda function invoker", () => {
userAttributes: {},
});

expect(result).toEqual("value");
expect(mockLambdaClient.invoke).toHaveBeenCalledWith({
FunctionName: "MyLambdaName",
InvocationType: "RequestResponse",
Payload: JSON.stringify({
version: 0,
userName: "username",
callerContext: { awsSdkVersion: "2.656.0", clientId: "clientId" },
region: "local",
userPoolId: "userPoolId",
triggerSource: "UserMigration_Authentication",
request: {
userAttributes: {},
password: "password",
validationData: {},
},
response: {},
}),
});
});
});

describe.each([
"CustomMessage_SignUp",
"CustomMessage_AdminCreateUser",
"CustomMessage_ResendCode",
"CustomMessage_ForgotPassword",
"CustomMessage_UpdateUserAttribute",
"CustomMessage_VerifyUserAttribute",
"CustomMessage_Authentication",
] as const)("%s", (source) => {
it("invokes the lambda function with the code parameter", async () => {
const response = Promise.resolve({
StatusCode: 200,
Payload: '{ "some": "json" }',
});
mockLambdaClient.invoke.mockReturnValue({
promise: () => response,
} as any);
const lambda = new LambdaService(
{
CustomMessage: "MyLambdaName",
},
mockLambdaClient,
MockLogger
);

await lambda.invoke("CustomMessage", {
clientId: "clientId",
code: "1234",
triggerSource: source,
userAttributes: {},
username: "username",
userPoolId: "userPoolId",
});

expect(mockLambdaClient.invoke).toHaveBeenCalledWith({
FunctionName: "MyLambdaName",
InvocationType: "RequestResponse",
Payload: JSON.stringify({
version: 0,
userName: "username",
callerContext: { awsSdkVersion: "2.656.0", clientId: "clientId" },
region: "local",
userPoolId: "userPoolId",
triggerSource: source,
request: {
userAttributes: {},
usernameParameter: "username",
codeParameter: "1234",
},
response: {},
}),
});
});
});
});
Expand Down
44 changes: 40 additions & 4 deletions src/services/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ import { UnexpectedLambdaExceptionError } from "../errors";
import { version as awsSdkVersion } from "aws-sdk/package.json";
import { Logger } from "../log";

interface CustomMessageEvent {
userPoolId: string;
clientId: string;
username: string;
code: string;
userAttributes: Record<string, string>;
triggerSource:
| "CustomMessage_SignUp"
| "CustomMessage_AdminCreateUser"
| "CustomMessage_ResendCode"
| "CustomMessage_ForgotPassword"
| "CustomMessage_UpdateUserAttribute"
| "CustomMessage_VerifyUserAttribute"
| "CustomMessage_Authentication";
}

interface UserMigrationEvent {
userPoolId: string;
clientId: string;
Expand All @@ -27,15 +43,24 @@ interface PostConfirmationEvent {
export type CognitoUserPoolResponse = CognitoUserPoolEvent["response"];

export interface FunctionConfig {
CustomMessage?: string;
UserMigration?: string;
PostConfirmation?: string;
}

export interface Lambda {
enabled(lambda: keyof FunctionConfig): boolean;
invoke(
trigger: keyof FunctionConfig,
event: UserMigrationEvent | PostConfirmationEvent
lambda: "CustomMessage",
event: CustomMessageEvent
): Promise<CognitoUserPoolResponse>;
invoke(
lambda: "UserMigration",
event: UserMigrationEvent
): Promise<CognitoUserPoolResponse>;
invoke(
lambda: "PostConfirmation",
event: PostConfirmationEvent
): Promise<CognitoUserPoolResponse>;
}

Expand All @@ -60,8 +85,8 @@ export class LambdaService implements Lambda {

public async invoke(
trigger: keyof FunctionConfig,
event: UserMigrationEvent | PostConfirmationEvent
): Promise<CognitoUserPoolResponse> {
event: CustomMessageEvent | UserMigrationEvent | PostConfirmationEvent
) {
const functionName = this.config[trigger];
if (!functionName) {
throw new Error(`${trigger} trigger not configured`);
Expand All @@ -86,6 +111,17 @@ export class LambdaService implements Lambda {
if (event.triggerSource === "UserMigration_Authentication") {
lambdaEvent.request.password = event.password;
lambdaEvent.request.validationData = {};
} else if (
event.triggerSource === "CustomMessage_SignUp" ||
event.triggerSource === "CustomMessage_AdminCreateUser" ||
event.triggerSource === "CustomMessage_ResendCode" ||
event.triggerSource === "CustomMessage_ForgotPassword" ||
event.triggerSource === "CustomMessage_UpdateUserAttribute" ||
event.triggerSource === "CustomMessage_VerifyUserAttribute" ||
event.triggerSource === "CustomMessage_Authentication"
) {
lambdaEvent.request.usernameParameter = event.username;
lambdaEvent.request.codeParameter = event.code;
}

this.logger.debug(
Expand Down
54 changes: 54 additions & 0 deletions src/services/messageDelivery/consoleMessageSender.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { MockLogger } from "../../__tests__/mockLogger";
import { ConsoleMessageSender } from "./consoleMessageSender";
import * as TDB from "../../__tests__/testDataBuilder";

describe("consoleMessageSender", () => {
const user = TDB.user();
const destination = "[email protected]";
const mockLog = MockLogger;
const sender = new ConsoleMessageSender(mockLog);

beforeEach(() => {
jest.resetAllMocks();
});

describe.each(["sendEmail", "sendSms"] as const)("%s", (fn) => {
it("prints the message to the console", async () => {
await sender[fn](user, destination, {
__code: "1234",
});

expect(mockLog.info).toHaveBeenCalledWith(
expect.stringMatching(/Username:\s+Username/)
);
expect(mockLog.info).toHaveBeenCalledWith(
expect.stringMatching(/Destination:\[email protected]/)
);
expect(mockLog.info).toHaveBeenCalledWith(
expect.stringMatching(/Code:\s+1234/)
);
});

it("doesn't print undefined fields", async () => {
await sender[fn](user, destination, {
__code: "1234",
emailMessage: undefined,
});

expect(mockLog.info).not.toHaveBeenCalledWith(
expect.stringMatching(/Email Message/)
);
});

it("prints additional fields", async () => {
await sender[fn](user, destination, {
__code: "1234",
emailMessage: "this is the email message",
});

expect(mockLog.info).toHaveBeenCalledWith(
expect.stringMatching(/Email Message:\s+this is the email message/)
);
});
});
});
Loading

0 comments on commit 6880a90

Please sign in to comment.