Skip to content

Commit

Permalink
feat(api): support for addCustomAttribute
Browse files Browse the repository at this point in the history
  • Loading branch information
jagregory committed May 31, 2022
1 parent 6803bff commit 7932176
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 1 deletion.
60 changes: 60 additions & 0 deletions integration-tests/aws-sdk/addCustomAttributes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { USER_POOL_AWS_DEFAULTS } from "../../src/services/cognitoService";
import { withCognitoSdk } from "./setup";

describe(
"CognitoIdentityServiceProvider.addCustomAttributes",
withCognitoSdk((Cognito) => {
it("updates a user pool", async () => {
const client = Cognito();

// create the user pool client
const up = await client
.createUserPool({
PoolName: "pool",
})
.promise();

const describeResponse = await client
.describeUserPool({
UserPoolId: up.UserPool?.Id!,
})
.promise();

expect(describeResponse.UserPool).toMatchObject({
SchemaAttributes: USER_POOL_AWS_DEFAULTS.SchemaAttributes,
});

await client
.addCustomAttributes({
UserPoolId: up.UserPool?.Id!,
CustomAttributes: [
{
AttributeDataType: "String",
Name: "test",
},
],
})
.promise();

const describeResponseAfterUpdate = await client
.describeUserPool({
UserPoolId: up.UserPool?.Id!,
})
.promise();

expect(describeResponseAfterUpdate.UserPool).toMatchObject({
SchemaAttributes: [
...(USER_POOL_AWS_DEFAULTS.SchemaAttributes ?? []),
{
AttributeDataType: "String",
DeveloperOnlyAttribute: false,
Mutable: true,
Name: "custom:test",
Required: false,
StringAttributeConstraints: {},
},
],
});
});
})
);
4 changes: 3 additions & 1 deletion src/__tests__/testDataBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { v4 } from "uuid";
import { AppClient } from "../services/appClient";
import { USER_POOL_AWS_DEFAULTS } from "../services/cognitoService";
import { Group, User, UserPool } from "../services/userPoolService";

export const id = (prefix: string, number?: number) =>
Expand Down Expand Up @@ -84,7 +85,8 @@ export const userPool = (partial?: Partial<UserPool>): UserPool => {
MfaConfiguration: partial?.MfaConfiguration ?? undefined,
Name: partial?.Name ?? undefined,
Policies: partial?.Policies ?? undefined,
SchemaAttributes: partial?.SchemaAttributes ?? undefined,
SchemaAttributes:
partial?.SchemaAttributes ?? USER_POOL_AWS_DEFAULTS.SchemaAttributes,
SmsAuthenticationMessage: partial?.SmsAuthenticationMessage ?? undefined,
SmsConfiguration: partial?.SmsConfiguration ?? undefined,
SmsConfigurationFailure: partial?.SmsConfigurationFailure ?? undefined,
Expand Down
146 changes: 146 additions & 0 deletions src/targets/addCustomAttributes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { ClockFake } from "../__tests__/clockFake";
import { newMockCognitoService } from "../__tests__/mockCognitoService";
import { newMockUserPoolService } from "../__tests__/mockUserPoolService";
import { TestContext } from "../__tests__/testContext";
import * as TDB from "../__tests__/testDataBuilder";
import {
GroupNotFoundError,
InvalidParameterError,
UserNotFoundError,
} from "../errors";
import { CognitoService, UserPoolService } from "../services";
import {
AddCustomAttributes,
AddCustomAttributesTarget,
} from "./addCustomAttributes";

const originalDate = new Date();

describe("AddCustomAttributes target", () => {
let addCustomAttributes: AddCustomAttributesTarget;
let clock: ClockFake;
let mockCognitoService: jest.Mocked<CognitoService>;

beforeEach(() => {
clock = new ClockFake(originalDate);

mockCognitoService = newMockCognitoService();
addCustomAttributes = AddCustomAttributes({
clock,
cognito: mockCognitoService,
});
});

it("appends a custom attribute to the user pool", async () => {
const userPool = TDB.userPool();
const mockUserPoolService = newMockUserPoolService(userPool);

mockCognitoService.getUserPool.mockResolvedValue(mockUserPoolService);

const newDate = new Date();
clock.advanceTo(newDate);

await addCustomAttributes(TestContext, {
UserPoolId: "test",
CustomAttributes: [
{
AttributeDataType: "String",
Name: "test",
},
],
});

expect(mockUserPoolService.updateOptions).toHaveBeenCalledWith(
TestContext,
{
...userPool,
SchemaAttributes: [
...(userPool.SchemaAttributes ?? []),
{
Name: "custom:test",
AttributeDataType: "String",
DeveloperOnlyAttribute: false,
Mutable: true,
Required: false,
StringAttributeConstraints: {},
},
],
LastModifiedDate: newDate,
}
);
});

it("can create a custom attribute with no name", async () => {
const userPool = TDB.userPool();
const mockUserPoolService = newMockUserPoolService(userPool);

mockCognitoService.getUserPool.mockResolvedValue(mockUserPoolService);

const newDate = new Date();
clock.advanceTo(newDate);

await addCustomAttributes(TestContext, {
UserPoolId: "test",
CustomAttributes: [
{
AttributeDataType: "String",
},
],
});

expect(mockUserPoolService.updateOptions).toHaveBeenCalledWith(
TestContext,
{
...userPool,
SchemaAttributes: [
...(userPool.SchemaAttributes ?? []),
{
Name: "custom:null",
AttributeDataType: "String",
DeveloperOnlyAttribute: false,
Mutable: true,
Required: false,
StringAttributeConstraints: {},
},
],
LastModifiedDate: newDate,
}
);
});

it("throws if an attribute with the name already exists", async () => {
const userPool = TDB.userPool({
SchemaAttributes: [
{
Name: "custom:test",
AttributeDataType: "String",
DeveloperOnlyAttribute: false,
Mutable: true,
Required: false,
StringAttributeConstraints: {},
},
],
});
const mockUserPoolService = newMockUserPoolService(userPool);

mockCognitoService.getUserPool.mockResolvedValue(mockUserPoolService);

await expect(
addCustomAttributes(TestContext, {
UserPoolId: "test",
CustomAttributes: [
{
AttributeDataType: "String",
Name: "test",
},
],
})
).rejects.toEqual(
new InvalidParameterError(
"custom:test: Existing attribute already has name custom:test."
)
);

expect(mockUserPoolService.updateOptions).not.toHaveBeenCalled();
});
});
55 changes: 55 additions & 0 deletions src/targets/addCustomAttributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
AddCustomAttributesRequest,
AddCustomAttributesResponse,
} from "aws-sdk/clients/cognitoidentityserviceprovider";
import { InvalidParameterError } from "../errors";
import { Services } from "../services";
import { assertParameterLength } from "./utils/assertions";
import { Target } from "./Target";

export type AddCustomAttributesTarget = Target<
AddCustomAttributesRequest,
AddCustomAttributesResponse
>;

type AddCustomAttributesServices = Pick<Services, "clock" | "cognito">;

export const AddCustomAttributes =
({
clock,
cognito,
}: AddCustomAttributesServices): AddCustomAttributesTarget =>
async (ctx, req) => {
assertParameterLength("CustomAttributes", 1, 25, req.CustomAttributes);

const userPool = await cognito.getUserPool(ctx, req.UserPoolId);

await userPool.updateOptions(ctx, {
...userPool.options,
SchemaAttributes: [
...(userPool.options.SchemaAttributes ?? []),
...req.CustomAttributes.map(({ Name, ...attr }) => {
const name = `custom:${Name ?? "null"}`;

if (userPool.options.SchemaAttributes?.find((x) => x.Name === name)) {
throw new InvalidParameterError(
`${name}: Existing attribute already has name ${name}.`
);
}

return {
AttributeDataType: "String",
DeveloperOnlyAttribute: false,
Mutable: true,
Required: false,
StringAttributeConstraints: {},
Name: name,
...attr,
};
}),
],
LastModifiedDate: clock.get(),
});

return {};
};
2 changes: 2 additions & 0 deletions src/targets/targets.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AddCustomAttributes } from "./addCustomAttributes";
import { AdminAddUserToGroup } from "./adminAddUserToGroup";
import { AdminConfirmSignUp } from "./adminConfirmSignUp";
import { AdminCreateUser } from "./adminCreateUser";
Expand Down Expand Up @@ -46,6 +47,7 @@ import { UpdateUserPoolClient } from "./updateUserPoolClient";
import { VerifyUserAttribute } from "./verifyUserAttribute";

export const Targets = {
AddCustomAttributes,
AdminAddUserToGroup,
AdminConfirmSignUp,
AdminCreateUser,
Expand Down
46 changes: 46 additions & 0 deletions src/targets/utils/assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { InvalidParameterError } from "../../errors";

/**
* Assert a required parameter has a value. Throws an InvalidParameterError.
*
* @param name Name of the parameter to include in the error message
* @param parameter Parameter to assert
* @param message Custom full message for the error, optional
*/
export function assertRequiredParameter<T>(
name: string,
parameter: T,
message?: string
): asserts parameter is NonNullable<T> {
if (!parameter) {
throw new InvalidParameterError(
message ?? `Missing required parameter ${name}`
);
}
}

/**
* Asserts an array parameter has a length between min and max. Throws an InvalidParameterError.
*
* @param name Name of the parameter to include in the error message
* @param min Minimum length
* @param max Maximum length
* @param parameter Parameter to assert
*/
export function assertParameterLength<T>(
name: string,
min: number,
max: number,
parameter: readonly T[]
): asserts parameter {
if (parameter.length < min) {
throw new InvalidParameterError(
`Invalid length for parameter ${name}, value: ${parameter.length}, valid min length: ${min}`
);
}
if (parameter.length > max) {
throw new InvalidParameterError(
`Invalid length for parameter ${name}, value: ${parameter.length}, valid max length: ${max}`
);
}
}

0 comments on commit 7932176

Please sign in to comment.