Skip to content

Commit

Permalink
feat(api): adminConfirmSignUp full support
Browse files Browse the repository at this point in the history
  • Loading branch information
jagregory committed Dec 9, 2021
1 parent 407122f commit e16a211
Show file tree
Hide file tree
Showing 16 changed files with 287 additions and 59 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ A _Good Enough_ offline emulator for [Amazon Cognito](https://aws.amazon.com/cog
| -------------------------------- | -------------------- |
| AddCustomAttributes ||
| AdminAddUserToGroup ||
| AdminConfirmSignUp | 🕒 (partial support) |
| AdminConfirmSignUp | |
| AdminCreateUser | 🕒 (partial support) |
| AdminDeleteUser ||
| AdminDeleteUserAttributes ||
Expand Down Expand Up @@ -295,7 +295,7 @@ Before starting Cognito Local, create a config file if one doesn't already exist
You can edit that `.cognito/config.json` and add any of the following settings:

| Setting | Type | Default | Description |
|--------------------------------------------| ---------- | ----------------------- |-------------------------------------------------------------|
| ------------------------------------------ | ---------- | ----------------------- | ----------------------------------------------------------- |
| `LambdaClient` | `object` | | Any setting you would pass to the AWS.Lambda Node.js client |
| `LambdaClient.credentials.accessKeyId` | `string` | `local` | |
| `LambdaClient.credentials.secretAccessKey` | `string` | `local` | |
Expand Down
43 changes: 43 additions & 0 deletions integration-tests/aws-sdk/adminConfirmSignUp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { withCognitoSdk } from "./setup";

describe(
"CognitoIdentityServiceProvider.adminConfirmSignUp",
withCognitoSdk((Cognito) => {
it("creates a user with only the required parameters", async () => {
const client = Cognito();

await client
.adminCreateUser({
UserAttributes: [{ Name: "phone_number", Value: "0400000000" }],
Username: "abc",
UserPoolId: "test",
})
.promise();

let user = await client
.adminGetUser({
UserPoolId: "test",
Username: "abc",
})
.promise();

expect(user.UserStatus).toEqual("FORCE_CHANGE_PASSWORD");

await client
.adminConfirmSignUp({
UserPoolId: "test",
Username: "abc",
})
.promise();

user = await client
.adminGetUser({
UserPoolId: "test",
Username: "abc",
})
.promise();

expect(user.UserStatus).toEqual("CONFIRMED");
});
})
);
4 changes: 2 additions & 2 deletions integration-tests/aws-sdk/initiateAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe(
aud: upc.UserPoolClient?.ClientId,
auth_time: expect.any(Number),
email: "[email protected]",
email_verified: true,
email_verified: false,
event_id: expect.stringMatching(UUID),
exp: expect.any(Number),
iat: expect.any(Number),
Expand Down Expand Up @@ -245,7 +245,7 @@ describe(
aud: upc.UserPoolClient?.ClientId,
auth_time: expect.any(Number),
email: "[email protected]",
email_verified: true,
email_verified: false,
event_id: expect.stringMatching(UUID),
exp: expect.any(Number),
iat: expect.any(Number),
Expand Down
8 changes: 6 additions & 2 deletions src/services/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,13 @@ interface PostAuthenticationEvent extends EventCommonParameters {
triggerSource: "PostAuthentication_Authentication";
}

interface PostConfirmationEvent extends EventCommonParameters {
interface PostConfirmationEvent
extends Omit<EventCommonParameters, "clientId"> {
triggerSource:
| "PostConfirmation_ConfirmSignUp"
| "PostConfirmation_ConfirmForgotPassword";
clientMetadata: Record<string, string> | undefined;
clientId: string | null;
}

export interface FunctionConfig {
Expand Down Expand Up @@ -257,7 +259,9 @@ export class LambdaService implements Lambda {
const version = "0"; // TODO: how do we know what this is?
const callerContext = {
awsSdkVersion,
clientId: event.clientId,

// client id can be null, even though the types don't allow it
clientId: event.clientId as string,
};
const region = "local"; // TODO: pull from above,

Expand Down
6 changes: 0 additions & 6 deletions src/services/triggers/postConfirmation.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
import { newMockCognitoService } from "../../__tests__/mockCognitoService";
import { newMockLambda } from "../../__tests__/mockLambda";
import { newMockUserPoolService } from "../../__tests__/mockUserPoolService";
import { TestContext } from "../../__tests__/testContext";
import { Lambda } from "../lambda";
import { UserPoolService } from "../userPoolService";
import { PostConfirmation, PostConfirmationTrigger } from "./postConfirmation";

describe("PostConfirmation trigger", () => {
let mockLambda: jest.Mocked<Lambda>;
let mockUserPoolService: jest.Mocked<UserPoolService>;
let postConfirmation: PostConfirmationTrigger;

beforeEach(() => {
mockLambda = newMockLambda();
mockUserPoolService = newMockUserPoolService();
postConfirmation = PostConfirmation({
lambda: mockLambda,
cognitoClient: newMockCognitoService(mockUserPoolService),
});
});

Expand Down
15 changes: 2 additions & 13 deletions src/services/triggers/postConfirmation.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { AttributeListType } from "aws-sdk/clients/cognitoidentityserviceprovider";
import { CognitoService } from "../cognitoService";
import { Lambda } from "../lambda";
import { attributesToRecord } from "../userPoolService";
import { ResourceNotFoundError } from "../../errors";
import { Trigger } from "./trigger";

export type PostConfirmationTrigger = Trigger<
{
source:
| "PostConfirmation_ConfirmSignUp"
| "PostConfirmation_ConfirmForgotPassword";
clientId: string;
clientId: string | null;

/**
* One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the
Expand All @@ -29,23 +27,14 @@ export type PostConfirmationTrigger = Trigger<

interface PostConfirmationServices {
lambda: Lambda;
cognitoClient: CognitoService;
}

export const PostConfirmation =
({
lambda,
cognitoClient,
}: PostConfirmationServices): PostConfirmationTrigger =>
({ lambda }: PostConfirmationServices): PostConfirmationTrigger =>
async (
ctx,
{ clientId, clientMetadata, source, userAttributes, username, userPoolId }
) => {
const userPool = await cognitoClient.getUserPoolForClientId(ctx, clientId);
if (!userPool) {
throw new ResourceNotFoundError();
}

try {
await lambda.invoke(ctx, "PostConfirmation", {
clientId,
Expand Down
2 changes: 1 addition & 1 deletion src/services/triggers/triggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class TriggersService implements Triggers {

this.customMessage = CustomMessage({ lambda, cognitoClient });
this.postAuthentication = PostAuthentication({ lambda });
this.postConfirmation = PostConfirmation({ lambda, cognitoClient });
this.postConfirmation = PostConfirmation({ lambda });
this.preSignUp = PreSignUp({ lambda });
this.preTokenGeneration = PreTokenGeneration({ lambda });
this.userMigration = UserMigration({ clock, lambda, cognitoClient });
Expand Down
21 changes: 21 additions & 0 deletions src/services/userPoolService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AttributeListType,
AttributeType,
MFAOptionListType,
UserPoolType,
UserStatusType,
Expand All @@ -15,6 +16,10 @@ export interface MFAOption {
AttributeName: "phone_number";
}

export const attribute = (
name: string,
value: string | undefined
): AttributeType => ({ Name: name, Value: value });
export const attributesIncludeMatch = (
attributeName: string,
attributeValue: string,
Expand Down Expand Up @@ -42,6 +47,22 @@ export const attributesFromRecord = (
attributes: Record<string, string>
): AttributeListType =>
Object.entries(attributes).map(([Name, Value]) => ({ Name, Value }));
export const attributesAppend = (
attributes: AttributeListType | undefined,
...toAppend: AttributeListType
): AttributeListType => {
const attributeSet = attributesToRecord(attributes);

for (const attr of toAppend) {
if (attr.Value) {
attributeSet[attr.Name] = attr.Value;
} else {
delete attributeSet[attr.Name];
}
}

return attributesFromRecord(attributeSet);
};

export const customAttributes = (
attributes: AttributeListType | undefined
Expand Down
121 changes: 121 additions & 0 deletions src/targets/adminConfirmSignUp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { ClockFake } from "../__tests__/clockFake";
import { newMockCognitoService } from "../__tests__/mockCognitoService";
import { newMockTriggers } from "../__tests__/mockTriggers";
import { newMockUserPoolService } from "../__tests__/mockUserPoolService";
import { TestContext } from "../__tests__/testContext";
import * as TDB from "../__tests__/testDataBuilder";
import { NotAuthorizedError } from "../errors";
import { Triggers, UserPoolService } from "../services";
import { attribute, attributesAppend } from "../services/userPoolService";
import {
AdminConfirmSignUp,
AdminConfirmSignUpTarget,
} from "./adminConfirmSignUp";

const currentDate = new Date();

const clock = new ClockFake(currentDate);

describe("AdminConfirmSignUp target", () => {
let adminConfirmSignUp: AdminConfirmSignUpTarget;
let mockUserPoolService: jest.Mocked<UserPoolService>;
let mockTriggers: jest.Mocked<Triggers>;

beforeEach(() => {
mockUserPoolService = newMockUserPoolService();
mockTriggers = newMockTriggers();
adminConfirmSignUp = AdminConfirmSignUp({
clock,
cognito: newMockCognitoService(mockUserPoolService),
triggers: mockTriggers,
});
});

it("throws if the user doesn't exist", async () => {
mockUserPoolService.getUserByUsername.mockResolvedValue(null);

await expect(
adminConfirmSignUp(TestContext, {
ClientMetadata: {
client: "metadata",
},
Username: "invalid user",
UserPoolId: "test",
})
).rejects.toEqual(new NotAuthorizedError());
});

it("updates the user's status", async () => {
const user = TDB.user();

mockUserPoolService.getUserByUsername.mockResolvedValue(user);

await adminConfirmSignUp(TestContext, {
ClientMetadata: {
client: "metadata",
},
Username: user.Username,
UserPoolId: "test",
});

expect(mockUserPoolService.saveUser).toHaveBeenCalledWith(TestContext, {
...user,
UserLastModifiedDate: currentDate,
UserStatus: "CONFIRMED",
});
});

describe("when PostConfirmation trigger is enabled", () => {
it("invokes the trigger", async () => {
mockTriggers.enabled.mockImplementation(
(trigger) => trigger === "PostConfirmation"
);

const user = TDB.user();

mockUserPoolService.getUserByUsername.mockResolvedValue(user);

await adminConfirmSignUp(TestContext, {
ClientMetadata: {
client: "metadata",
},
Username: user.Username,
UserPoolId: "test",
});

expect(mockTriggers.postConfirmation).toHaveBeenCalledWith(TestContext, {
clientId: null,
clientMetadata: {
client: "metadata",
},
source: "PostConfirmation_ConfirmSignUp",
userAttributes: attributesAppend(
user.Attributes,
attribute("cognito:user_status", "CONFIRMED")
),
userPoolId: "test",
username: user.Username,
});
});
});

describe("when PostConfirmation trigger is not enabled", () => {
it("invokes the trigger", async () => {
mockTriggers.enabled.mockReturnValue(false);

const user = TDB.user();

mockUserPoolService.getUserByUsername.mockResolvedValue(user);

await adminConfirmSignUp(TestContext, {
ClientMetadata: {
client: "metadata",
},
Username: user.Username,
UserPoolId: "test",
});

expect(mockTriggers.postConfirmation).not.toHaveBeenCalled();
});
});
});
Loading

0 comments on commit e16a211

Please sign in to comment.