diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index af580d9ad6394..b1c899c7068f7 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -160,6 +160,39 @@ When the `smsRole` property is specified, the `smsRoleExternalId` may also be sp assume role policy should be configured to accept this value as the ExternalId. Learn more about [ExternalId here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html). +### Attributes + +Attributes represent the various properties of each user that's collected and stored in the user pool. Cognito +provides a set of standard attributes that are available for all user pools. Users are allowed to select any of these +standard attributes to be required. Users will not be able to sign up to the user pool without providing the required +attributes. Besides these, additional attributes can be further defined, and are known as custom attributes. + +Learn more on [attributes in Cognito's +documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html). + +The following code sample configures a user pool with two standard attributes (name and address) as required, and adds +four optional attributes. + +```ts +new UserPool(this, 'myuserpool', { + // ... + // ... + attributes: { + required: [ StandardAttrs.address, StandardAttrs.name ], + custom: { + 'myappid': new StringAttr({ minLen: 5, maxLen: 15 }), + 'callingcode': new NumberAttr({ min: 1, max: 3 }), + 'isEmployee': new BooleanAttr(), + 'joinedOn': new DateTimeAttr() + }, + } +}); +``` + +As shown in the code snippet, there are data types that are available for custom attributes. The 'String' and 'Number' +data types allow for further constraints on their length and values, respectively. Custom attributes cannot be marked +as required. + ### Importing User Pools Any user pool that has been created outside of this stack, can be imported into the CDK app. Importing a user pool diff --git a/packages/@aws-cdk/aws-cognito/lib/index.ts b/packages/@aws-cdk/aws-cognito/lib/index.ts index 11171b83add28..d19f4fc1882cf 100644 --- a/packages/@aws-cdk/aws-cognito/lib/index.ts +++ b/packages/@aws-cdk/aws-cognito/lib/index.ts @@ -1,5 +1,5 @@ -// AWS::Cognito CloudFormation Resources: export * from './cognito.generated'; export * from './user-pool'; +export * from './user-pool-attr'; export * from './user-pool-client'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts new file mode 100644 index 0000000000000..23e2e11fa6ad1 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -0,0 +1,266 @@ +/** + * Standard attributes + * + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#cognito-user-pools-standard-attributes + */ +export enum StandardAttrs { + /** + * End-User's preferred postal address. + */ + ADDRESS = 'address', + + /** + * End-User's birthday, represented as an ISO 8601:2004 [ISO8601‑2004] YYYY-MM-DD format. + * The year MAY be 0000, indicating that it is omitted. + * To represent only the year, YYYY format is allowed. + */ + BIRTHDATE = 'birthdate', + + /** + * End-User's preferred e-mail address. + * Its value MUST conform to the RFC 5322 [RFC5322] addr-spec syntax. + */ + EMAIL = 'email', + + /** + * Surname(s) or last name(s) of the End-User. + * Note that in some cultures, people can have multiple family names or no family name; + * all can be present, with the names being separated by space characters. + */ + FAMILY_NAME = 'family_name', + + /** + * End-User's gender. + */ + GENDER = 'gender', + + /** + * Given name(s) or first name(s) of the End-User. + * Note that in some cultures, people can have multiple given names; + * all can be present, with the names being separated by space characters. + */ + GIVEN_NAME = 'given_name', + + /** + * End-User's locale, represented as a BCP47 [RFC5646] language tag. + * This is typically an ISO 639-1 Alpha-2 [ISO639‑1] language code in lowercase + * and an ISO 3166-1 Alpha-2 [ISO3166‑1] country code in uppercase, separated by a dash. + * For example, en-US or fr-CA. + */ + LOCALE = 'locale', + + /** + * Middle name(s) of the End-User. + * Note that in some cultures, people can have multiple middle names; + * all can be present, with the names being separated by space characters. + * Also note that in some cultures, middle names are not used. + */ + MIDDLE_NAME = 'middle_name', + + /** + * End-User's full name in displayable form including all name parts, + * possibly including titles and suffixes, ordered according to the End-User's locale and preferences. + */ + NAME = 'name', + + /** + * Casual name of the End-User that may or may not be the same as the given_name. + * For instance, a nickname value of Mike might be returned alongside a given_name value of Michael. + */ + NICKNAME = 'nickname', + + /** + * End-User's preferred telephone number. + * + * Phone numbers must follow these formatting rules: A phone number must start with a plus (+) sign, followed + * immediately by the country code. A phone number can only contain the + sign and digits. You must remove any other + * characters from a phone number, such as parentheses, spaces, or dashes (-) before submitting the value to the + * service. + */ + PHONE_NUMBER = 'phone_number', + + /** + * URL of the End-User's profile picture. + * This URL MUST refer to an image file (for example, a PNG, JPEG, or GIF image file), + * rather than to a Web page containing an image. + * Note that this URL SHOULD specifically reference a profile photo of the End-User + * suitable for displaying when describing the End-User, rather than an arbitrary photo taken by the End-User + */ + PICTURE = 'picture', + + /** + * Shorthand name by which the End-User wishes to be referred to. + */ + PREFERRED_USERNAME = 'preferred_username', + + /** + * URL of the End-User's profile page. The contents of this Web page SHOULD be about the End-User. + */ + PROFILE = 'profile', + + /** + * The End-User's time zone + */ + TIMEZONE = 'zoneinfo', + + /** + * Time the End-User's information was last updated. + * Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z + * as measured in UTC until the date/time. + */ + UPDATED_AT = 'updated_at', + + /** + * URL of the End-User's Web page or blog. + */ + WEBSITE = 'website' +} + +/** + * Represents a custom attribute type. + */ +export interface ICustomAttr { + /** + * Bind this custom attribute type to the values as expected by CloudFormation + */ + bind(): CustomAttrConfig; +} + +/** + * Configuration that will be fed into CloudFormation for any custom attribute type. + */ +export interface CustomAttrConfig { + // tslint:disable:max-line-length + /** + * The data type of the custom attribute. + * + * @see https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_SchemaAttributeType.html#CognitoUserPools-Type-SchemaAttributeType-AttributeDataType + */ + readonly attrDataType: string; + // tslint:enable:max-line-length + + /** + * The constraints attached to this custom attribute. + * The structure here would be the fragment of `CfnUserPool.SchemaAttributeProperty` associated with this data type. + * For example, in the case of the 'String' data type, this would be `{ "stringAttributeConstraints": { "minLength": "..", "maxLength": ".." } }`. + * @see https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_SchemaAttributeType.html + * @default - no constraints + */ + readonly constraints?: { [key: string]: any }; +} + +/** + * Props for constructing a StringAttr + */ +export interface StringAttrProps { + /** + * Minimum length of this attribute. + * @default 0 + */ + readonly minLen?: number; + + /** + * Maximum length of this attribute. + * @default 2048 + */ + readonly maxLen?: number; +} + +/** + * The String custom attribute type. + */ +export class StringAttr implements ICustomAttr { + private readonly minLen?: number; + private readonly maxLen?: number; + + constructor(props: StringAttrProps = {}) { + if (props.minLen && props.minLen < 0) { + throw new Error(`minLen cannot be less than 0 (value: ${props.minLen}).`); + } + if (props.maxLen && props.maxLen > 2048) { + throw new Error(`maxLen cannot be greater than 2048 (value: ${props.maxLen}).`); + } + this.minLen = props?.minLen; + this.maxLen = props?.maxLen; + } + + public bind(): CustomAttrConfig { + const constraints = { + stringAttributeConstraints: { + minLength: this.minLen?.toString(), + maxLength: this.maxLen?.toString(), + } + }; + + return { + attrDataType: 'String', + constraints: (this.minLen || this.maxLen) ? constraints : undefined, + }; + } +} + +/** + * Props for NumberAttr + */ +export interface NumberAttrProps { + /** + * Minimum value of this attribute. + * @default - no minimum value + */ + readonly min?: number; + + /** + * Maximum value of this attribute. + * @default - no maximum value + */ + readonly max?: number; +} + +/** + * The Number custom attribute type. + */ +export class NumberAttr implements ICustomAttr { + private readonly min?: number; + private readonly max?: number; + + constructor(props: NumberAttrProps = {}) { + this.min = props?.min; + this.max = props?.max; + } + + public bind(): CustomAttrConfig { + const constraints = { + numberAttributeConstraints: { + minValue: this.min?.toString(), + maxValue: this.max?.toString(), + } + }; + + return { + attrDataType: 'Number', + constraints: (this.min || this.max) ? constraints : undefined, + }; + } +} + +/** + * The Boolean custom attribute type. + */ +export class BooleanAttr implements ICustomAttr { + public bind(): CustomAttrConfig { + return { + attrDataType: 'Boolean' + }; + } +} + +/** + * The DateTime custom attribute type. + */ +export class DateTimeAttr implements ICustomAttr { + public bind(): CustomAttrConfig { + return { + attrDataType: 'DateTime' + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 43a41541ac136..884d05f207216 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -2,123 +2,7 @@ import { IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from ' import * as lambda from '@aws-cdk/aws-lambda'; import { Construct, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { CfnUserPool } from './cognito.generated'; - -/** - * Standard attributes - * Specified following the OpenID Connect spec - * @see https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims - */ -export enum UserPoolAttribute { - /** - * End-User's preferred postal address. - */ - ADDRESS = 'address', - - /** - * End-User's birthday, represented as an ISO 8601:2004 [ISO8601‑2004] YYYY-MM-DD format. - * The year MAY be 0000, indicating that it is omitted. - * To represent only the year, YYYY format is allowed. - */ - BIRTHDATE = 'birthdate', - - /** - * End-User's preferred e-mail address. - * Its value MUST conform to the RFC 5322 [RFC5322] addr-spec syntax. - */ - EMAIL = 'email', - - /** - * Surname(s) or last name(s) of the End-User. - * Note that in some cultures, people can have multiple family names or no family name; - * all can be present, with the names being separated by space characters. - */ - FAMILY_NAME = 'family_name', - - /** - * End-User's gender. - */ - GENDER = 'gender', - - /** - * Given name(s) or first name(s) of the End-User. - * Note that in some cultures, people can have multiple given names; - * all can be present, with the names being separated by space characters. - */ - GIVEN_NAME = 'given_name', - - /** - * End-User's locale, represented as a BCP47 [RFC5646] language tag. - * This is typically an ISO 639-1 Alpha-2 [ISO639‑1] language code in lowercase - * and an ISO 3166-1 Alpha-2 [ISO3166‑1] country code in uppercase, separated by a dash. - * For example, en-US or fr-CA. - */ - LOCALE = 'locale', - - /** - * Middle name(s) of the End-User. - * Note that in some cultures, people can have multiple middle names; - * all can be present, with the names being separated by space characters. - * Also note that in some cultures, middle names are not used. - */ - MIDDLE_NAME = 'middle_name', - - /** - * End-User's full name in displayable form including all name parts, - * possibly including titles and suffixes, ordered according to the End-User's locale and preferences. - */ - NAME = 'name', - - /** - * Casual name of the End-User that may or may not be the same as the given_name. - * For instance, a nickname value of Mike might be returned alongside a given_name value of Michael. - */ - NICKNAME = 'nickname', - - /** - * End-User's preferred telephone number. - * E.164 [E.164] is RECOMMENDED as the format of this Claim, for example, +1 (425) 555-1212 or +56 (2) 687 2400. - * If the phone number contains an extension, it is RECOMMENDED that the extension be represented using the - * RFC 3966 [RFC3966] extension syntax, for example, +1 (604) 555-1234;ext=5678. - */ - PHONE_NUMBER = 'phone_number', - - /** - * URL of the End-User's profile picture. - * This URL MUST refer to an image file (for example, a PNG, JPEG, or GIF image file), - * rather than to a Web page containing an image. - * Note that this URL SHOULD specifically reference a profile photo of the End-User - * suitable for displaying when describing the End-User, rather than an arbitrary photo taken by the End-User - */ - PICTURE = 'picture', - - /** - * Shorthand name by which the End-User wishes to be referred to. - */ - PREFERRED_USERNAME = 'preferred_username', - - /** - * URL of the End-User's profile page. The contents of this Web page SHOULD be about the End-User. - */ - PROFILE = 'profile', - - /** - * The End-User's time zone - */ - TIMEZONE = 'zoneinfo', - - /** - * Time the End-User's information was last updated. - * Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z - * as measured in UTC until the date/time. - */ - UPDATED_AT = 'updated_at', - - /** - * URL of the End-User's Web page or blog. - * This Web page SHOULD contain information published by the End-User or an organization that the End-User is affiliated with. - */ - WEBSITE = 'website' -} +import { ICustomAttr, StandardAttrs } from './user-pool-attr'; /** * The different ways in which users of this pool can sign up or sign in. @@ -372,6 +256,21 @@ export interface UserPoolProps { */ readonly autoVerify?: AutoVerifiedAttrs; + /** + * The set of attributes that are required for every user in the user pool. + * Read more on attributes here - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html + * + * @default - No attributes are required. + */ + readonly requiredAttrs?: StandardAttrs[]; + + /** + * Define a set of custom attributes that can be configured for each user in the user pool. + * + * @default - No custom attributes. + */ + readonly customAttrs?: { [key: string]: ICustomAttr }; + /** * Lambda functions to use for supported Cognito triggers. * @@ -507,6 +406,7 @@ export class UserPool extends Resource implements IUserPool { emailVerificationSubject, smsVerificationMessage, verificationMessageTemplate, + schema: this.schemaConfiguration(props), }); this.userPoolId = userPool.ref; @@ -647,24 +547,24 @@ export class UserPool extends Resource implements IUserPool { if (signIn.username) { aliasAttrs = []; - if (signIn.email) { aliasAttrs.push(UserPoolAttribute.EMAIL); } - if (signIn.phone) { aliasAttrs.push(UserPoolAttribute.PHONE_NUMBER); } - if (signIn.preferredUsername) { aliasAttrs.push(UserPoolAttribute.PREFERRED_USERNAME); } + if (signIn.email) { aliasAttrs.push(StandardAttrs.EMAIL); } + if (signIn.phone) { aliasAttrs.push(StandardAttrs.PHONE_NUMBER); } + if (signIn.preferredUsername) { aliasAttrs.push(StandardAttrs.PREFERRED_USERNAME); } if (aliasAttrs.length === 0) { aliasAttrs = undefined; } } else { usernameAttrs = []; - if (signIn.email) { usernameAttrs.push(UserPoolAttribute.EMAIL); } - if (signIn.phone) { usernameAttrs.push(UserPoolAttribute.PHONE_NUMBER); } + if (signIn.email) { usernameAttrs.push(StandardAttrs.EMAIL); } + if (signIn.phone) { usernameAttrs.push(StandardAttrs.PHONE_NUMBER); } } if (props.autoVerify) { autoVerifyAttrs = []; - if (props.autoVerify.email) { autoVerifyAttrs.push(UserPoolAttribute.EMAIL); } - if (props.autoVerify.phone) { autoVerifyAttrs.push(UserPoolAttribute.PHONE_NUMBER); } + if (props.autoVerify.email) { autoVerifyAttrs.push(StandardAttrs.EMAIL); } + if (props.autoVerify.phone) { autoVerifyAttrs.push(StandardAttrs.PHONE_NUMBER); } } else if (signIn.email || signIn.phone) { autoVerifyAttrs = []; - if (signIn.email) { autoVerifyAttrs.push(UserPoolAttribute.EMAIL); } - if (signIn.phone) { autoVerifyAttrs.push(UserPoolAttribute.PHONE_NUMBER); } + if (signIn.email) { autoVerifyAttrs.push(StandardAttrs.EMAIL); } + if (signIn.phone) { autoVerifyAttrs.push(StandardAttrs.PHONE_NUMBER); } } return { usernameAttrs, aliasAttrs, autoVerifyAttrs }; @@ -706,4 +606,31 @@ export class UserPool extends Resource implements IUserPool { }; } } + + private schemaConfiguration(props: UserPoolProps): CfnUserPool.SchemaAttributeProperty[] | undefined { + const schema: CfnUserPool.SchemaAttributeProperty[] = []; + + if (props.requiredAttrs) { + schema.push(...props.requiredAttrs.map((attr) => { + return { name: attr, required: true }; + })); + } + + if (props.customAttrs) { + const customAttrs = Object.keys(props.customAttrs).map((attrName) => { + const attrConfig = props.customAttrs![attrName].bind(); + return { + name: attrName, + attributeDataType: attrConfig.attrDataType, + ...attrConfig.constraints, + }; + }); + schema.push(...customAttrs); + } + + if (schema.length === 0) { + return undefined; + } + return schema; + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index 6a1bdf7edcdcf..de2919f4f5931 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -88,7 +88,6 @@ }, "awslint": { "exclude": [ - "no-unused-type:@aws-cdk/aws-cognito.UserPoolAttribute", "props-default-doc:@aws-cdk/aws-cognito.UserPoolTriggers.verifyAuthChallengeResponse", "props-default-doc:@aws-cdk/aws-cognito.UserPoolTriggers.userMigration", "props-default-doc:@aws-cdk/aws-cognito.UserPoolTriggers.preTokenGeneration", diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json index f7b8a29230d8d..90a959a0937a8 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json @@ -58,6 +58,48 @@ "EmailVerificationMessage": "verification email body from the integ test. Code is {####}.", "EmailVerificationSubject": "verification email subject from the integ test", "LambdaConfig": {}, + "Schema": [ + { + "Name": "name", + "Required": true + }, + { + "Name": "email", + "Required": true + }, + { + "AttributeDataType": "String", + "Name": "some-string-attr" + }, + { + "AttributeDataType": "String", + "Name": "another-string-attr", + "StringAttributeConstraints": { + "MaxLength": "100", + "MinLength": "4" + } + }, + { + "AttributeDataType": "Number", + "Name": "some-number-attr" + }, + { + "AttributeDataType": "Number", + "Name": "another-number-attr", + "NumberAttributeConstraints": { + "MaxValue": "50", + "MinValue": "10" + } + }, + { + "AttributeDataType": "Boolean", + "Name": "some-boolean-attr" + }, + { + "AttributeDataType": "DateTime", + "Name": "some-datetime-attr" + } + ], "SmsConfiguration": { "ExternalId": "integuserpoolmyuserpoolDA38443C", "SnsCallerArn": { @@ -77,5 +119,12 @@ } } } + }, + "Outputs": { + "userpoolId": { + "Value": { + "Ref": "myuserpool01998219" + } + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts index 316beb5d3a494..6b7aaa676b77f 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts @@ -1,10 +1,10 @@ -import { App, Stack } from '@aws-cdk/core'; -import { UserPool } from '../lib'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { BooleanAttr, DateTimeAttr, NumberAttr, StandardAttrs, StringAttr, UserPool } from '../lib'; const app = new App(); const stack = new Stack(app, 'integ-user-pool'); -new UserPool(stack, 'myuserpool', { +const userpool = new UserPool(stack, 'myuserpool', { userPoolName: 'MyUserPool', userInvitation: { emailSubject: 'invitation email subject from the integ test', @@ -25,4 +25,17 @@ new UserPool(stack, 'myuserpool', { email: true, phone: true, }, + requiredAttrs: [ StandardAttrs.NAME, StandardAttrs.EMAIL ], + customAttrs: { + 'some-string-attr': new StringAttr(), + 'another-string-attr': new StringAttr({ minLen: 4, maxLen: 100 }), + 'some-number-attr': new NumberAttr(), + 'another-number-attr': new NumberAttr({ min: 10, max: 50 }), + 'some-boolean-attr': new BooleanAttr(), + 'some-datetime-attr': new DateTimeAttr(), + } +}); + +new CfnOutput(stack, 'userpoolId', { + value: userpool.userPoolId }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool.expected.json index a90b415dd7cb9..7b6f4b86e66bd 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool.expected.json @@ -65,5 +65,12 @@ } } } + }, + "Outputs": { + "userpoolid": { + "Value": { + "Ref": "myuserpool01998219" + } + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool.ts index c053d9d5e0d89..565a6b1dd549e 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool.ts @@ -1,9 +1,13 @@ -import { App, Stack } from '@aws-cdk/core'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; import { UserPool } from '../lib'; const app = new App(); const stack = new Stack(app, 'integ-user-pool'); -new UserPool(stack, 'myuserpool', { +const userpool = new UserPool(stack, 'myuserpool', { userPoolName: 'MyUserPool', +}); + +new CfnOutput(stack, 'user-pool-id', { + value: userpool.userPoolId, }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts new file mode 100644 index 0000000000000..94767649da3ce --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts @@ -0,0 +1,71 @@ +import '@aws-cdk/assert/jest'; +import { NumberAttr, StringAttr } from '../lib'; + +describe('User Pool Attributes', () => { + describe('StringAttr', () => { + test('default', () => { + // GIVEN + const attr = new StringAttr(); + + // WHEN + const bound = attr.bind(); + + // THEN + expect(bound.attrDataType).toEqual('String'); + expect(bound.constraints).toBeUndefined(); + }); + + test('specified constraints are recognized', () => { + // GIVEN + const attr = new StringAttr({ minLen: 10, maxLen: 60 }); + + // WHEN + const bound = attr.bind(); + + // THEN + expect(bound.constraints).toEqual({ + stringAttributeConstraints: { + minLength: '10', + maxLength: '60', + } + }); + }); + + test('throws error when crossing limits', () => { + expect(() => new StringAttr({ minLen: -10 })) + .toThrow(/minLen cannot be less than/); + expect(() => new StringAttr({ maxLen: 5000 })) + .toThrow(/maxLen cannot be greater than/); + }); + }); + + describe('NumberAttr', () => { + test('default', () => { + // GIVEN + const attr = new NumberAttr(); + + // WHEN + const bound = attr.bind(); + + // THEN + expect(bound.attrDataType).toEqual('Number'); + expect(bound.constraints).toBeUndefined(); + }); + + test('specified constraints are recognized', () => { + // GIVEN + const attr = new NumberAttr({ min: 5, max: 600 }); + + // WHEN + const bound = attr.bind(); + + // THEN + expect(bound.constraints).toEqual({ + numberAttributeConstraints: { + minValue: '5', + maxValue: '600', + } + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts index 16cfc7b9b051b..ea6906c4423b9 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -3,7 +3,7 @@ import { ABSENT } from '@aws-cdk/assert/lib/assertions/have-resource'; import { Role } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { Stack, Tag } from '@aws-cdk/core'; -import { UserPool, VerificationEmailStyle } from '../lib'; +import { NumberAttr, StandardAttrs, StringAttr, UserPool, VerificationEmailStyle } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -436,4 +436,118 @@ describe('User Pool', () => { AutoVerifiedAttributes: [ 'email', 'phone_number' ], }); }); + + test('required attributes', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + requiredAttrs: [ StandardAttrs.NAME, StandardAttrs.TIMEZONE ] + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + Schema: [ + { + Name: 'name', + Required: true + }, + { + Name: 'zoneinfo', + Required: true + }, + ] + }); + }); + + test('schema is absent when no required attributes are specified', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool1', { + userPoolName: 'Pool1', + }); + new UserPool(stack, 'Pool2', { + userPoolName: 'Pool2', + requiredAttrs: [] + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + UserPoolName: 'Pool1', + Schema: ABSENT + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + UserPoolName: 'Pool2', + Schema: ABSENT + }); + }); + + test('custom attributes with default constraints', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + customAttrs: { + 'custom-string-attr': new StringAttr(), + 'custom-number-attr': new NumberAttr(), + } + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + Schema: [ + { + Name: 'custom-string-attr', + AttributeDataType: 'String', + StringAttributeConstraints: ABSENT, + NumberAttributeConstraints: ABSENT, + }, + { + Name: 'custom-number-attr', + AttributeDataType: 'Number', + StringAttributeConstraints: ABSENT, + NumberAttributeConstraints: ABSENT, + } + ] + }); + }); + + test('custom attributes with constraints', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + customAttrs: { + 'custom-string-attr': new StringAttr({ minLen: 5, maxLen: 50 }), + 'custom-number-attr': new NumberAttr({ min: 500, max: 2000 }), + } + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + Schema: [ + { + AttributeDataType: 'String', + Name: 'custom-string-attr', + StringAttributeConstraints: { + MaxLength: '50', + MinLength: '5', + } + }, + { + AttributeDataType: 'Number', + Name: 'custom-number-attr', + NumberAttributeConstraints: { + MaxValue: '2000', + MinValue: '500', + } + } + ] + }); + }); }); \ No newline at end of file