From 89fb23ff5398833f7283dda7292dadaa5fb50397 Mon Sep 17 00:00:00 2001 From: Karri Balk Date: Mon, 22 Mar 2021 21:17:00 -0400 Subject: [PATCH 1/6] Add InvokeDryRunE() and its unit test --- modules/aws/lambda.go | 31 +++++++++++++++++++++++ test/terraform_aws_lambda_example_test.go | 8 ++++++ 2 files changed, 39 insertions(+) diff --git a/modules/aws/lambda.go b/modules/aws/lambda.go index e7a9be1b3..c887e23f4 100644 --- a/modules/aws/lambda.go +++ b/modules/aws/lambda.go @@ -49,6 +49,37 @@ func InvokeFunctionE(t testing.TestingT, region, functionName string, payload in return out.Payload, nil } +// InvokeDryRun invokes a lambda function with an invocation type of DryRun +// and returns a status code of 204 if the invocation is permissable. Any +// other status code indicates an error and further details should be available +// in the returned error. +func InvokeDryRunE(t testing.TestingT, region, functionName string, payload interface{}) (int, error) { + lambdaClient, err := NewLambdaClientE(t, region) + if err != nil { + // 401 - Unauthorized. + return 401, err + } + + typeDryRun := lambda.InvocationTypeDryRun + invokeInput := &lambda.InvokeInput{ + FunctionName: &functionName, + InvocationType: &typeDryRun, + } + + if payload != nil { + payloadJson, err := json.Marshal(payload) + + if err != nil { + // 400 - Bad Request + return 400, err + } + invokeInput.Payload = payloadJson + } + + out, err := lambdaClient.Invoke(invokeInput) + return int(*out.StatusCode), err +} + type FunctionError struct { Message string StatusCode int64 diff --git a/test/terraform_aws_lambda_example_test.go b/test/terraform_aws_lambda_example_test.go index aab8594f0..0c99b3aff 100644 --- a/test/terraform_aws_lambda_example_test.go +++ b/test/terraform_aws_lambda_example_test.go @@ -60,6 +60,14 @@ func TestTerraformAwsLambdaExample(t *testing.T) { // Make sure the function-specific error comes back assert.Contains(t, string(functionError.Payload), "Failed to handle") + + // Conduct a "DryRun" invocation to confirm that the user has + // permission to invoke the function. A "DryRun" invocation does + // not execute the function, so the example test function will not + // be checking the payload. + statusCode, err := aws.InvokeDryRunE(t, awsRegion, functionName, ExampleFunctionPayload{ShouldFail: true, Echo: "bye!"}) + require.NoError(t, err) + assert.Equal(t, statusCode, 204) } type ExampleFunctionPayload struct { From 93067816edd63892c67a6c09e299490ac969bfe5 Mon Sep 17 00:00:00 2001 From: Karri Balk Date: Thu, 25 Mar 2021 21:09:45 -0400 Subject: [PATCH 2/6] Add aws.InvokeFunctionWithParams() for invocations Provides a more general solution for other types of Lambda invocations. --- modules/aws/lambda.go | 126 +++++++++++++++------- test/terraform_aws_lambda_example_test.go | 86 +++++++++++++-- 2 files changed, 168 insertions(+), 44 deletions(-) diff --git a/modules/aws/lambda.go b/modules/aws/lambda.go index c887e23f4..7dc270f92 100644 --- a/modules/aws/lambda.go +++ b/modules/aws/lambda.go @@ -2,6 +2,7 @@ package aws import ( "encoding/json" + "errors" "fmt" "github.com/aws/aws-sdk-go/service/lambda" @@ -9,35 +10,60 @@ import ( "github.com/stretchr/testify/require" ) +// LambdaOptions contains additional parameters for InvokeFunctionWithParams(). +// It contains a subset of the fields found in the lambda.InvokeInput struct. +type LambdaOptions struct { + // FunctionName is a required field containing the lambda function name. + FunctionName *string + + // InvocationType can be one of "RequestResponse" or "DryRun". + // * RequestResponse (default) - Invoke the function synchronously. + // Keep the connection open until the function returns a response + // or times out. + // + // * DryRun - Validate parameter values and verify that the user or + // role has permission to invoke the function. + InvocationType *string + + // Lambda function input; will be converted to JSON. + Payload interface{} +} + +// LambdaOutput contains the output from InvokeFunctionWithParams(). The +// fields may or may not have a value depending on the invocation type and +// whether an error occurred or not. +type LambdaOutput struct { + // If present, indicates that an error occurred during function execution. + // Error details are included in the response payload. + FunctionError *string + + // The response from the function, or an error object. + Payload []byte + + // The HTTP status code for a successful request is in the 200 range. + // For RequestResponse invocation type, the status code is 200. + // For the DryRun invocation type, the status code is 204. + StatusCode *int64 +} + // InvokeFunction invokes a lambda function. func InvokeFunction(t testing.TestingT, region, functionName string, payload interface{}) []byte { - out, err := InvokeFunctionE(t, region, functionName, payload) + input := &LambdaOptions{ + FunctionName: &functionName, + Payload: &payload, + } + out, err := InvokeFunctionWithParams(t, region, input) require.NoError(t, err) - return out + return out.Payload } // InvokeFunctionE invokes a lambda function. func InvokeFunctionE(t testing.TestingT, region, functionName string, payload interface{}) ([]byte, error) { - lambdaClient, err := NewLambdaClientE(t, region) - if err != nil { - return nil, err - } - - invokeInput := &lambda.InvokeInput{ + input := &LambdaOptions{ FunctionName: &functionName, + Payload: &payload, } - - if payload != nil { - payloadJson, err := json.Marshal(payload) - - if err != nil { - return nil, err - } - invokeInput.Payload = payloadJson - } - - out, err := lambdaClient.Invoke(invokeInput) - require.NoError(t, err) + out, err := InvokeFunctionWithParams(t, region, input) if err != nil { return nil, err } @@ -49,35 +75,63 @@ func InvokeFunctionE(t testing.TestingT, region, functionName string, payload in return out.Payload, nil } -// InvokeDryRun invokes a lambda function with an invocation type of DryRun -// and returns a status code of 204 if the invocation is permissable. Any -// other status code indicates an error and further details should be available -// in the returned error. -func InvokeDryRunE(t testing.TestingT, region, functionName string, payload interface{}) (int, error) { +// InvokeFunctionWithParams invokes a lambda function using parameters +// supplied in the LambdaOptions struct and returns values in a LambdaOutput +// struct. +func InvokeFunctionWithParams(t testing.TestingT, region string, input *LambdaOptions) (*LambdaOutput, error) { lambdaClient, err := NewLambdaClientE(t, region) if err != nil { - // 401 - Unauthorized. - return 401, err + return nil, err } - typeDryRun := lambda.InvocationTypeDryRun - invokeInput := &lambda.InvokeInput{ - FunctionName: &functionName, - InvocationType: &typeDryRun, + // The function name is a required field in LambdaOptions. If missing, + // report the error. + if input.FunctionName == nil { + msg := "LambdaOptions.FunctionName is a required field" + return &LambdaOutput{FunctionError: &msg}, errors.New(msg) } - if payload != nil { - payloadJson, err := json.Marshal(payload) + // Verify the InvocationType is one of the allowed values and report + // an error if its not. By default the InvocationType will be + // "RequestResponse". + invocationType := lambda.InvocationTypeRequestResponse + if input.InvocationType != nil { + switch *input.InvocationType { + case + lambda.InvocationTypeRequestResponse, + lambda.InvocationTypeDryRun: + invocationType = *input.InvocationType + default: + msg := fmt.Sprintf("LambdaOptions.InvocationType, if specified, must either be \"%s\" or \"%s\"", + lambda.InvocationTypeRequestResponse, + lambda.InvocationTypeDryRun) + return &LambdaOutput{FunctionError: &msg}, errors.New(msg) + } + } + + invokeInput := &lambda.InvokeInput{ + FunctionName: input.FunctionName, + InvocationType: &invocationType, + } + if input.Payload != nil { + payloadJson, err := json.Marshal(input.Payload) if err != nil { - // 400 - Bad Request - return 400, err + return nil, err } invokeInput.Payload = payloadJson } out, err := lambdaClient.Invoke(invokeInput) - return int(*out.StatusCode), err + + // As this function supports different invocation types, so it must + // support different combinations of output. + lambdaOutput := LambdaOutput{ + FunctionError: out.FunctionError, + Payload: out.Payload, + StatusCode: out.StatusCode, + } + return &lambdaOutput, err } type FunctionError struct { diff --git a/test/terraform_aws_lambda_example_test.go b/test/terraform_aws_lambda_example_test.go index 0c99b3aff..b905a5b76 100644 --- a/test/terraform_aws_lambda_example_test.go +++ b/test/terraform_aws_lambda_example_test.go @@ -52,7 +52,7 @@ func TestTerraformAwsLambdaExample(t *testing.T) { assert.Equal(t, `"hi!"`, string(response)) // Invoke the function, this time causing it to error and capturing the error - response, err := aws.InvokeFunctionE(t, awsRegion, functionName, ExampleFunctionPayload{ShouldFail: true, Echo: "hi!"}) + _, err := aws.InvokeFunctionE(t, awsRegion, functionName, ExampleFunctionPayload{ShouldFail: true, Echo: "hi!"}) // Function-specific errors have their own special return functionError, ok := err.(*aws.FunctionError) @@ -60,14 +60,84 @@ func TestTerraformAwsLambdaExample(t *testing.T) { // Make sure the function-specific error comes back assert.Contains(t, string(functionError.Payload), "Failed to handle") +} + +// Annother example of how to test the Terraform module in +// examples/terraform-aws-lambda-example using Terratest, this time with +// the aws.InvokeFunctionWithParams. +func TestTerraformAwsLambdaWithParamsExample(t *testing.T) { + t.Parallel() + + // Give this lambda function a unique ID for a name so we can distinguish it from any other lambdas + // in your AWS account + functionName := fmt.Sprintf("terratest-aws-lambda-withparams-example-%s", random.UniqueId()) + + // Pick a random AWS region to test in. This helps ensure your code works in all regions. + awsRegion := aws.GetRandomStableRegion(t, nil, nil) + + // Construct the terraform options with default retryable errors to handle the most common retryable errors in + // terraform testing. + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + // The path to where our Terraform code is located + TerraformDir: "../examples/terraform-aws-lambda-example", + + // Variables to pass to our Terraform code using -var options + Vars: map[string]interface{}{ + "function_name": functionName, + }, + + // Environment variables to set when running Terraform + EnvVars: map[string]string{ + "AWS_DEFAULT_REGION": awsRegion, + }, + }) + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer terraform.Destroy(t, terraformOptions) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + terraform.InitAndApply(t, terraformOptions) - // Conduct a "DryRun" invocation to confirm that the user has - // permission to invoke the function. A "DryRun" invocation does - // not execute the function, so the example test function will not - // be checking the payload. - statusCode, err := aws.InvokeDryRunE(t, awsRegion, functionName, ExampleFunctionPayload{ShouldFail: true, Echo: "bye!"}) - require.NoError(t, err) - assert.Equal(t, statusCode, 204) + // Call InvokeFunctionWithParms with an InvocationType of "DryRun". + // A "DryRun" invocation does not execute the function, so the example + // test function will not be checking the payload. + invocationType := "DryRun" + input := &aws.LambdaOptions{ + FunctionName: &functionName, + InvocationType: &invocationType, + } + out, err := aws.InvokeFunctionWithParams(t, awsRegion, input) + + // With "DryRun", there's no message in the output, but there is + // a status code which will have a value of 204 for a successful + // invocation. + require.Nil(t, err) + assert.Equal(t, int(*out.StatusCode), 204) + + // Call InvokeFunctionWithParams with a LambdaOptions struct that's + // missing a function name. The function should fail. + input = &aws.LambdaOptions{ + Payload: ExampleFunctionPayload{ShouldFail: false, Echo: "hi!"}, + } + out, err = aws.InvokeFunctionWithParams(t, awsRegion, input) + require.NotNil(t, err) + msg := "LambdaOptions.FunctionName is a required field" + assert.Contains(t, err.Error(), msg) + assert.Contains(t, *out.FunctionError, msg) + + // Call InvokeFunctionWithParams with a LambdaOptions struct with an + // unsupported InvocationType. The function should fail. + invocationType = "Event" + input = &aws.LambdaOptions{ + FunctionName: &functionName, + InvocationType: &invocationType, + Payload: ExampleFunctionPayload{ShouldFail: false, Echo: "hi!"}, + } + out, err = aws.InvokeFunctionWithParams(t, awsRegion, input) + require.NotNil(t, err) + msg = "LambdaOptions.InvocationType, if specified, must either be \"RequestResponse\" or \"DryRun\"" + assert.Contains(t, err.Error(), msg) + assert.Contains(t, *out.FunctionError, msg) } type ExampleFunctionPayload struct { From 9551bd36effdf9e22e23a3c47bfd04993e2e3655 Mon Sep 17 00:00:00 2001 From: Karri Balk Date: Tue, 30 Mar 2021 00:43:44 -0400 Subject: [PATCH 3/6] Create InvokeFunctionWithParamsE - Restore InvokeFunction so it once again calls InvokeFunctionE version InvokeFunctionWithParams - Use functionName as an argument and not a field of the LambdaOptions struct. --- modules/aws/lambda.go | 54 ++++++++++------------- test/terraform_aws_lambda_example_test.go | 41 ++++++++--------- 2 files changed, 44 insertions(+), 51 deletions(-) diff --git a/modules/aws/lambda.go b/modules/aws/lambda.go index 7dc270f92..574f5bc2a 100644 --- a/modules/aws/lambda.go +++ b/modules/aws/lambda.go @@ -13,16 +13,13 @@ import ( // LambdaOptions contains additional parameters for InvokeFunctionWithParams(). // It contains a subset of the fields found in the lambda.InvokeInput struct. type LambdaOptions struct { - // FunctionName is a required field containing the lambda function name. - FunctionName *string - - // InvocationType can be one of "RequestResponse" or "DryRun". - // * RequestResponse (default) - Invoke the function synchronously. - // Keep the connection open until the function returns a response - // or times out. - // - // * DryRun - Validate parameter values and verify that the user or - // role has permission to invoke the function. + // InvocationType can be one of lambda.InvocationTypeRequestResponse + // or lambda.InvocationTypeDryRun. + // * InvocationTypeRequestResponse (default) - Invoke the function + // synchronously. Keep the connection open until the function + // returns a response or times out. + // * InvocationTypeDryRun - Validate parameter values and verify + // that the user or role has permission to invoke the function. InvocationType *string // Lambda function input; will be converted to JSON. @@ -48,22 +45,15 @@ type LambdaOutput struct { // InvokeFunction invokes a lambda function. func InvokeFunction(t testing.TestingT, region, functionName string, payload interface{}) []byte { - input := &LambdaOptions{ - FunctionName: &functionName, - Payload: &payload, - } - out, err := InvokeFunctionWithParams(t, region, input) + out, err := InvokeFunctionE(t, region, functionName, payload) require.NoError(t, err) - return out.Payload + return out } // InvokeFunctionE invokes a lambda function. func InvokeFunctionE(t testing.TestingT, region, functionName string, payload interface{}) ([]byte, error) { - input := &LambdaOptions{ - FunctionName: &functionName, - Payload: &payload, - } - out, err := InvokeFunctionWithParams(t, region, input) + input := &LambdaOptions{Payload: &payload} + out, err := InvokeFunctionWithParamsE(t, region, functionName, input) if err != nil { return nil, err } @@ -77,20 +67,22 @@ func InvokeFunctionE(t testing.TestingT, region, functionName string, payload in // InvokeFunctionWithParams invokes a lambda function using parameters // supplied in the LambdaOptions struct and returns values in a LambdaOutput -// struct. -func InvokeFunctionWithParams(t testing.TestingT, region string, input *LambdaOptions) (*LambdaOutput, error) { +// struct. Checks for failure using "require". +func InvokeFunctionWithParams(t testing.TestingT, region, functionName string, input *LambdaOptions) *LambdaOutput { + out, err := InvokeFunctionWithParamsE(t, region, functionName, input) + require.NoError(t, err) + return out +} + +// InvokeFunctionWithParamsE invokes a lambda function using parameters +// supplied in the LambdaOptions struct and returns values in a LambdaOutput +// struct and the error. +func InvokeFunctionWithParamsE(t testing.TestingT, region, functionName string, input *LambdaOptions) (*LambdaOutput, error) { lambdaClient, err := NewLambdaClientE(t, region) if err != nil { return nil, err } - // The function name is a required field in LambdaOptions. If missing, - // report the error. - if input.FunctionName == nil { - msg := "LambdaOptions.FunctionName is a required field" - return &LambdaOutput{FunctionError: &msg}, errors.New(msg) - } - // Verify the InvocationType is one of the allowed values and report // an error if its not. By default the InvocationType will be // "RequestResponse". @@ -110,7 +102,7 @@ func InvokeFunctionWithParams(t testing.TestingT, region string, input *LambdaOp } invokeInput := &lambda.InvokeInput{ - FunctionName: input.FunctionName, + FunctionName: &functionName, InvocationType: &invocationType, } diff --git a/test/terraform_aws_lambda_example_test.go b/test/terraform_aws_lambda_example_test.go index b905a5b76..683583aae 100644 --- a/test/terraform_aws_lambda_example_test.go +++ b/test/terraform_aws_lambda_example_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/lambda" "github.com/gruntwork-io/terratest/modules/aws" "github.com/gruntwork-io/terratest/modules/random" "github.com/gruntwork-io/terratest/modules/terraform" @@ -101,41 +102,41 @@ func TestTerraformAwsLambdaWithParamsExample(t *testing.T) { // Call InvokeFunctionWithParms with an InvocationType of "DryRun". // A "DryRun" invocation does not execute the function, so the example // test function will not be checking the payload. - invocationType := "DryRun" - input := &aws.LambdaOptions{ - FunctionName: &functionName, - InvocationType: &invocationType, - } - out, err := aws.InvokeFunctionWithParams(t, awsRegion, input) + invocationType := lambda.InvocationTypeDryRun + input := &aws.LambdaOptions{InvocationType: &invocationType} + out := aws.InvokeFunctionWithParams(t, awsRegion, functionName, input) // With "DryRun", there's no message in the output, but there is // a status code which will have a value of 204 for a successful // invocation. - require.Nil(t, err) assert.Equal(t, int(*out.StatusCode), 204) - // Call InvokeFunctionWithParams with a LambdaOptions struct that's - // missing a function name. The function should fail. + // Invoke the function, this time causing the Lambda to error and + // capturing the error. + invocationType = lambda.InvocationTypeRequestResponse input = &aws.LambdaOptions{ - Payload: ExampleFunctionPayload{ShouldFail: false, Echo: "hi!"}, + InvocationType: &invocationType, + Payload: ExampleFunctionPayload{ShouldFail: true, Echo: "hi!"}, } - out, err = aws.InvokeFunctionWithParams(t, awsRegion, input) - require.NotNil(t, err) - msg := "LambdaOptions.FunctionName is a required field" - assert.Contains(t, err.Error(), msg) - assert.Contains(t, *out.FunctionError, msg) + out, err := aws.InvokeFunctionWithParamsE(t, awsRegion, functionName, input) + + // No error in the invocation as Lambda was found and executed. + require.Nil(t, err) + assert.Equal(t, int(*out.StatusCode), 200) + + // Make sure the function-specific error comes back + assert.Contains(t, string(out.Payload), "Failed to handle") - // Call InvokeFunctionWithParams with a LambdaOptions struct with an - // unsupported InvocationType. The function should fail. + // Call InvokeFunctionWithParamsE with a LambdaOptions struct that has + // an unsupported InvocationType. The function should fail. invocationType = "Event" input = &aws.LambdaOptions{ - FunctionName: &functionName, InvocationType: &invocationType, Payload: ExampleFunctionPayload{ShouldFail: false, Echo: "hi!"}, } - out, err = aws.InvokeFunctionWithParams(t, awsRegion, input) + out, err = aws.InvokeFunctionWithParamsE(t, awsRegion, functionName, input) require.NotNil(t, err) - msg = "LambdaOptions.InvocationType, if specified, must either be \"RequestResponse\" or \"DryRun\"" + msg := "LambdaOptions.InvocationType, if specified, must either be \"RequestResponse\" or \"DryRun\"" assert.Contains(t, err.Error(), msg) assert.Contains(t, *out.FunctionError, msg) } From 4c80caa2895feded274ade06a9e21b53ab179053 Mon Sep 17 00:00:00 2001 From: Karri Balk Date: Tue, 30 Mar 2021 17:52:20 -0400 Subject: [PATCH 4/6] Use an enum type for InvocationType --- modules/aws/lambda.go | 48 +++++++++++++++-------- test/terraform_aws_lambda_example_test.go | 5 +-- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/modules/aws/lambda.go b/modules/aws/lambda.go index 574f5bc2a..9ed1cfacb 100644 --- a/modules/aws/lambda.go +++ b/modules/aws/lambda.go @@ -10,17 +10,40 @@ import ( "github.com/stretchr/testify/require" ) +type InvocationTypeOption string + +const ( + InvocationTypeRequestResponse InvocationTypeOption = "RequestResponse" + InvocationTypeDryRun = "DryRun" +) + +func (itype *InvocationTypeOption) Value() (string, error) { + if itype != nil { + switch *itype { + case + InvocationTypeRequestResponse, + InvocationTypeDryRun: + return string(*itype), nil + default: + msg := fmt.Sprintf("LambdaOptions.InvocationType, if specified, must either be \"%s\" or \"%s\"", + InvocationTypeRequestResponse, + InvocationTypeDryRun) + return "", errors.New(msg) + } + } + return string(InvocationTypeRequestResponse), nil +} + // LambdaOptions contains additional parameters for InvokeFunctionWithParams(). // It contains a subset of the fields found in the lambda.InvokeInput struct. type LambdaOptions struct { - // InvocationType can be one of lambda.InvocationTypeRequestResponse - // or lambda.InvocationTypeDryRun. + // InvocationType can be one of InvocationTypeOption values: // * InvocationTypeRequestResponse (default) - Invoke the function // synchronously. Keep the connection open until the function // returns a response or times out. // * InvocationTypeDryRun - Validate parameter values and verify // that the user or role has permission to invoke the function. - InvocationType *string + InvocationType *InvocationTypeOption // Lambda function input; will be converted to JSON. Payload interface{} @@ -84,21 +107,12 @@ func InvokeFunctionWithParamsE(t testing.TestingT, region, functionName string, } // Verify the InvocationType is one of the allowed values and report - // an error if its not. By default the InvocationType will be + // an error if it's not. By default the InvocationType will be // "RequestResponse". - invocationType := lambda.InvocationTypeRequestResponse - if input.InvocationType != nil { - switch *input.InvocationType { - case - lambda.InvocationTypeRequestResponse, - lambda.InvocationTypeDryRun: - invocationType = *input.InvocationType - default: - msg := fmt.Sprintf("LambdaOptions.InvocationType, if specified, must either be \"%s\" or \"%s\"", - lambda.InvocationTypeRequestResponse, - lambda.InvocationTypeDryRun) - return &LambdaOutput{FunctionError: &msg}, errors.New(msg) - } + invocationType, err := input.InvocationType.Value() + if err != nil { + msg := err.Error() + return &LambdaOutput{FunctionError: &msg}, err } invokeInput := &lambda.InvokeInput{ diff --git a/test/terraform_aws_lambda_example_test.go b/test/terraform_aws_lambda_example_test.go index 683583aae..9da241d91 100644 --- a/test/terraform_aws_lambda_example_test.go +++ b/test/terraform_aws_lambda_example_test.go @@ -4,7 +4,6 @@ import ( "fmt" "testing" - "github.com/aws/aws-sdk-go/service/lambda" "github.com/gruntwork-io/terratest/modules/aws" "github.com/gruntwork-io/terratest/modules/random" "github.com/gruntwork-io/terratest/modules/terraform" @@ -102,7 +101,7 @@ func TestTerraformAwsLambdaWithParamsExample(t *testing.T) { // Call InvokeFunctionWithParms with an InvocationType of "DryRun". // A "DryRun" invocation does not execute the function, so the example // test function will not be checking the payload. - invocationType := lambda.InvocationTypeDryRun + var invocationType aws.InvocationTypeOption = aws.InvocationTypeDryRun input := &aws.LambdaOptions{InvocationType: &invocationType} out := aws.InvokeFunctionWithParams(t, awsRegion, functionName, input) @@ -113,7 +112,7 @@ func TestTerraformAwsLambdaWithParamsExample(t *testing.T) { // Invoke the function, this time causing the Lambda to error and // capturing the error. - invocationType = lambda.InvocationTypeRequestResponse + invocationType = aws.InvocationTypeRequestResponse input = &aws.LambdaOptions{ InvocationType: &invocationType, Payload: ExampleFunctionPayload{ShouldFail: true, Echo: "hi!"}, From a6637ff3d119f2d0436867a7b41c8a8f6b18ce6a Mon Sep 17 00:00:00 2001 From: Karri Balk Date: Thu, 1 Apr 2021 11:26:12 -0400 Subject: [PATCH 5/6] Remove FunctionError field from LambdaOutput Restore original InvokeFunctionE(). --- modules/aws/lambda.go | 55 ++++++++++++++++------- test/terraform_aws_lambda_example_test.go | 9 ++-- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/modules/aws/lambda.go b/modules/aws/lambda.go index 9ed1cfacb..634d7290a 100644 --- a/modules/aws/lambda.go +++ b/modules/aws/lambda.go @@ -53,10 +53,6 @@ type LambdaOptions struct { // fields may or may not have a value depending on the invocation type and // whether an error occurred or not. type LambdaOutput struct { - // If present, indicates that an error occurred during function execution. - // Error details are included in the response payload. - FunctionError *string - // The response from the function, or an error object. Payload []byte @@ -75,8 +71,26 @@ func InvokeFunction(t testing.TestingT, region, functionName string, payload int // InvokeFunctionE invokes a lambda function. func InvokeFunctionE(t testing.TestingT, region, functionName string, payload interface{}) ([]byte, error) { - input := &LambdaOptions{Payload: &payload} - out, err := InvokeFunctionWithParamsE(t, region, functionName, input) + lambdaClient, err := NewLambdaClientE(t, region) + if err != nil { + return nil, err + } + + invokeInput := &lambda.InvokeInput{ + FunctionName: &functionName, + } + + if payload != nil { + payloadJson, err := json.Marshal(payload) + + if err != nil { + return nil, err + } + invokeInput.Payload = payloadJson + } + + out, err := lambdaClient.Invoke(invokeInput) + require.NoError(t, err) if err != nil { return nil, err } @@ -98,8 +112,10 @@ func InvokeFunctionWithParams(t testing.TestingT, region, functionName string, i } // InvokeFunctionWithParamsE invokes a lambda function using parameters -// supplied in the LambdaOptions struct and returns values in a LambdaOutput -// struct and the error. +// supplied in the LambdaOptions struct. Returns the status code and payload +// in a LambdaOutput struct and the error. A non-nil error will either reflect +// a problem with the parameters supplied to this function or an error returned +// by the Lambda. func InvokeFunctionWithParamsE(t testing.TestingT, region, functionName string, input *LambdaOptions) (*LambdaOutput, error) { lambdaClient, err := NewLambdaClientE(t, region) if err != nil { @@ -111,8 +127,7 @@ func InvokeFunctionWithParamsE(t testing.TestingT, region, functionName string, // "RequestResponse". invocationType, err := input.InvocationType.Value() if err != nil { - msg := err.Error() - return &LambdaOutput{FunctionError: &msg}, err + return nil, err } invokeInput := &lambda.InvokeInput{ @@ -129,15 +144,23 @@ func InvokeFunctionWithParamsE(t testing.TestingT, region, functionName string, } out, err := lambdaClient.Invoke(invokeInput) + if err != nil { + return nil, err + } - // As this function supports different invocation types, so it must - // support different combinations of output. + // As this function supports different invocation types, it must + // then support different combinations of output other than just + // payload. lambdaOutput := LambdaOutput{ - FunctionError: out.FunctionError, - Payload: out.Payload, - StatusCode: out.StatusCode, + Payload: out.Payload, + StatusCode: out.StatusCode, } - return &lambdaOutput, err + + if out.FunctionError != nil { + return &lambdaOutput, errors.New(*out.FunctionError) + } + + return &lambdaOutput, nil } type FunctionError struct { diff --git a/test/terraform_aws_lambda_example_test.go b/test/terraform_aws_lambda_example_test.go index 9da241d91..fab47b8dd 100644 --- a/test/terraform_aws_lambda_example_test.go +++ b/test/terraform_aws_lambda_example_test.go @@ -119,8 +119,9 @@ func TestTerraformAwsLambdaWithParamsExample(t *testing.T) { } out, err := aws.InvokeFunctionWithParamsE(t, awsRegion, functionName, input) - // No error in the invocation as Lambda was found and executed. - require.Nil(t, err) + // The Lambda executed, but should have failed. + require.NotNil(t, err) + assert.Contains(t, err.Error(), "Unhandled") assert.Equal(t, int(*out.StatusCode), 200) // Make sure the function-specific error comes back @@ -135,9 +136,7 @@ func TestTerraformAwsLambdaWithParamsExample(t *testing.T) { } out, err = aws.InvokeFunctionWithParamsE(t, awsRegion, functionName, input) require.NotNil(t, err) - msg := "LambdaOptions.InvocationType, if specified, must either be \"RequestResponse\" or \"DryRun\"" - assert.Contains(t, err.Error(), msg) - assert.Contains(t, *out.FunctionError, msg) + assert.Contains(t, err.Error(), "LambdaOptions.InvocationType, if specified, must either be \"RequestResponse\" or \"DryRun\"") } type ExampleFunctionPayload struct { From f10e0c2a323828d9a4c580d61374ce4ba7e584c7 Mon Sep 17 00:00:00 2001 From: Karri Balk Date: Fri, 2 Apr 2021 11:48:57 -0400 Subject: [PATCH 6/6] Use different .terraform dir for each lambda test --- test/terraform_aws_lambda_example_test.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/test/terraform_aws_lambda_example_test.go b/test/terraform_aws_lambda_example_test.go index fab47b8dd..1e28afccc 100644 --- a/test/terraform_aws_lambda_example_test.go +++ b/test/terraform_aws_lambda_example_test.go @@ -7,6 +7,7 @@ import ( "github.com/gruntwork-io/terratest/modules/aws" "github.com/gruntwork-io/terratest/modules/random" "github.com/gruntwork-io/terratest/modules/terraform" + test_structure "github.com/gruntwork-io/terratest/modules/test-structure" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -15,6 +16,10 @@ import ( func TestTerraformAwsLambdaExample(t *testing.T) { t.Parallel() + // Make a copy of the terraform module to a temporary directory. This allows running multiple tests in parallel + // against the same terraform module. + exampleFolder := test_structure.CopyTerraformFolderToTemp(t, "../", "examples/terraform-aws-lambda-example") + // Give this lambda function a unique ID for a name so we can distinguish it from any other lambdas // in your AWS account functionName := fmt.Sprintf("terratest-aws-lambda-example-%s", random.UniqueId()) @@ -26,7 +31,7 @@ func TestTerraformAwsLambdaExample(t *testing.T) { // terraform testing. terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ // The path to where our Terraform code is located - TerraformDir: "../examples/terraform-aws-lambda-example", + TerraformDir: exampleFolder, // Variables to pass to our Terraform code using -var options Vars: map[string]interface{}{ @@ -68,6 +73,10 @@ func TestTerraformAwsLambdaExample(t *testing.T) { func TestTerraformAwsLambdaWithParamsExample(t *testing.T) { t.Parallel() + // Make a copy of the terraform module to a temporary directory. This allows running multiple tests in parallel + // against the same terraform module. + exampleFolder := test_structure.CopyTerraformFolderToTemp(t, "../", "examples/terraform-aws-lambda-example") + // Give this lambda function a unique ID for a name so we can distinguish it from any other lambdas // in your AWS account functionName := fmt.Sprintf("terratest-aws-lambda-withparams-example-%s", random.UniqueId()) @@ -79,7 +88,7 @@ func TestTerraformAwsLambdaWithParamsExample(t *testing.T) { // terraform testing. terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ // The path to where our Terraform code is located - TerraformDir: "../examples/terraform-aws-lambda-example", + TerraformDir: exampleFolder, // Variables to pass to our Terraform code using -var options Vars: map[string]interface{}{ @@ -120,9 +129,7 @@ func TestTerraformAwsLambdaWithParamsExample(t *testing.T) { out, err := aws.InvokeFunctionWithParamsE(t, awsRegion, functionName, input) // The Lambda executed, but should have failed. - require.NotNil(t, err) - assert.Contains(t, err.Error(), "Unhandled") - assert.Equal(t, int(*out.StatusCode), 200) + assert.Error(t, err, "Unhandled") // Make sure the function-specific error comes back assert.Contains(t, string(out.Payload), "Failed to handle")