diff --git a/.changeset/shy-lions-smash.md b/.changeset/shy-lions-smash.md new file mode 100644 index 0000000000..a30b19bca4 --- /dev/null +++ b/.changeset/shy-lions-smash.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-function': patch +--- + +change errors in FunctionFactory to AmplifyUserError diff --git a/.changeset/weak-nails-change.md b/.changeset/weak-nails-change.md new file mode 100644 index 0000000000..72f2675d11 --- /dev/null +++ b/.changeset/weak-nails-change.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/backend-function': patch +--- + +add validation for environment prop diff --git a/packages/backend-function/src/factory.test.ts b/packages/backend-function/src/factory.test.ts index 8b964f336c..df619e0012 100644 --- a/packages/backend-function/src/factory.test.ts +++ b/packages/backend-function/src/factory.test.ts @@ -19,6 +19,7 @@ import { Runtime } from 'aws-cdk-lib/aws-lambda'; import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import fsp from 'fs/promises'; import path from 'node:path'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; const createStackAndSetContext = (): Stack => { const app = new App(); @@ -207,9 +208,10 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', timeoutSeconds: 0, }).getInstance(getInstanceProps), - new Error( - 'timeoutSeconds must be a whole number between 1 and 900 inclusive' - ) + new AmplifyUserError('InvalidTimeoutError', { + message: `Invalid function timeout of 0`, + resolution: `timeoutSeconds must be a whole number between 1 and 900 inclusive`, + }) ); }); @@ -220,9 +222,10 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', timeoutSeconds: 901, }).getInstance(getInstanceProps), - new Error( - 'timeoutSeconds must be a whole number between 1 and 900 inclusive' - ) + new AmplifyUserError('InvalidTimeoutError', { + message: `Invalid function timeout of 901`, + resolution: `timeoutSeconds must be a whole number between 1 and 900 inclusive`, + }) ); }); @@ -233,9 +236,10 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', timeoutSeconds: 10.5, }).getInstance(getInstanceProps), - new Error( - 'timeoutSeconds must be a whole number between 1 and 900 inclusive' - ) + new AmplifyUserError('InvalidTimeoutError', { + message: `Invalid function timeout of 10.5`, + resolution: `timeoutSeconds must be a whole number between 1 and 900 inclusive`, + }) ); }); }); @@ -271,9 +275,10 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', memoryMB: 127, }).getInstance(getInstanceProps), - new Error( - 'memoryMB must be a whole number between 128 and 10240 inclusive' - ) + new AmplifyUserError('InvalidMemoryMBError', { + message: `Invalid function memoryMB of 127`, + resolution: `memoryMB must be a whole number between 128 and 10240 inclusive`, + }) ); }); @@ -284,9 +289,10 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', memoryMB: 10241, }).getInstance(getInstanceProps), - new Error( - 'memoryMB must be a whole number between 128 and 10240 inclusive' - ) + new AmplifyUserError('InvalidMemoryMBError', { + message: `Invalid function memoryMB of 10241`, + resolution: `memoryMB must be a whole number between 128 and 10240 inclusive`, + }) ); }); @@ -297,9 +303,103 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', memoryMB: 256.2, }).getInstance(getInstanceProps), - new Error( - 'memoryMB must be a whole number between 128 and 10240 inclusive' - ) + new AmplifyUserError('InvalidMemoryMBError', { + message: `Invalid function memoryMB of 256.2`, + resolution: `memoryMB must be a whole number between 128 and 10240 inclusive`, + }) + ); + }); + }); + + void describe('environment property', () => { + void it('sets valid environment', () => { + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'myCoolLambda', + environment: { + TEST_ENV: 'testValue', + }, + }); + const lambda = functionFactory.getInstance(getInstanceProps); + const stack = lambda.stack; + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + Environment: { + Variables: { + TEST_ENV: 'testValue', + }, + }, + }); + }); + + void it('sets default environment', () => { + const functionFactory = defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'myCoolLambda', + }); + const lambda = functionFactory.getInstance(getInstanceProps); + const stack = lambda.stack; + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + Environment: {}, + }); + }); + + void it('throws when adding environment variables with invalid key', () => { + assert.throws( + () => + defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'myCoolLambda', + environment: { + 'this.is.wrong': 'testValue', + }, + }).getInstance(getInstanceProps), + new AmplifyUserError('InvalidEnvironmentKeyError', { + message: `Invalid function environment key(s): this.is.wrong`, + resolution: + 'Environment keys must match [a-zA-Z]([a-zA-Z0-9_])+ and be at least 2 characters', + }) + ); + }); + + void it('throws when adding environment variables with key less than 2 characters', () => { + assert.throws( + () => + defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'myCoolLambda', + environment: { + A: 'testValue', + }, + }).getInstance(getInstanceProps), + new AmplifyUserError('InvalidEnvironmentKeyError', { + message: `Invalid function environment key(s): A`, + resolution: + 'Environment keys must match [a-zA-Z]([a-zA-Z0-9_])+ and be at least 2 characters', + }) + ); + }); + + void it('throws when multiple environment variables are invalid', () => { + assert.throws( + () => + defineFunction({ + entry: './test-assets/default-lambda/handler.ts', + name: 'lambdaWithMultipleEnvVars', + environment: { + A: 'testValueA', + TEST_ENV: 'envValue', + 'this.is.wrong': 'testValue', + }, + }).getInstance(getInstanceProps), + new AmplifyUserError('InvalidEnvironmentKeyError', { + message: `Invalid function environment key(s): A, this.is.wrong`, + resolution: + 'Environment keys must match [a-zA-Z]([a-zA-Z0-9_])+ and be at least 2 characters', + }) ); }); }); @@ -335,7 +435,10 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', runtime: 14 as NodeVersion, }).getInstance(getInstanceProps), - new Error('runtime must be one of the following: 16, 18, 20, 22') + new AmplifyUserError('InvalidRuntimeError', { + message: `Invalid function runtime of 14`, + resolution: 'runtime must be one of the following: 16, 18, 20, 22', + }) ); }); diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index e752b46d44..81769e7d6a 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -232,7 +232,7 @@ class FunctionFactory implements ConstructFactory { entry: this.resolveEntry(), timeoutSeconds: this.resolveTimeout(), memoryMB: this.resolveMemory(), - environment: this.props.environment ?? {}, + environment: this.resolveEnvironment(), runtime: this.resolveRuntime(), schedule: this.resolveSchedule(), bundling: this.resolveBundling(), @@ -294,9 +294,10 @@ class FunctionFactory implements ConstructFactory { timeoutMax ) ) { - throw new Error( - `timeoutSeconds must be a whole number between ${timeoutMin} and ${timeoutMax} inclusive` - ); + throw new AmplifyUserError('InvalidTimeoutError', { + message: `Invalid function timeout of ${this.props.timeoutSeconds}`, + resolution: `timeoutSeconds must be a whole number between ${timeoutMin} and ${timeoutMax} inclusive`, + }); } return this.props.timeoutSeconds; }; @@ -311,13 +312,41 @@ class FunctionFactory implements ConstructFactory { if ( !isWholeNumberBetweenInclusive(this.props.memoryMB, memoryMin, memoryMax) ) { - throw new Error( - `memoryMB must be a whole number between ${memoryMin} and ${memoryMax} inclusive` - ); + throw new AmplifyUserError('InvalidMemoryMBError', { + message: `Invalid function memoryMB of ${this.props.memoryMB}`, + resolution: `memoryMB must be a whole number between ${memoryMin} and ${memoryMax} inclusive`, + }); } return this.props.memoryMB; }; + private resolveEnvironment = () => { + if (this.props.environment === undefined) { + return {}; + } + + const invalidKeys: string[] = []; + + Object.keys(this.props.environment).forEach((key) => { + // validate using key pattern from https://docs.aws.amazon.com/lambda/latest/api/API_Environment.html + if (!key.match(/^[a-zA-Z]([a-zA-Z0-9_])+$/)) { + invalidKeys.push(key); + } + }); + + if (invalidKeys.length > 0) { + throw new AmplifyUserError('InvalidEnvironmentKeyError', { + message: `Invalid function environment key(s): ${invalidKeys.join( + ', ' + )}`, + resolution: + 'Environment keys must match [a-zA-Z]([a-zA-Z0-9_])+ and be at least 2 characters', + }); + } + + return this.props.environment; + }; + private resolveRuntime = () => { const runtimeDefault = 18; @@ -327,11 +356,12 @@ class FunctionFactory implements ConstructFactory { } if (!(this.props.runtime in nodeVersionMap)) { - throw new Error( - `runtime must be one of the following: ${Object.keys( + throw new AmplifyUserError('InvalidRuntimeError', { + message: `Invalid function runtime of ${this.props.runtime}`, + resolution: `runtime must be one of the following: ${Object.keys( nodeVersionMap - ).join(', ')}` - ); + ).join(', ')}`, + }); } return this.props.runtime;