From 1552ea65c7501b9ba4658059473e9a757abca84d Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Mon, 9 Nov 2020 11:07:32 -0800 Subject: [PATCH 001/105] set up integ test folder structure --- .../single/expected_basic_function.json | 4 ++++ .../templates/single/basic_function.yaml | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 test_integ/resources/expected/single/expected_basic_function.json create mode 100644 test_integ/resources/templates/single/basic_function.yaml diff --git a/test_integ/resources/expected/single/expected_basic_function.json b/test_integ/resources/expected/single/expected_basic_function.json new file mode 100644 index 000000000..1c0f5f116 --- /dev/null +++ b/test_integ/resources/expected/single/expected_basic_function.json @@ -0,0 +1,4 @@ +{ + "MyLambdaFunction": "AWS::Lambda::Function", + "MyLambdaFunctionRole": "AWS::IAM::Role" +} \ No newline at end of file diff --git a/test_integ/resources/templates/single/basic_function.yaml b/test_integ/resources/templates/single/basic_function.yaml new file mode 100644 index 000000000..29ce6c6c3 --- /dev/null +++ b/test_integ/resources/templates/single/basic_function.yaml @@ -0,0 +1,19 @@ +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + InlineCode: | + exports.handler = function(event) { + console.log("Hello World"); + } + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Environment: + Variables: + Name: Value + Name2: Value2 + Name2: Value2 \ No newline at end of file From a890b7c268a17763feab741983b39c0fc9ac328d Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Mon, 9 Nov 2020 15:51:12 -0800 Subject: [PATCH 002/105] rename test_integ to tests_integ to keep it consistent with the unitest folder tests --- .../resources/expected/single/expected_basic_function.json | 0 .../resources/templates/single/basic_function.yaml | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {test_integ => tests_integ}/resources/expected/single/expected_basic_function.json (100%) rename {test_integ => tests_integ}/resources/templates/single/basic_function.yaml (100%) diff --git a/test_integ/resources/expected/single/expected_basic_function.json b/tests_integ/resources/expected/single/expected_basic_function.json similarity index 100% rename from test_integ/resources/expected/single/expected_basic_function.json rename to tests_integ/resources/expected/single/expected_basic_function.json diff --git a/test_integ/resources/templates/single/basic_function.yaml b/tests_integ/resources/templates/single/basic_function.yaml similarity index 100% rename from test_integ/resources/templates/single/basic_function.yaml rename to tests_integ/resources/templates/single/basic_function.yaml From 5a16b781b02ca5e0841fcf935e5c748cf6428299 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Mon, 9 Nov 2020 16:19:57 -0800 Subject: [PATCH 003/105] Added test-integ action to Makefile to launch integration tests --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 709820279..c60a5b176 100755 --- a/Makefile +++ b/Makefile @@ -8,6 +8,9 @@ init: test: pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests +test-integ: + pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests_integ + black: black setup.py samtranslator/* tests/* bin/* @@ -27,6 +30,7 @@ Usage: $ make [TARGETS] TARGETS init Initialize and install the requirements and dev-requirements for this project. test Run the Unit tests. + test-integ Run the Integration tests. dev Run all development tests after a change. pr Perform all checks before submitting a Pull Request. From 582262c19595b1fcc9e36ff265be1c3852dada22 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 10 Nov 2020 08:40:44 -0800 Subject: [PATCH 004/105] Added all the single templates, fixed an error in basic_function.yaml --- .../resources/templates/single/basic_api.yaml | 6 ++ .../single/basic_api_inline_openapi.yaml | 27 ++++++ .../single/basic_api_inline_swagger.yaml | 27 ++++++ .../single/basic_api_with_cache.yaml | 12 +++ .../templates/single/basic_api_with_tags.yaml | 9 ++ .../single/basic_application_s3_location.yaml | 5 + .../basic_application_sar_location.yaml | 9 ++ ...lication_sar_location_with_intrinsics.yaml | 57 +++++++++++ .../templates/single/basic_function.yaml | 1 - .../basic_function_event_destinations.yaml | 95 +++++++++++++++++++ .../single/basic_function_no_envvar.yaml | 11 +++ .../single/basic_function_openapi.yaml | 18 ++++ .../single/basic_function_with_kmskeyarn.yaml | 32 +++++++ .../single/basic_function_with_sns_dlq.yaml | 14 +++ .../single/basic_function_with_sqs_dlq.yaml | 15 +++ .../single/basic_function_with_tags.yaml | 14 +++ .../single/basic_function_with_tracing.yaml | 36 +++++++ .../templates/single/basic_http_api.yaml | 11 +++ .../templates/single/basic_layer.yaml | 6 ++ .../single/basic_layer_with_parameters.yaml | 46 +++++++++ ...basic_state_machine_inline_definition.yaml | 23 +++++ .../single/basic_state_machine_with_tags.yaml | 33 +++++++ .../single/basic_table_no_param.yaml | 4 + .../single/basic_table_with_param.yaml | 12 +++ 24 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 tests_integ/resources/templates/single/basic_api.yaml create mode 100644 tests_integ/resources/templates/single/basic_api_inline_openapi.yaml create mode 100644 tests_integ/resources/templates/single/basic_api_inline_swagger.yaml create mode 100644 tests_integ/resources/templates/single/basic_api_with_cache.yaml create mode 100644 tests_integ/resources/templates/single/basic_api_with_tags.yaml create mode 100644 tests_integ/resources/templates/single/basic_application_s3_location.yaml create mode 100644 tests_integ/resources/templates/single/basic_application_sar_location.yaml create mode 100644 tests_integ/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_event_destinations.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_no_envvar.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_openapi.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_with_kmskeyarn.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_with_sns_dlq.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_with_sqs_dlq.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_with_tags.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_with_tracing.yaml create mode 100644 tests_integ/resources/templates/single/basic_http_api.yaml create mode 100644 tests_integ/resources/templates/single/basic_layer.yaml create mode 100644 tests_integ/resources/templates/single/basic_layer_with_parameters.yaml create mode 100644 tests_integ/resources/templates/single/basic_state_machine_inline_definition.yaml create mode 100644 tests_integ/resources/templates/single/basic_state_machine_with_tags.yaml create mode 100644 tests_integ/resources/templates/single/basic_table_no_param.yaml create mode 100644 tests_integ/resources/templates/single/basic_table_with_param.yaml diff --git a/tests_integ/resources/templates/single/basic_api.yaml b/tests_integ/resources/templates/single/basic_api.yaml new file mode 100644 index 000000000..0b6322cec --- /dev/null +++ b/tests_integ/resources/templates/single/basic_api.yaml @@ -0,0 +1,6 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: MyNewStageName + DefinitionUri: ${definitionuri} diff --git a/tests_integ/resources/templates/single/basic_api_inline_openapi.yaml b/tests_integ/resources/templates/single/basic_api_inline_openapi.yaml new file mode 100644 index 000000000..7f14ad5dc --- /dev/null +++ b/tests_integ/resources/templates/single/basic_api_inline_openapi.yaml @@ -0,0 +1,27 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: MyNewStageName + DefinitionBody: + # Simple HTTP Proxy API + openapi: "3.0" + info: + version: "2016-09-23T22:23:23Z" + title: "Simple Api" + basePath: "/demo" + schemes: + - "https" + paths: + /http/{proxy+}: + x-amazon-apigateway-any-method: + parameters: + - name: "proxy" + in: "path" + x-amazon-apigateway-integration: + type: "http_proxy" + uri: "http://httpbin.org/{proxy}" + httpMethod: "ANY" + passthroughBehavior: "when_no_match" + requestParameters: + integration.request.path.proxy: "method.request.path.proxy" diff --git a/tests_integ/resources/templates/single/basic_api_inline_swagger.yaml b/tests_integ/resources/templates/single/basic_api_inline_swagger.yaml new file mode 100644 index 000000000..ce9ff2429 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_api_inline_swagger.yaml @@ -0,0 +1,27 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: MyNewStageName + DefinitionBody: + # Simple HTTP Proxy API + swagger: "2.0" + info: + version: "2016-09-23T22:23:23Z" + title: "Simple Api" + basePath: "/demo" + schemes: + - "https" + paths: + /http/{proxy+}: + x-amazon-apigateway-any-method: + parameters: + - name: "proxy" + in: "path" + x-amazon-apigateway-integration: + type: "http_proxy" + uri: "http://httpbin.org/{proxy}" + httpMethod: "ANY" + passthroughBehavior: "when_no_match" + requestParameters: + integration.request.path.proxy: "method.request.path.proxy" diff --git a/tests_integ/resources/templates/single/basic_api_with_cache.yaml b/tests_integ/resources/templates/single/basic_api_with_cache.yaml new file mode 100644 index 000000000..be6779f9e --- /dev/null +++ b/tests_integ/resources/templates/single/basic_api_with_cache.yaml @@ -0,0 +1,12 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionUri: ${definitionuri} + CacheClusterEnabled: true + CacheClusterSize: '1.6' + Variables: + a: foo + b: bar + diff --git a/tests_integ/resources/templates/single/basic_api_with_tags.yaml b/tests_integ/resources/templates/single/basic_api_with_tags.yaml new file mode 100644 index 000000000..c3bce8d8d --- /dev/null +++ b/tests_integ/resources/templates/single/basic_api_with_tags.yaml @@ -0,0 +1,9 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: my-new-stage-name + DefinitionUri: ${definitionuri} + Tags: + TagKey1: TagValue1 + TagKey2: "" diff --git a/tests_integ/resources/templates/single/basic_application_s3_location.yaml b/tests_integ/resources/templates/single/basic_application_s3_location.yaml new file mode 100644 index 000000000..5a6044674 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_application_s3_location.yaml @@ -0,0 +1,5 @@ +Resources: + MyNestedApp: + Type: AWS::Serverless::Application + Properties: + Location: ${templateurl} diff --git a/tests_integ/resources/templates/single/basic_application_sar_location.yaml b/tests_integ/resources/templates/single/basic_application_sar_location.yaml new file mode 100644 index 000000000..5bd59c2e4 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_application_sar_location.yaml @@ -0,0 +1,9 @@ +Resources: + MyNestedApp: + Type: AWS::Serverless::Application + Properties: + Location: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python + SemanticVersion: 1.0.2 + Parameters: + IdentityNameParameter: test diff --git a/tests_integ/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml b/tests_integ/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml new file mode 100644 index 000000000..471af7ae0 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml @@ -0,0 +1,57 @@ +Parameters: + SemanticVersion: + Type: String + Default: 1.0.2 + +Mappings: + SARApplication: + us-east-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python + us-east-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + us-west-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + us-west-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + eu-central-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + eu-west-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + eu-west-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + eu-west-3: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-south-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-northeast-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-northeast-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-southeast-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-southeast-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ca-central-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + sa-east-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + +Resources: + MyNestedApp: + Type: AWS::Serverless::Application + Properties: + Location: + ApplicationId: + Fn::FindInMap: + - SARApplication + - {Ref: 'AWS::Region'} + - ApplicationId + SemanticVersion: + Ref: SemanticVersion + Parameters: + IdentityNameParameter: test + NotificationARNs: + - Ref: MySns + + MySns: + Type: AWS::SNS::Topic \ No newline at end of file diff --git a/tests_integ/resources/templates/single/basic_function.yaml b/tests_integ/resources/templates/single/basic_function.yaml index 29ce6c6c3..3ec291f00 100644 --- a/tests_integ/resources/templates/single/basic_function.yaml +++ b/tests_integ/resources/templates/single/basic_function.yaml @@ -16,4 +16,3 @@ Resources: Variables: Name: Value Name2: Value2 - Name2: Value2 \ No newline at end of file diff --git a/tests_integ/resources/templates/single/basic_function_event_destinations.yaml b/tests_integ/resources/templates/single/basic_function_event_destinations.yaml new file mode 100644 index 000000000..2129313b1 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_event_destinations.yaml @@ -0,0 +1,95 @@ +Conditions: + QueueCreationDisabled: + Fn::Equals: + - false + - true +Resources: + MyTestFunction: + Type: AWS::Serverless::Function + Properties: + EventInvokeConfig: + MaximumEventAgeInSeconds: 70 + MaximumRetryAttempts: 1 + DestinationConfig: + OnSuccess: + Type: SQS + Destination: + Fn::If: + - QueueCreationDisabled + - Fn::GetAtt: + - DestinationSQS + - Arn + - Ref: 'AWS::NoValue' + OnFailure: + Type: Lambda + Destination: + Fn::GetAtt: + - DestinationLambda + - Arn + InlineCode: | + exports.handler = function(event, context, callback) { + var event_received_at = new Date().toISOString(); + console.log('Event received at: ' + event_received_at); + console.log('Received event:', JSON.stringify(event, null, 2)); + if (event.Success) { + console.log("Success"); + context.callbackWaitsForEmptyEventLoop = false; + callback(null); + } else { + console.log("Failure"); + context.callbackWaitsForEmptyEventLoop = false; + callback(new Error("Failure from event, Success = false, I am failing!"), 'Destination Function Error Thrown'); + } + }; + Handler: index.handler + Runtime: nodejs10.x + MemorySize: 1024 + MyTestFunction2: + Type: AWS::Serverless::Function + Properties: + AutoPublishAlias: live + EventInvokeConfig: + MaximumEventAgeInSeconds: 80 + MaximumRetryAttempts: 2 + DestinationConfig: + OnSuccess: + Type: SNS + OnFailure: + Type: EventBridge + Destination: + Fn::Sub: arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/default + InlineCode: | + exports.handler = function(event, context, callback) { + var event_received_at = new Date().toISOString(); + console.log('Event received at: ' + event_received_at); + console.log('Received event:', JSON.stringify(event, null, 2)); + if (event.Success) { + console.log("Success"); + context.callbackWaitsForEmptyEventLoop = false; + callback(null); + } else { + console.log("Failure"); + context.callbackWaitsForEmptyEventLoop = false; + callback(new Error("Failure from event, Success = false, I am failing!"), 'Destination Function Error Thrown'); + } + }; + Handler: index.handler + Runtime: nodejs10.x + MemorySize: 1024 + DestinationLambda: + Type: AWS::Serverless::Function + Properties: + InlineCode: | + exports.handler = async (event) => { + const response = { + statusCode: 200, + body: JSON.stringify('Hello from Lambda!'), + }; + return response; + }; + Handler: index.handler + Runtime: nodejs10.x + MemorySize: 1024 + DestinationSQS: + Condition: QueueCreationDisabled + Type: AWS::SQS::Queue diff --git a/tests_integ/resources/templates/single/basic_function_no_envvar.yaml b/tests_integ/resources/templates/single/basic_function_no_envvar.yaml new file mode 100644 index 000000000..6ecedfd92 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_no_envvar.yaml @@ -0,0 +1,11 @@ +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess diff --git a/tests_integ/resources/templates/single/basic_function_openapi.yaml b/tests_integ/resources/templates/single/basic_function_openapi.yaml new file mode 100644 index 000000000..359f7d58b --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_openapi.yaml @@ -0,0 +1,18 @@ +Globals: + Api: + OpenApiVersion: 3.0.1 +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Environment: + Variables: + Name: Value + Name2: Value2 diff --git a/tests_integ/resources/templates/single/basic_function_with_kmskeyarn.yaml b/tests_integ/resources/templates/single/basic_function_with_kmskeyarn.yaml new file mode 100644 index 000000000..91a9fc05f --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_with_kmskeyarn.yaml @@ -0,0 +1,32 @@ +Resources: + BasicFunctionWithKmsKeyArn: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Environment: + Variables: + Key: Value + KmsKeyArn: + Fn::GetAtt: [MyKey, Arn] + + + MyKey: + Type: "AWS::KMS::Key" + Properties: + Description: "A sample key" + KeyPolicy: + Version: "2012-10-17" + Id: "key-default-1" + Statement: + - + Sid: "Allow administration of the key" + Effect: "Allow" + Principal: + AWS: + Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: + - "kms:*" + Resource: "*" \ No newline at end of file diff --git a/tests_integ/resources/templates/single/basic_function_with_sns_dlq.yaml b/tests_integ/resources/templates/single/basic_function_with_sns_dlq.yaml new file mode 100644 index 000000000..0d4435fef --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_with_sns_dlq.yaml @@ -0,0 +1,14 @@ +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + DeadLetterQueue: + Type: SNS + TargetArn: + Ref: "MyTopic" + + MyTopic: + Type: AWS::SNS::Topic diff --git a/tests_integ/resources/templates/single/basic_function_with_sqs_dlq.yaml b/tests_integ/resources/templates/single/basic_function_with_sqs_dlq.yaml new file mode 100644 index 000000000..26aa16b01 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_with_sqs_dlq.yaml @@ -0,0 +1,15 @@ + +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + DeadLetterQueue: + Type: SQS + TargetArn: + Fn::GetAtt: ["MyQueue", "Arn"] + + MyQueue: + Type: AWS::SQS::Queue diff --git a/tests_integ/resources/templates/single/basic_function_with_tags.yaml b/tests_integ/resources/templates/single/basic_function_with_tags.yaml new file mode 100644 index 000000000..cc815a52e --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_with_tags.yaml @@ -0,0 +1,14 @@ +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Tags: + TagKey1: TagValue1 + TagKey2: "" diff --git a/tests_integ/resources/templates/single/basic_function_with_tracing.yaml b/tests_integ/resources/templates/single/basic_function_with_tracing.yaml new file mode 100644 index 000000000..f9fa0809c --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_with_tracing.yaml @@ -0,0 +1,36 @@ +Parameters: + Bucket: + Type: String + CodeKey: + Type: String + SwaggerKey: + Type: String + TracingParamPassThrough: + Type: String + Default: PassThrough + +Resources: + ActiveTracingFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Tracing: Active + + PassThroughTracingFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Tracing: + Ref: TracingParamPassThrough diff --git a/tests_integ/resources/templates/single/basic_http_api.yaml b/tests_integ/resources/templates/single/basic_http_api.yaml new file mode 100644 index 000000000..27e3c340d --- /dev/null +++ b/tests_integ/resources/templates/single/basic_http_api.yaml @@ -0,0 +1,11 @@ +Resources: + MyApi: + Type: AWS::Serverless::HttpApi + Properties: + DefinitionBody: + info: + version: '1.0' + title: + Ref: AWS::StackName + openapi: 3.0.1 + paths: {} \ No newline at end of file diff --git a/tests_integ/resources/templates/single/basic_layer.yaml b/tests_integ/resources/templates/single/basic_layer.yaml new file mode 100644 index 000000000..6b308fd91 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_layer.yaml @@ -0,0 +1,6 @@ +Resources: + MyLayerVersion: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: ${contenturi} + RetentionPolicy: Delete diff --git a/tests_integ/resources/templates/single/basic_layer_with_parameters.yaml b/tests_integ/resources/templates/single/basic_layer_with_parameters.yaml new file mode 100644 index 000000000..f5eb8fdb8 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_layer_with_parameters.yaml @@ -0,0 +1,46 @@ +Parameters: + Retention: + Type: String + Default: Retain + License: + Type: String + Default: MIT-0 + Runtimes: + Type: CommaDelimitedList + Default: nodejs12.x,nodejs10.x + LayerName: + Type: String + Default: MyNamedLayerVersion + Description: + Type: String + Default: Some description about this layer goes here + +Resources: + MyLayerVersion: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: ${contenturi} + LayerName: + Ref: LayerName + RetentionPolicy: + Ref: Retention + CompatibleRuntimes: + Ref: Runtimes + LicenseInfo: + Ref: License + Description: + Ref: Description + +Outputs: + MyLayerArn: + Value: + Ref: MyLayerVersion + License: + Value: + Ref: License + Description: + Value: + Ref: Description + LayerName: + Value: + Ref: LayerName \ No newline at end of file diff --git a/tests_integ/resources/templates/single/basic_state_machine_inline_definition.yaml b/tests_integ/resources/templates/single/basic_state_machine_inline_definition.yaml new file mode 100644 index 000000000..8fb8a16cd --- /dev/null +++ b/tests_integ/resources/templates/single/basic_state_machine_inline_definition.yaml @@ -0,0 +1,23 @@ +Resources: + MyBasicStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + Type: STANDARD + Definition: + Comment: A Hello World example of the Amazon States Language using Pass states + StartAt: Hello + States: + Hello: + Type: Pass + Result: Hello + Next: World + World: + Type: Pass + Result: World + End: true + Policies: + - Version: '2012-10-17' + Statement: + - Effect: Deny + Action: "*" + Resource: "*" diff --git a/tests_integ/resources/templates/single/basic_state_machine_with_tags.yaml b/tests_integ/resources/templates/single/basic_state_machine_with_tags.yaml new file mode 100644 index 000000000..815652641 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_state_machine_with_tags.yaml @@ -0,0 +1,33 @@ +Resources: + MyStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + Definition: + Comment: A Hello World example of the Amazon States Language using Pass states + StartAt: Hello + States: + Hello: + Type: Pass + Result: Hello + Next: World + World: + Type: Pass + Result: World + End: true + Policies: + - Version: "2012-10-17" + Statement: + - Effect: Deny + Action: "*" + Resource: "*" + Tags: + TagOne: ValueOne + TagTwo: ValueTwo + Tracing: + Enabled: true + +Outputs: + MyStateMachineArn: + Description: ARN of the state machine + Value: + Ref: MyStateMachine diff --git a/tests_integ/resources/templates/single/basic_table_no_param.yaml b/tests_integ/resources/templates/single/basic_table_no_param.yaml new file mode 100644 index 000000000..ae4fe8164 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_table_no_param.yaml @@ -0,0 +1,4 @@ +Resources: + MyApi: + Type: AWS::Serverless::SimpleTable + # SimpleTable does NOT require any parameters diff --git a/tests_integ/resources/templates/single/basic_table_with_param.yaml b/tests_integ/resources/templates/single/basic_table_with_param.yaml new file mode 100644 index 000000000..c0a7039c3 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_table_with_param.yaml @@ -0,0 +1,12 @@ +Resources: + MyApi: + Type: AWS::Serverless::SimpleTable + Properties: + + PrimaryKey: + Name: mynewid + Type: Number + + ProvisionedThroughput: + ReadCapacityUnits: 2 + WriteCapacityUnits: 2 From 5117bf6c36f5dcd631453eb41aea52fb0486aff4 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 10 Nov 2020 08:45:37 -0800 Subject: [PATCH 005/105] Added test_basic_function.py from the POC, untouched; removed the "expected" prefix of the basic function result as we already are in the expected directory --- ...asic_function.json => basic_function.json} | 0 tests_integ/single/test_basic_function.py | 108 ++++++++++++++++++ 2 files changed, 108 insertions(+) rename tests_integ/resources/expected/single/{expected_basic_function.json => basic_function.json} (100%) create mode 100644 tests_integ/single/test_basic_function.py diff --git a/tests_integ/resources/expected/single/expected_basic_function.json b/tests_integ/resources/expected/single/basic_function.json similarity index 100% rename from tests_integ/resources/expected/single/expected_basic_function.json rename to tests_integ/resources/expected/single/basic_function.json diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py new file mode 100644 index 000000000..5a035b987 --- /dev/null +++ b/tests_integ/single/test_basic_function.py @@ -0,0 +1,108 @@ +import json +import logging +import os +import sys +from functools import reduce +from unittest.case import TestCase + +import boto3 +from samcli.lib.deploy.deployer import Deployer +from samtranslator.model.exceptions import InvalidDocumentException +from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader +from samtranslator.translator.transform import transform +from samtranslator.yaml_helper import yaml_parse + +LOG = logging.getLogger(__name__) +iam_client = boto3.client("iam") +my_path = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, my_path + "/..") + +STACK_NAME = "foss-integ-test" +INPUT_TEMPLATE = "basic_function.yaml" +TRANSLATED_CFN_TEMPLATE = "cfn_basic_function.yaml" +EXPECTED_JSON_FILE = "expected_basic_function.json" + + +# can we import this from sam-translate.py? +def transform_template(input_file_path, output_file_path): + with open(input_file_path, "r") as f: + sam_template = yaml_parse(f) + + try: + cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) + cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) + + with open(output_file_path, "w") as f: + f.write(cloud_formation_template_prettified) + + print("Wrote transformed CloudFormation template to: " + output_file_path) + except InvalidDocumentException as e: + errorMessage = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) + LOG.error(errorMessage) + errors = map(lambda cause: cause.message, e.causes) + LOG.error(errors) + + +def verify_stack_resources(expected_file_path, stack_resources): + with open(expected_file_path, 'r') as expected_data: + expected_resources = json.load(expected_data) + parsed_resources = _parse_stack_resources(stack_resources) + return expected_resources == parsed_resources + + +def _parse_stack_resources(stack_resources): + logic_id_to_resource_type = {} + for resource in stack_resources['StackResourceSummaries']: + logic_id_to_resource_type[resource['LogicalResourceId']] = resource['ResourceType'] + return logic_id_to_resource_type + + +class TestBasicFunction(TestCase): + # set up before every tests run: + # upload test code to s3 + # replace the template's codeuri with s3 location + + # move client set up to here and create template folder to store translated cfn template, or upload template to s3? + # def setUp(self): + # pass + + def test_basic_function(self): + # make stack + # transform sam template to cfn template + cwd = os.getcwd() + input_file_path = os.path.join(cwd, 'resources', 'templates', 'single', INPUT_TEMPLATE) + output_file_path = os.path.join(cwd, TRANSLATED_CFN_TEMPLATE) + expected_resource_path = os.path.join(cwd, EXPECTED_JSON_FILE) + transform_template(input_file_path, output_file_path) + + # cfn part + cloudformation_client = boto3.client("cloudformation") + deployer = Deployer(cloudformation_client, changeset_prefix="foss-integ") + + # deploy to cfn + with open(output_file_path, 'r') as cfn_file: + result, changeset_type = deployer.create_and_wait_for_changeset( + stack_name=STACK_NAME, + cfn_template=cfn_file.read(), + parameter_values=[], + capabilities=['CAPABILITY_IAM'], + role_arn=None, + notification_arns=[], + s3_uploader=None, + tags=[], + ) + deployer.execute_changeset(result["Id"], STACK_NAME) + deployer.wait_for_execute(STACK_NAME, changeset_type) + + # verify + stacks_description = cloudformation_client.describe_stacks(StackName=STACK_NAME) + stack_resources = cloudformation_client.list_stack_resources(StackName=STACK_NAME) + # verify if the stack is create successfully or not + self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') + # verify if the stack contain the expected resources + self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) + + # clean up + # delete stack and delete translated cfn template + # def tearDown(self): + # pass From c070cdce299aa128c6645de79c597a26dcc177ba Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 10 Nov 2020 15:44:55 -0800 Subject: [PATCH 006/105] Added basic api inline openapi test as a standalone file to be merged later into a single basic test file, added utils/stack.py --- .../single/basic_api_inline_openapi.json | 5 + tests_integ/single/__init__.py | 0 .../single/basic_api_inline_openapi.py | 98 +++++++++++++++++++ tests_integ/single/utils/__init__.py | 0 tests_integ/single/utils/stack.py | 24 +++++ 5 files changed, 127 insertions(+) create mode 100644 tests_integ/resources/expected/single/basic_api_inline_openapi.json create mode 100755 tests_integ/single/__init__.py create mode 100644 tests_integ/single/basic_api_inline_openapi.py create mode 100755 tests_integ/single/utils/__init__.py create mode 100644 tests_integ/single/utils/stack.py diff --git a/tests_integ/resources/expected/single/basic_api_inline_openapi.json b/tests_integ/resources/expected/single/basic_api_inline_openapi.json new file mode 100644 index 000000000..b6ca8490f --- /dev/null +++ b/tests_integ/resources/expected/single/basic_api_inline_openapi.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi"}, + { "LogicalResourceId":"MyApiDeployment[0-9a-e]{10}", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/tests_integ/single/__init__.py b/tests_integ/single/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/tests_integ/single/basic_api_inline_openapi.py b/tests_integ/single/basic_api_inline_openapi.py new file mode 100644 index 000000000..958a759ae --- /dev/null +++ b/tests_integ/single/basic_api_inline_openapi.py @@ -0,0 +1,98 @@ +import json +import logging +import os +import sys +from functools import reduce +from unittest.case import TestCase + +import boto3 +from samcli.lib.deploy.deployer import Deployer +from samtranslator.model.exceptions import InvalidDocumentException +from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader +from samtranslator.translator.transform import transform +from samtranslator.yaml_helper import yaml_parse +from utils import stack + +LOG = logging.getLogger(__name__) +iam_client = boto3.client("iam") +my_path = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, my_path + "/..") + +STACK_NAME = "foss-integ-test" +INPUT_TEMPLATE = "basic_api_inline_openapi.yaml" +TRANSLATED_CFN_TEMPLATE = "cfn_basic_api_inline_openapi.yaml" +EXPECTED_JSON_FILE = "basic_api_inline_openapi.json" + +# can we import this from sam-translate.py? +def transform_template(input_file_path, output_file_path): + with open(input_file_path, "r") as f: + sam_template = yaml_parse(f) + + try: + cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) + cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) + + with open(output_file_path, "w") as f: + f.write(cloud_formation_template_prettified) + + print("Wrote transformed CloudFormation template to: " + output_file_path) + except InvalidDocumentException as e: + errorMessage = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) + LOG.error(errorMessage) + errors = map(lambda cause: cause.message, e.causes) + LOG.error(errors) + +class TestBasicFunction(TestCase): + # set up before every tests run: + # upload test code to s3 + # replace the template's codeuri with s3 location + + # move client set up to here and create template folder to store translated cfn template, or upload template to s3? + # def setUp(self): + # pass + + def test_basic_function(self): + # make stack + # transform sam template to cfn template + cwd = os.getcwd() + input_file_path = os.path.join(cwd, 'tests_integ', 'resources', 'templates', 'single', INPUT_TEMPLATE) + output_file_path = os.path.join(cwd, TRANSLATED_CFN_TEMPLATE) + expected_resource_path = os.path.join(cwd, 'tests_integ', 'resources', 'expected', 'single', EXPECTED_JSON_FILE) + transform_template(input_file_path, output_file_path) + + # cfn part + cloudformation_client = boto3.client("cloudformation") + deployer = Deployer(cloudformation_client, changeset_prefix="foss-integ") + + # deploy to cfn + with open(output_file_path, 'r') as cfn_file: + result, changeset_type = deployer.create_and_wait_for_changeset( + stack_name=STACK_NAME, + cfn_template=cfn_file.read(), + parameter_values=[], + capabilities=['CAPABILITY_IAM'], + role_arn=None, + notification_arns=[], + s3_uploader=None, + tags=[], + ) + deployer.execute_changeset(result["Id"], STACK_NAME) + deployer.wait_for_execute(STACK_NAME, changeset_type) + + # verify + stacks_description = cloudformation_client.describe_stacks(StackName=STACK_NAME) + stack_resources = cloudformation_client.list_stack_resources(StackName=STACK_NAME) + # verify if the stack is create successfully or not + self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') + # verify if the stack contain the expected resources + self.assertTrue(stack.verify_stack_resources(expected_resource_path, stack_resources)) + + cloudformation_client.delete_stack(StackName=STACK_NAME) + + # clean up + # delete stack and delete translated cfn template + # def tearDown(self): + # pass + +#test = TestBasicFunction() +#test.test_basic_function() diff --git a/tests_integ/single/utils/__init__.py b/tests_integ/single/utils/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/tests_integ/single/utils/stack.py b/tests_integ/single/utils/stack.py new file mode 100644 index 000000000..ff35cfbdf --- /dev/null +++ b/tests_integ/single/utils/stack.py @@ -0,0 +1,24 @@ +import json +import re + +def verify_stack_resources(expected_file_path, stack_resources): + with open(expected_file_path, 'r') as expected_data: + expected_resources = _sort_resources(json.load(expected_data)) + parsed_resources = _sort_resources(stack_resources['StackResourceSummaries']) + + if len(expected_resources) != len(parsed_resources): + return False + + for i, in enumerate(expected_resources): + exp = expected_resources[i] + parsed = parsed_resources[i] + if parsed["ResourceStatus"] != "CREATE_COMPLETE": + return False + if re.fullmatch(exp["LogicalResourceId"], parsed["LogicalResourceId"]) is None: + return False + if exp["ResourceType"] != parsed["ResourceType"]: + return False + return True + +def _sort_resources(resources): + return sorted(resources, key=lambda d: d["LogicalResourceId"]) From 243edc3dd94a7590134fe6bf9eda3fddbdf0b772 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 10 Nov 2020 15:47:25 -0800 Subject: [PATCH 007/105] Renamed basic api inline openapi test with the test prefix --- ...sic_api_inline_openapi.py => test_basic_api_inline_openapi.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests_integ/single/{basic_api_inline_openapi.py => test_basic_api_inline_openapi.py} (100%) diff --git a/tests_integ/single/basic_api_inline_openapi.py b/tests_integ/single/test_basic_api_inline_openapi.py similarity index 100% rename from tests_integ/single/basic_api_inline_openapi.py rename to tests_integ/single/test_basic_api_inline_openapi.py From 8a9efd279d1683ba716e6ca6ad05df7c76a9fb9f Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 10 Nov 2020 15:58:25 -0800 Subject: [PATCH 008/105] fix: Reverted to range instead of enumerate --- tests_integ/single/utils/stack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests_integ/single/utils/stack.py b/tests_integ/single/utils/stack.py index ff35cfbdf..e6d4ca7c5 100644 --- a/tests_integ/single/utils/stack.py +++ b/tests_integ/single/utils/stack.py @@ -9,7 +9,7 @@ def verify_stack_resources(expected_file_path, stack_resources): if len(expected_resources) != len(parsed_resources): return False - for i, in enumerate(expected_resources): + for i in range(len(expected_resources)): exp = expected_resources[i] parsed = parsed_resources[i] if parsed["ResourceStatus"] != "CREATE_COMPLETE": From b35d9123566a6c2447ce9ab66c4e6d3afafe1962 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Tue, 10 Nov 2020 16:04:08 -0800 Subject: [PATCH 009/105] set up basic code structure for sam integration test --- tests_integ/helpers/__init__.py | 0 tests_integ/helpers/helpers.py | 46 ++++++++++ tests_integ/single/__init__.py | 0 tests_integ/single/test_basic_function.py | 106 ++++++---------------- 4 files changed, 76 insertions(+), 76 deletions(-) create mode 100644 tests_integ/helpers/__init__.py create mode 100644 tests_integ/helpers/helpers.py create mode 100644 tests_integ/single/__init__.py diff --git a/tests_integ/helpers/__init__.py b/tests_integ/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py new file mode 100644 index 000000000..85d32789f --- /dev/null +++ b/tests_integ/helpers/helpers.py @@ -0,0 +1,46 @@ +import json +import logging +from functools import reduce + +import boto3 + +from samtranslator.model.exceptions import InvalidDocumentException +from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader +from samtranslator.translator.transform import transform +from samtranslator.yaml_helper import yaml_parse + + +def transform_template(input_file_path, output_file_path): + LOG = logging.getLogger(__name__) + iam_client = boto3.client("iam") + + with open(input_file_path, "r") as f: + sam_template = yaml_parse(f) + + try: + cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) + cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) + + with open(output_file_path, "w") as f: + f.write(cloud_formation_template_prettified) + + print("Wrote transformed CloudFormation template to: " + output_file_path) + except InvalidDocumentException as e: + errorMessage = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) + LOG.error(errorMessage) + errors = map(lambda cause: cause.message, e.causes) + LOG.error(errors) + + +def verify_stack_resources(expected_file_path, stack_resources): + with open(expected_file_path, 'r') as expected_data: + expected_resources = json.load(expected_data) + parsed_resources = _parse_stack_resources(stack_resources) + return expected_resources == parsed_resources + + +def _parse_stack_resources(stack_resources): + logic_id_to_resource_type = {} + for resource in stack_resources['StackResourceSummaries']: + logic_id_to_resource_type[resource['LogicalResourceId']] = resource['ResourceType'] + return logic_id_to_resource_type \ No newline at end of file diff --git a/tests_integ/single/__init__.py b/tests_integ/single/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index 5a035b987..b8e90837e 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -1,88 +1,41 @@ -import json -import logging import os -import sys -from functools import reduce +from pathlib import Path from unittest.case import TestCase import boto3 +import pytest +from parameterized import parameterized from samcli.lib.deploy.deployer import Deployer -from samtranslator.model.exceptions import InvalidDocumentException -from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader -from samtranslator.translator.transform import transform -from samtranslator.yaml_helper import yaml_parse +from tests_integ.helpers.helpers import transform_template, verify_stack_resources -LOG = logging.getLogger(__name__) -iam_client = boto3.client("iam") -my_path = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, my_path + "/..") - -STACK_NAME = "foss-integ-test" -INPUT_TEMPLATE = "basic_function.yaml" -TRANSLATED_CFN_TEMPLATE = "cfn_basic_function.yaml" -EXPECTED_JSON_FILE = "expected_basic_function.json" - - -# can we import this from sam-translate.py? -def transform_template(input_file_path, output_file_path): - with open(input_file_path, "r") as f: - sam_template = yaml_parse(f) - - try: - cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) - cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) - - with open(output_file_path, "w") as f: - f.write(cloud_formation_template_prettified) - - print("Wrote transformed CloudFormation template to: " + output_file_path) - except InvalidDocumentException as e: - errorMessage = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) - LOG.error(errorMessage) - errors = map(lambda cause: cause.message, e.causes) - LOG.error(errors) - - -def verify_stack_resources(expected_file_path, stack_resources): - with open(expected_file_path, 'r') as expected_data: - expected_resources = json.load(expected_data) - parsed_resources = _parse_stack_resources(stack_resources) - return expected_resources == parsed_resources - - -def _parse_stack_resources(stack_resources): - logic_id_to_resource_type = {} - for resource in stack_resources['StackResourceSummaries']: - logic_id_to_resource_type[resource['LogicalResourceId']] = resource['ResourceType'] - return logic_id_to_resource_type +STACK_NAME_PREFIX = "sam-integ-test-" class TestBasicFunction(TestCase): # set up before every tests run: - # upload test code to s3 - # replace the template's codeuri with s3 location + def setUp(self): + tests_integ_dir = Path(__file__).resolve().parents[1] + self.template_dir = Path(tests_integ_dir, 'resources', 'templates', 'single') + self.output_dir = tests_integ_dir + self.expected_dir = Path(tests_integ_dir, 'resources', 'expected', 'single') - # move client set up to here and create template folder to store translated cfn template, or upload template to s3? - # def setUp(self): - # pass + self.cloudformation_client = boto3.client("cloudformation") + self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") - def test_basic_function(self): - # make stack - # transform sam template to cfn template - cwd = os.getcwd() - input_file_path = os.path.join(cwd, 'resources', 'templates', 'single', INPUT_TEMPLATE) - output_file_path = os.path.join(cwd, TRANSLATED_CFN_TEMPLATE) - expected_resource_path = os.path.join(cwd, EXPECTED_JSON_FILE) - transform_template(input_file_path, output_file_path) + @parameterized.expand([("basic_function.yaml", "cfn_basic_function.yaml", "basic_function.json")]) + @pytest.mark.flaky(reruns=1) # why we need this one? + def test_basic_function(self, input_file_name, out_put_file_name, expected_name): + input_file_path = str(Path(self.template_dir, input_file_name)) + self.output_file_path = str(Path(self.output_dir, out_put_file_name)) + expected_resource_path = str(Path(self.expected_dir, expected_name)) + self.stack_name = STACK_NAME_PREFIX + input_file_name.split('.')[0].replace('_', '-') - # cfn part - cloudformation_client = boto3.client("cloudformation") - deployer = Deployer(cloudformation_client, changeset_prefix="foss-integ") + transform_template(input_file_path, self.output_file_path) # deploy to cfn - with open(output_file_path, 'r') as cfn_file: - result, changeset_type = deployer.create_and_wait_for_changeset( - stack_name=STACK_NAME, + with open(self.output_file_path, 'r') as cfn_file: + result, changeset_type = self.deployer.create_and_wait_for_changeset( + stack_name=self.stack_name, cfn_template=cfn_file.read(), parameter_values=[], capabilities=['CAPABILITY_IAM'], @@ -91,12 +44,12 @@ def test_basic_function(self): s3_uploader=None, tags=[], ) - deployer.execute_changeset(result["Id"], STACK_NAME) - deployer.wait_for_execute(STACK_NAME, changeset_type) + self.deployer.execute_changeset(result["Id"], self.stack_name) + self.deployer.wait_for_execute(self.stack_name, changeset_type) # verify - stacks_description = cloudformation_client.describe_stacks(StackName=STACK_NAME) - stack_resources = cloudformation_client.list_stack_resources(StackName=STACK_NAME) + stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) + stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) # verify if the stack is create successfully or not self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') # verify if the stack contain the expected resources @@ -104,5 +57,6 @@ def test_basic_function(self): # clean up # delete stack and delete translated cfn template - # def tearDown(self): - # pass + def tearDown(self): + self.cloudformation_client.delete_stack(StackName=self.stack_name) + os.remove(self.output_file_path) From 0f79f4bce551f57045c2205c2ab05f6dc91b120d Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Thu, 12 Nov 2020 11:13:42 -0800 Subject: [PATCH 010/105] Updated helpers and expected basic_function file for the new verification. Updated the makefile to not check coverage for integration tests --- Makefile | 4 +-- tests_integ/helpers/helpers.py | 31 ++++++++++++------- .../expected/single/basic_function.json | 8 ++--- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index c60a5b176..be9fd2cb6 100755 --- a/Makefile +++ b/Makefile @@ -6,10 +6,10 @@ init: pip install -e '.[dev]' test: - pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests + pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests/* test-integ: - pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests_integ + pytest --no-cov tests_integ/* black: black setup.py samtranslator/* tests/* bin/* diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 85d32789f..89c32cdfe 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -1,5 +1,6 @@ import json import logging +import re from functools import reduce import boto3 @@ -31,16 +32,24 @@ def transform_template(input_file_path, output_file_path): errors = map(lambda cause: cause.message, e.causes) LOG.error(errors) - def verify_stack_resources(expected_file_path, stack_resources): with open(expected_file_path, 'r') as expected_data: - expected_resources = json.load(expected_data) - parsed_resources = _parse_stack_resources(stack_resources) - return expected_resources == parsed_resources - - -def _parse_stack_resources(stack_resources): - logic_id_to_resource_type = {} - for resource in stack_resources['StackResourceSummaries']: - logic_id_to_resource_type[resource['LogicalResourceId']] = resource['ResourceType'] - return logic_id_to_resource_type \ No newline at end of file + expected_resources = _sort_resources(json.load(expected_data)) + parsed_resources = _sort_resources(stack_resources['StackResourceSummaries']) + + if len(expected_resources) != len(parsed_resources): + return False + + for i in range(len(expected_resources)): + exp = expected_resources[i] + parsed = parsed_resources[i] + if parsed["ResourceStatus"] != "CREATE_COMPLETE": + return False + if re.fullmatch(exp["LogicalResourceId"], parsed["LogicalResourceId"]) is None: + return False + if exp["ResourceType"] != parsed["ResourceType"]: + return False + return True + +def _sort_resources(resources): + return sorted(resources, key=lambda d: d["LogicalResourceId"]) diff --git a/tests_integ/resources/expected/single/basic_function.json b/tests_integ/resources/expected/single/basic_function.json index 1c0f5f116..4e83eaf6b 100644 --- a/tests_integ/resources/expected/single/basic_function.json +++ b/tests_integ/resources/expected/single/basic_function.json @@ -1,4 +1,4 @@ -{ - "MyLambdaFunction": "AWS::Lambda::Function", - "MyLambdaFunctionRole": "AWS::IAM::Role" -} \ No newline at end of file +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } +] From 090560bd7d0378aeea8126f8b10e8c3581c42fd3 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Thu, 12 Nov 2020 15:32:41 -0800 Subject: [PATCH 011/105] add expected json file and fix some bugs --- tests_integ/__init__.py | 0 tests_integ/helpers/helpers.py | 9 ++++++++- .../expected/single/basic_api_inline_openapi.json | 2 +- .../single/basic_function_event_destinations.json | 14 ++++++++++++++ .../expected/single/basic_function_no_envvar.json | 4 ++++ .../expected/single/basic_function_openapi.json | 4 ++++ .../single/basic_function_with_kmskeyarn.json | 5 +++++ .../single/basic_function_with_sns_dlq.json | 5 +++++ .../single/basic_function_with_sqs_dlq.json | 5 +++++ .../expected/single/basic_function_with_tags.json | 4 ++++ .../single/basic_function_with_tracing.json | 6 ++++++ .../resources/expected/single/basic_http_api.json | 4 ++++ .../resources/expected/single/basic_layer.json | 3 +++ .../single/basic_layer_with_parameters.json | 3 +++ .../basic_state_machine_inline_definition.json | 4 ++++ .../single/basic_state_machine_with_tags.json | 0 16 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests_integ/__init__.py create mode 100644 tests_integ/resources/expected/single/basic_function_event_destinations.json create mode 100644 tests_integ/resources/expected/single/basic_function_no_envvar.json create mode 100644 tests_integ/resources/expected/single/basic_function_openapi.json create mode 100644 tests_integ/resources/expected/single/basic_function_with_kmskeyarn.json create mode 100644 tests_integ/resources/expected/single/basic_function_with_sns_dlq.json create mode 100644 tests_integ/resources/expected/single/basic_function_with_sqs_dlq.json create mode 100644 tests_integ/resources/expected/single/basic_function_with_tags.json create mode 100644 tests_integ/resources/expected/single/basic_function_with_tracing.json create mode 100644 tests_integ/resources/expected/single/basic_http_api.json create mode 100644 tests_integ/resources/expected/single/basic_layer.json create mode 100644 tests_integ/resources/expected/single/basic_layer_with_parameters.json create mode 100644 tests_integ/resources/expected/single/basic_state_machine_inline_definition.json create mode 100644 tests_integ/resources/expected/single/basic_state_machine_with_tags.json diff --git a/tests_integ/__init__.py b/tests_integ/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 89c32cdfe..24183d9fb 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -32,6 +32,7 @@ def transform_template(input_file_path, output_file_path): errors = map(lambda cause: cause.message, e.causes) LOG.error(errors) + def verify_stack_resources(expected_file_path, stack_resources): with open(expected_file_path, 'r') as expected_data: expected_resources = _sort_resources(json.load(expected_data)) @@ -44,12 +45,18 @@ def verify_stack_resources(expected_file_path, stack_resources): exp = expected_resources[i] parsed = parsed_resources[i] if parsed["ResourceStatus"] != "CREATE_COMPLETE": + print("---------not complete") return False - if re.fullmatch(exp["LogicalResourceId"], parsed["LogicalResourceId"]) is None: + if not re.fullmatch(exp["LogicalResourceId"]+"([0-9a-f]{10})?", parsed["LogicalResourceId"]): + print("---------id not match") + print(exp["LogicalResourceId"]) + print(parsed["LogicalResourceId"]) return False if exp["ResourceType"] != parsed["ResourceType"]: + print("---------resource not match") return False return True + def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) diff --git a/tests_integ/resources/expected/single/basic_api_inline_openapi.json b/tests_integ/resources/expected/single/basic_api_inline_openapi.json index b6ca8490f..218baf53c 100644 --- a/tests_integ/resources/expected/single/basic_api_inline_openapi.json +++ b/tests_integ/resources/expected/single/basic_api_inline_openapi.json @@ -1,5 +1,5 @@ [ { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi"}, - { "LogicalResourceId":"MyApiDeployment[0-9a-e]{10}", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } ] diff --git a/tests_integ/resources/expected/single/basic_function_event_destinations.json b/tests_integ/resources/expected/single/basic_function_event_destinations.json new file mode 100644 index 000000000..3588c9d13 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_event_destinations.json @@ -0,0 +1,14 @@ +[ + { "LogicalResourceId":"MyTestFunction2", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyTestFunction2EventInvokeConfigOnSuccessTopic", "ResourceType":"AWS::SNS::Topic" }, + { "LogicalResourceId":"MyTestFunctionRole", "ResourceType":"AWS::IAM::Role"}, + { "LogicalResourceId":"MyTestFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyTestFunction2Aliaslive", "ResourceType":"AWS::Lambda::Alias"}, + { "LogicalResourceId":"MyTestFunctionEventInvokeConfigOnSuccessQueue", "ResourceType":"AWS::SQS::Queue"}, + { "LogicalResourceId":"MyTestFunction2Role", "ResourceType":"AWS::IAM::Role"}, + { "LogicalResourceId":"DestinationLambda", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyTestFunction2EventInvokeConfig", "ResourceType":"AWS::Lambda::EventInvokeConfig"}, + { "LogicalResourceId":"MyTestFunction2Version", "ResourceType":"AWS::Lambda::Version"}, + { "LogicalResourceId":"MyTestFunctionEventInvokeConfig", "ResourceType":"AWS::Lambda::EventInvokeConfig"}, + { "LogicalResourceId":"DestinationLambdaRole", "ResourceType":"AWS::IAM::Role"} +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_function_no_envvar.json b/tests_integ/resources/expected/single/basic_function_no_envvar.json new file mode 100644 index 000000000..98d62e4bb --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_no_envvar.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_function_openapi.json b/tests_integ/resources/expected/single/basic_function_openapi.json new file mode 100644 index 000000000..98d62e4bb --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_openapi.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_function_with_kmskeyarn.json b/tests_integ/resources/expected/single/basic_function_with_kmskeyarn.json new file mode 100644 index 000000000..5cba0276b --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_with_kmskeyarn.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"BasicFunctionWithKmsKeyArn", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"BasicFunctionWithKmsKeyArnRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"MyKey", "ResourceType":"AWS::KMS::Key" } +] diff --git a/tests_integ/resources/expected/single/basic_function_with_sns_dlq.json b/tests_integ/resources/expected/single/basic_function_with_sns_dlq.json new file mode 100644 index 000000000..3ded5dd12 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_with_sns_dlq.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyFunctionRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"MyTopic", "ResourceType":"AWS::SNS::Topic" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_function_with_sqs_dlq.json b/tests_integ/resources/expected/single/basic_function_with_sqs_dlq.json new file mode 100644 index 000000000..a29c8734b --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_with_sqs_dlq.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyFunctionRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"MyQueue", "ResourceType":"AWS::SQS::Queue" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_function_with_tags.json b/tests_integ/resources/expected/single/basic_function_with_tags.json new file mode 100644 index 000000000..4e83eaf6b --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_with_tags.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } +] diff --git a/tests_integ/resources/expected/single/basic_function_with_tracing.json b/tests_integ/resources/expected/single/basic_function_with_tracing.json new file mode 100644 index 000000000..e47b43ddd --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_with_tracing.json @@ -0,0 +1,6 @@ +[ + { "LogicalResourceId":"ActiveTracingFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"ActiveTracingFunctionRole", "ResourceType":"AWS::IAM::Role"}, + { "LogicalResourceId":"PassThroughTracingFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"PassThroughTracingFunctionRole", "ResourceType":"AWS::IAM::Role" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_http_api.json b/tests_integ/resources/expected/single/basic_http_api.json new file mode 100644 index 000000000..7fac895cb --- /dev/null +++ b/tests_integ/resources/expected/single/basic_http_api.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGatewayV2::Api"}, + { "LogicalResourceId":"MyApiApiGatewayDefaultStage", "ResourceType":"AWS::ApiGatewayV2::Stage" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_layer.json b/tests_integ/resources/expected/single/basic_layer.json new file mode 100644 index 000000000..123db2316 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_layer.json @@ -0,0 +1,3 @@ +[ + { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"}, +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_layer_with_parameters.json b/tests_integ/resources/expected/single/basic_layer_with_parameters.json new file mode 100644 index 000000000..123db2316 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_layer_with_parameters.json @@ -0,0 +1,3 @@ +[ + { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"}, +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_state_machine_inline_definition.json b/tests_integ/resources/expected/single/basic_state_machine_inline_definition.json new file mode 100644 index 000000000..4d778a7e4 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_state_machine_inline_definition.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyBasicStateMachine", "ResourceType":"AWS::StepFunctions::StateMachine"}, + { "LogicalResourceId":"MyBasicStateMachineRole", "ResourceType":"AWS::IAM::Role" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_state_machine_with_tags.json b/tests_integ/resources/expected/single/basic_state_machine_with_tags.json new file mode 100644 index 000000000..e69de29bb From f7ee9688e5a6b1c48126911eca8d8d1907da2fa5 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Thu, 12 Nov 2020 15:38:37 -0800 Subject: [PATCH 012/105] remove unnecessary print statement --- tests_integ/helpers/helpers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 24183d9fb..2d6663c9b 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -45,15 +45,10 @@ def verify_stack_resources(expected_file_path, stack_resources): exp = expected_resources[i] parsed = parsed_resources[i] if parsed["ResourceStatus"] != "CREATE_COMPLETE": - print("---------not complete") return False if not re.fullmatch(exp["LogicalResourceId"]+"([0-9a-f]{10})?", parsed["LogicalResourceId"]): - print("---------id not match") - print(exp["LogicalResourceId"]) - print(parsed["LogicalResourceId"]) return False if exp["ResourceType"] != parsed["ResourceType"]: - print("---------resource not match") return False return True From 2bde98589540d46e07d30006ecc0506838a9b98a Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Thu, 12 Nov 2020 15:41:06 -0800 Subject: [PATCH 013/105] update basic function test --- tests_integ/single/test_basic_function.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index b8e90837e..6b42a9f6d 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -22,11 +22,12 @@ def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") - @parameterized.expand([("basic_function.yaml", "cfn_basic_function.yaml", "basic_function.json")]) - @pytest.mark.flaky(reruns=1) # why we need this one? - def test_basic_function(self, input_file_name, out_put_file_name, expected_name): + @parameterized.expand([("basic_function.yaml", "basic_function.json"), + ("basic_function_event_destinations.yaml", "basic_function_event_destinations.json") + ]) + def test_basic_function(self, input_file_name, expected_name): input_file_path = str(Path(self.template_dir, input_file_name)) - self.output_file_path = str(Path(self.output_dir, out_put_file_name)) + self.output_file_path = str(Path(self.output_dir, 'cfn_' + input_file_name)) expected_resource_path = str(Path(self.expected_dir, expected_name)) self.stack_name = STACK_NAME_PREFIX + input_file_name.split('.')[0].replace('_', '-') @@ -59,4 +60,5 @@ def test_basic_function(self, input_file_name, out_put_file_name, expected_name) # delete stack and delete translated cfn template def tearDown(self): self.cloudformation_client.delete_stack(StackName=self.stack_name) - os.remove(self.output_file_path) + if os.path.exists(self.output_file_path): + os.remove(self.output_file_path) From 65c2ebdd323801212af00d18f7e66f4cbd6428d6 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Thu, 12 Nov 2020 16:39:59 -0800 Subject: [PATCH 014/105] Added missing expected files, updated runner --- .../resources/expected/single/basic_api.json | 5 + .../single/basic_api_inline_swagger.json | 5 + .../single/basic_api_inline_with_cache.json | 5 + .../single/basic_api_inline_with_tags.json | 5 + .../basic_application_sar_location.json | 3 + ...lication_sar_location_with_intrinsics.json | 4 + .../expected/single/basic_function.json | 2 +- .../single/test_basic_api_inline_openapi.py | 112 +++++++----------- 8 files changed, 72 insertions(+), 69 deletions(-) create mode 100644 tests_integ/resources/expected/single/basic_api.json create mode 100644 tests_integ/resources/expected/single/basic_api_inline_swagger.json create mode 100644 tests_integ/resources/expected/single/basic_api_inline_with_cache.json create mode 100644 tests_integ/resources/expected/single/basic_api_inline_with_tags.json create mode 100644 tests_integ/resources/expected/single/basic_application_sar_location.json create mode 100644 tests_integ/resources/expected/single/basic_application_sar_location_with_intrinsics.json diff --git a/tests_integ/resources/expected/single/basic_api.json b/tests_integ/resources/expected/single/basic_api.json new file mode 100644 index 000000000..d45cb8ce3 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_api.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/tests_integ/resources/expected/single/basic_api_inline_swagger.json b/tests_integ/resources/expected/single/basic_api_inline_swagger.json new file mode 100644 index 000000000..d45cb8ce3 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_api_inline_swagger.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/tests_integ/resources/expected/single/basic_api_inline_with_cache.json b/tests_integ/resources/expected/single/basic_api_inline_with_cache.json new file mode 100644 index 000000000..d45cb8ce3 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_api_inline_with_cache.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/tests_integ/resources/expected/single/basic_api_inline_with_tags.json b/tests_integ/resources/expected/single/basic_api_inline_with_tags.json new file mode 100644 index 000000000..84d8e643f --- /dev/null +++ b/tests_integ/resources/expected/single/basic_api_inline_with_tags.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/tests_integ/resources/expected/single/basic_application_sar_location.json b/tests_integ/resources/expected/single/basic_application_sar_location.json new file mode 100644 index 000000000..a033c90b7 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_application_sar_location.json @@ -0,0 +1,3 @@ +[ + { "LogicalResourceId":"MyNestedApp", "ResourceType":"AWS::CloudFormation::Stack" } +] diff --git a/tests_integ/resources/expected/single/basic_application_sar_location_with_intrinsics.json b/tests_integ/resources/expected/single/basic_application_sar_location_with_intrinsics.json new file mode 100644 index 000000000..884f93d12 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_application_sar_location_with_intrinsics.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyNestedApp", "ResourceType":"AWS::CloudFormation::Stack" }, + { "LogicalResourceId":"MySns", "ResourceType":"AWS::SNS::Topic" } +] diff --git a/tests_integ/resources/expected/single/basic_function.json b/tests_integ/resources/expected/single/basic_function.json index 4e83eaf6b..2cb129f54 100644 --- a/tests_integ/resources/expected/single/basic_function.json +++ b/tests_integ/resources/expected/single/basic_function.json @@ -1,4 +1,4 @@ [ - { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function" }, { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } ] diff --git a/tests_integ/single/test_basic_api_inline_openapi.py b/tests_integ/single/test_basic_api_inline_openapi.py index 958a759ae..82e7b1a9e 100644 --- a/tests_integ/single/test_basic_api_inline_openapi.py +++ b/tests_integ/single/test_basic_api_inline_openapi.py @@ -1,98 +1,74 @@ -import json -import logging import os -import sys -from functools import reduce +from pathlib import Path from unittest.case import TestCase import boto3 +import pytest +from parameterized import parameterized from samcli.lib.deploy.deployer import Deployer -from samtranslator.model.exceptions import InvalidDocumentException -from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader -from samtranslator.translator.transform import transform -from samtranslator.yaml_helper import yaml_parse -from utils import stack +from tests_integ.helpers.helpers import transform_template, verify_stack_resources -LOG = logging.getLogger(__name__) -iam_client = boto3.client("iam") -my_path = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, my_path + "/..") +STACK_NAME_PREFIX = "sam-integ-test-" -STACK_NAME = "foss-integ-test" -INPUT_TEMPLATE = "basic_api_inline_openapi.yaml" -TRANSLATED_CFN_TEMPLATE = "cfn_basic_api_inline_openapi.yaml" -EXPECTED_JSON_FILE = "basic_api_inline_openapi.json" - -# can we import this from sam-translate.py? -def transform_template(input_file_path, output_file_path): - with open(input_file_path, "r") as f: - sam_template = yaml_parse(f) - - try: - cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) - cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) - - with open(output_file_path, "w") as f: - f.write(cloud_formation_template_prettified) - - print("Wrote transformed CloudFormation template to: " + output_file_path) - except InvalidDocumentException as e: - errorMessage = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) - LOG.error(errorMessage) - errors = map(lambda cause: cause.message, e.causes) - LOG.error(errors) - -class TestBasicFunction(TestCase): +class TestBasicApiInlineOpenapi(TestCase): # set up before every tests run: - # upload test code to s3 - # replace the template's codeuri with s3 location - - # move client set up to here and create template folder to store translated cfn template, or upload template to s3? - # def setUp(self): - # pass - - def test_basic_function(self): - # make stack - # transform sam template to cfn template - cwd = os.getcwd() - input_file_path = os.path.join(cwd, 'tests_integ', 'resources', 'templates', 'single', INPUT_TEMPLATE) - output_file_path = os.path.join(cwd, TRANSLATED_CFN_TEMPLATE) - expected_resource_path = os.path.join(cwd, 'tests_integ', 'resources', 'expected', 'single', EXPECTED_JSON_FILE) - transform_template(input_file_path, output_file_path) + def setUp(self): + tests_integ_dir = Path(__file__).resolve().parents[1] + self.template_dir = Path(tests_integ_dir, 'resources', 'templates', 'single') + self.output_dir = tests_integ_dir + self.expected_dir = Path(tests_integ_dir, 'resources', 'expected', 'single') + + self.cloudformation_client = boto3.client("cloudformation") + self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") + + @parameterized.expand([ + ("basic_api_inline_openapi.yaml", "cfn_basic_api_inline_openapi.yaml", "basic_api_inline_openapi.json"), + ("basic_api_inline_swagger.yaml", "cfn_basic_api_inline_swagger.yaml", "basic_api_inline_swagger.json"), + # These three require a template replacement of ${definitionuri} + # ("basic_api_with_cache.yaml", "cfn_basic_api_with_cache.yaml", "basic_api_with_cache.json"), + # ("basic_api_with_tags.yaml", "cfn_basic_api_with_tags.yaml", "basic_api_with_tags.json"), + # ("basic_api.yaml", "cfn_basic_api.yaml", "basic_api.json"), + ("basic_application_sar_location.yaml", "cfn_basic_application_sar_location.yaml", "basic_application_sar_location.json"), + ("basic_application_sar_location_with_intrinsics.yaml", "cfn_basic_application_sar_location_with_intrinsics.yaml", "basic_application_sar_location_with_intrinsics.json"), + ]) + def test_basic_api(self, input_file_name, out_put_file_name, expected_name): + input_file_path = str(Path(self.template_dir, input_file_name)) + self.output_file_path = str(Path(self.output_dir, out_put_file_name)) + expected_resource_path = str(Path(self.expected_dir, expected_name)) + self.stack_name = STACK_NAME_PREFIX + input_file_name.split('.')[0].replace('_', '-') + + transform_template(input_file_path, self.output_file_path) # cfn part cloudformation_client = boto3.client("cloudformation") deployer = Deployer(cloudformation_client, changeset_prefix="foss-integ") # deploy to cfn - with open(output_file_path, 'r') as cfn_file: + with open(self.output_file_path, 'r') as cfn_file: result, changeset_type = deployer.create_and_wait_for_changeset( - stack_name=STACK_NAME, + stack_name=self.stack_name, cfn_template=cfn_file.read(), parameter_values=[], - capabilities=['CAPABILITY_IAM'], + capabilities=['CAPABILITY_IAM', 'CAPABILITY_AUTO_EXPAND'], role_arn=None, notification_arns=[], s3_uploader=None, tags=[], ) - deployer.execute_changeset(result["Id"], STACK_NAME) - deployer.wait_for_execute(STACK_NAME, changeset_type) + deployer.execute_changeset(result["Id"], self.stack_name) + deployer.wait_for_execute(self.stack_name, changeset_type) # verify - stacks_description = cloudformation_client.describe_stacks(StackName=STACK_NAME) - stack_resources = cloudformation_client.list_stack_resources(StackName=STACK_NAME) + stacks_description = cloudformation_client.describe_stacks(StackName=self.stack_name) + stack_resources = cloudformation_client.list_stack_resources(StackName=self.stack_name) # verify if the stack is create successfully or not self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') # verify if the stack contain the expected resources - self.assertTrue(stack.verify_stack_resources(expected_resource_path, stack_resources)) - - cloudformation_client.delete_stack(StackName=STACK_NAME) + self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) # clean up # delete stack and delete translated cfn template - # def tearDown(self): - # pass - -#test = TestBasicFunction() -#test.test_basic_function() + def tearDown(self): + self.cloudformation_client.delete_stack(StackName=self.stack_name) + if os.path.exists(self.output_file_path): + os.remove(self.output_file_path) From b763e7e59124f1e6dc9e131028a61c7feae90b9e Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Thu, 12 Nov 2020 16:52:33 -0800 Subject: [PATCH 015/105] Removed the ResourceStatus check for each resource --- tests_integ/helpers/helpers.py | 5 +---- .../resources/expected/single/basic_api_inline_openapi.json | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 2d6663c9b..91a08ec25 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -44,14 +44,11 @@ def verify_stack_resources(expected_file_path, stack_resources): for i in range(len(expected_resources)): exp = expected_resources[i] parsed = parsed_resources[i] - if parsed["ResourceStatus"] != "CREATE_COMPLETE": - return False - if not re.fullmatch(exp["LogicalResourceId"]+"([0-9a-f]{10})?", parsed["LogicalResourceId"]): + if not re.fullmatch(exp["LogicalResourceId"] + "([0-9a-f]{10})?", parsed["LogicalResourceId"]): return False if exp["ResourceType"] != parsed["ResourceType"]: return False return True - def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) diff --git a/tests_integ/resources/expected/single/basic_api_inline_openapi.json b/tests_integ/resources/expected/single/basic_api_inline_openapi.json index 218baf53c..d45cb8ce3 100644 --- a/tests_integ/resources/expected/single/basic_api_inline_openapi.json +++ b/tests_integ/resources/expected/single/basic_api_inline_openapi.json @@ -1,5 +1,5 @@ [ - { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi"}, + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } ] From 5a11d35b928e62017f52cd180cdd8cf1284f0be0 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Thu, 12 Nov 2020 17:22:18 -0800 Subject: [PATCH 016/105] Refactored the test to inherit a BaseTest class, moved the basic_application tests to their own file, fixed some names --- tests_integ/helpers/base_test.py | 59 +++++++++++++++ tests_integ/single/test_basic_api.py | 16 ++++ .../single/test_basic_api_inline_openapi.py | 74 ------------------- tests_integ/single/test_basic_application.py | 12 +++ tests_integ/single/test_basic_function.py | 70 +++--------------- tests_integ/single/utils/__init__.py | 0 tests_integ/single/utils/stack.py | 24 ------ 7 files changed, 96 insertions(+), 159 deletions(-) create mode 100644 tests_integ/helpers/base_test.py create mode 100644 tests_integ/single/test_basic_api.py delete mode 100644 tests_integ/single/test_basic_api_inline_openapi.py create mode 100644 tests_integ/single/test_basic_application.py delete mode 100755 tests_integ/single/utils/__init__.py delete mode 100644 tests_integ/single/utils/stack.py diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py new file mode 100644 index 000000000..e23b79705 --- /dev/null +++ b/tests_integ/helpers/base_test.py @@ -0,0 +1,59 @@ +import os +from pathlib import Path +from unittest.case import TestCase + +import boto3 +import pytest +from samcli.lib.deploy.deployer import Deployer +from tests_integ.helpers.helpers import transform_template, verify_stack_resources + +STACK_NAME_PREFIX = "sam-integ-test-" + +class BaseTest(TestCase): + output_file_path = "" + stack_name = "" + + def setUp(self): + tests_integ_dir = Path(__file__).resolve().parents[1] + self.template_dir = Path(tests_integ_dir, 'resources', 'templates', 'single') + self.output_dir = tests_integ_dir + self.expected_dir = Path(tests_integ_dir, 'resources', 'expected', 'single') + + self.cloudformation_client = boto3.client("cloudformation") + self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") + + def create_and_verify_stack(self, file_name): + input_file_path = str(Path(self.template_dir, file_name + ".yaml")) + self.output_file_path = str(Path(self.output_dir, 'cfn_' + file_name + ".yaml")) + expected_resource_path = str(Path(self.expected_dir, file_name + ".json")) + self.stack_name = STACK_NAME_PREFIX + file_name.replace('_', '-') + + transform_template(input_file_path, self.output_file_path) + + # deploy to cfn + with open(self.output_file_path, 'r') as cfn_file: + result, changeset_type = self.deployer.create_and_wait_for_changeset( + stack_name=self.stack_name, + cfn_template=cfn_file.read(), + parameter_values=[], + capabilities=['CAPABILITY_IAM', 'CAPABILITY_AUTO_EXPAND'], + role_arn=None, + notification_arns=[], + s3_uploader=None, + tags=[], + ) + self.deployer.execute_changeset(result["Id"], self.stack_name) + self.deployer.wait_for_execute(self.stack_name, changeset_type) + + # verify + stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) + stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) + # verify if the stack is create successfully or not + self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') + # verify if the stack contain the expected resources + self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) + + def tearDown(self): + self.cloudformation_client.delete_stack(StackName=self.stack_name) + if os.path.exists(self.output_file_path): + os.remove(self.output_file_path) diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py new file mode 100644 index 000000000..0a9bc0905 --- /dev/null +++ b/tests_integ/single/test_basic_api.py @@ -0,0 +1,16 @@ +import os + +from parameterized import parameterized +from tests_integ.helpers.base_test import BaseTest + +class TestBasicApi(BaseTest): + @parameterized.expand([ + ("basic_api_inline_openapi"), + ("basic_api_inline_swagger"), + # These three require a template replacement of ${definitionuri} + # ("basic_api_with_cache"), + # ("basic_api_with_tags"), + # ("basic_api.yaml"), + ]) + def test_basic_api(self, file_name): + self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_api_inline_openapi.py b/tests_integ/single/test_basic_api_inline_openapi.py deleted file mode 100644 index 82e7b1a9e..000000000 --- a/tests_integ/single/test_basic_api_inline_openapi.py +++ /dev/null @@ -1,74 +0,0 @@ -import os -from pathlib import Path -from unittest.case import TestCase - -import boto3 -import pytest -from parameterized import parameterized -from samcli.lib.deploy.deployer import Deployer -from tests_integ.helpers.helpers import transform_template, verify_stack_resources - -STACK_NAME_PREFIX = "sam-integ-test-" - -class TestBasicApiInlineOpenapi(TestCase): - # set up before every tests run: - def setUp(self): - tests_integ_dir = Path(__file__).resolve().parents[1] - self.template_dir = Path(tests_integ_dir, 'resources', 'templates', 'single') - self.output_dir = tests_integ_dir - self.expected_dir = Path(tests_integ_dir, 'resources', 'expected', 'single') - - self.cloudformation_client = boto3.client("cloudformation") - self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") - - @parameterized.expand([ - ("basic_api_inline_openapi.yaml", "cfn_basic_api_inline_openapi.yaml", "basic_api_inline_openapi.json"), - ("basic_api_inline_swagger.yaml", "cfn_basic_api_inline_swagger.yaml", "basic_api_inline_swagger.json"), - # These three require a template replacement of ${definitionuri} - # ("basic_api_with_cache.yaml", "cfn_basic_api_with_cache.yaml", "basic_api_with_cache.json"), - # ("basic_api_with_tags.yaml", "cfn_basic_api_with_tags.yaml", "basic_api_with_tags.json"), - # ("basic_api.yaml", "cfn_basic_api.yaml", "basic_api.json"), - ("basic_application_sar_location.yaml", "cfn_basic_application_sar_location.yaml", "basic_application_sar_location.json"), - ("basic_application_sar_location_with_intrinsics.yaml", "cfn_basic_application_sar_location_with_intrinsics.yaml", "basic_application_sar_location_with_intrinsics.json"), - ]) - def test_basic_api(self, input_file_name, out_put_file_name, expected_name): - input_file_path = str(Path(self.template_dir, input_file_name)) - self.output_file_path = str(Path(self.output_dir, out_put_file_name)) - expected_resource_path = str(Path(self.expected_dir, expected_name)) - self.stack_name = STACK_NAME_PREFIX + input_file_name.split('.')[0].replace('_', '-') - - transform_template(input_file_path, self.output_file_path) - - # cfn part - cloudformation_client = boto3.client("cloudformation") - deployer = Deployer(cloudformation_client, changeset_prefix="foss-integ") - - # deploy to cfn - with open(self.output_file_path, 'r') as cfn_file: - result, changeset_type = deployer.create_and_wait_for_changeset( - stack_name=self.stack_name, - cfn_template=cfn_file.read(), - parameter_values=[], - capabilities=['CAPABILITY_IAM', 'CAPABILITY_AUTO_EXPAND'], - role_arn=None, - notification_arns=[], - s3_uploader=None, - tags=[], - ) - deployer.execute_changeset(result["Id"], self.stack_name) - deployer.wait_for_execute(self.stack_name, changeset_type) - - # verify - stacks_description = cloudformation_client.describe_stacks(StackName=self.stack_name) - stack_resources = cloudformation_client.list_stack_resources(StackName=self.stack_name) - # verify if the stack is create successfully or not - self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') - # verify if the stack contain the expected resources - self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) - - # clean up - # delete stack and delete translated cfn template - def tearDown(self): - self.cloudformation_client.delete_stack(StackName=self.stack_name) - if os.path.exists(self.output_file_path): - os.remove(self.output_file_path) diff --git a/tests_integ/single/test_basic_application.py b/tests_integ/single/test_basic_application.py new file mode 100644 index 000000000..d533299d8 --- /dev/null +++ b/tests_integ/single/test_basic_application.py @@ -0,0 +1,12 @@ +import os + +from parameterized import parameterized +from tests_integ.helpers.base_test import BaseTest + +class TestBasicApplication(BaseTest): + @parameterized.expand([ + ("basic_application_sar_location"), + ("basic_application_sar_location_with_intrinsics"), + ]) + def test_basic_application(self, file_name): + self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index 6b42a9f6d..894357e67 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -1,64 +1,12 @@ import os -from pathlib import Path -from unittest.case import TestCase -import boto3 -import pytest from parameterized import parameterized -from samcli.lib.deploy.deployer import Deployer -from tests_integ.helpers.helpers import transform_template, verify_stack_resources - -STACK_NAME_PREFIX = "sam-integ-test-" - - -class TestBasicFunction(TestCase): - # set up before every tests run: - def setUp(self): - tests_integ_dir = Path(__file__).resolve().parents[1] - self.template_dir = Path(tests_integ_dir, 'resources', 'templates', 'single') - self.output_dir = tests_integ_dir - self.expected_dir = Path(tests_integ_dir, 'resources', 'expected', 'single') - - self.cloudformation_client = boto3.client("cloudformation") - self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") - - @parameterized.expand([("basic_function.yaml", "basic_function.json"), - ("basic_function_event_destinations.yaml", "basic_function_event_destinations.json") - ]) - def test_basic_function(self, input_file_name, expected_name): - input_file_path = str(Path(self.template_dir, input_file_name)) - self.output_file_path = str(Path(self.output_dir, 'cfn_' + input_file_name)) - expected_resource_path = str(Path(self.expected_dir, expected_name)) - self.stack_name = STACK_NAME_PREFIX + input_file_name.split('.')[0].replace('_', '-') - - transform_template(input_file_path, self.output_file_path) - - # deploy to cfn - with open(self.output_file_path, 'r') as cfn_file: - result, changeset_type = self.deployer.create_and_wait_for_changeset( - stack_name=self.stack_name, - cfn_template=cfn_file.read(), - parameter_values=[], - capabilities=['CAPABILITY_IAM'], - role_arn=None, - notification_arns=[], - s3_uploader=None, - tags=[], - ) - self.deployer.execute_changeset(result["Id"], self.stack_name) - self.deployer.wait_for_execute(self.stack_name, changeset_type) - - # verify - stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) - stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) - # verify if the stack is create successfully or not - self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') - # verify if the stack contain the expected resources - self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) - - # clean up - # delete stack and delete translated cfn template - def tearDown(self): - self.cloudformation_client.delete_stack(StackName=self.stack_name) - if os.path.exists(self.output_file_path): - os.remove(self.output_file_path) +from tests_integ.helpers.base_test import BaseTest + +class TestBasicFunction(BaseTest): + @parameterized.expand([ + ("basic_function"), + ("basic_function_event_destinations") + ]) + def test_basic_function(self, file_name): + self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/utils/__init__.py b/tests_integ/single/utils/__init__.py deleted file mode 100755 index e69de29bb..000000000 diff --git a/tests_integ/single/utils/stack.py b/tests_integ/single/utils/stack.py deleted file mode 100644 index e6d4ca7c5..000000000 --- a/tests_integ/single/utils/stack.py +++ /dev/null @@ -1,24 +0,0 @@ -import json -import re - -def verify_stack_resources(expected_file_path, stack_resources): - with open(expected_file_path, 'r') as expected_data: - expected_resources = _sort_resources(json.load(expected_data)) - parsed_resources = _sort_resources(stack_resources['StackResourceSummaries']) - - if len(expected_resources) != len(parsed_resources): - return False - - for i in range(len(expected_resources)): - exp = expected_resources[i] - parsed = parsed_resources[i] - if parsed["ResourceStatus"] != "CREATE_COMPLETE": - return False - if re.fullmatch(exp["LogicalResourceId"], parsed["LogicalResourceId"]) is None: - return False - if exp["ResourceType"] != parsed["ResourceType"]: - return False - return True - -def _sort_resources(resources): - return sorted(resources, key=lambda d: d["LogicalResourceId"]) From a1f583e57236c3302bab07bc7df5cd38466e740f Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 13 Nov 2020 02:55:42 -0800 Subject: [PATCH 017/105] add upload zip file and resolve uri with url feature, need refactoring later --- tests_integ/helpers/base_test.py | 56 ++- tests_integ/helpers/helpers.py | 1 + tests_integ/resources/code/code.zip | Bin 0 -> 224 bytes tests_integ/resources/code/layer1.zip | Bin 0 -> 1963 bytes tests_integ/resources/code/swagger1.json | 340 ++++++++++++++++++ .../expected/single/basic_api_with_tags.json | 5 + .../expected/single/basic_layer.json | 2 +- .../single/basic_layer_with_parameters.json | 2 +- .../templates/single/basic_function.yaml | 5 +- tests_integ/single/test_basic_api.py | 12 +- tests_integ/single/test_basic_application.py | 4 +- tests_integ/single/test_basic_function.py | 19 +- 12 files changed, 419 insertions(+), 27 deletions(-) create mode 100644 tests_integ/resources/code/code.zip create mode 100644 tests_integ/resources/code/layer1.zip create mode 100644 tests_integ/resources/code/swagger1.json create mode 100644 tests_integ/resources/expected/single/basic_api_with_tags.json diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index e23b79705..81e5a591e 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -4,31 +4,56 @@ import boto3 import pytest +import yaml from samcli.lib.deploy.deployer import Deployer from tests_integ.helpers.helpers import transform_template, verify_stack_resources STACK_NAME_PREFIX = "sam-integ-test-" +S3_BUCKET = "sam-integ-bucket-test245b" # need to add suffix to allow multiple test +CODE_KEY_TO_FILE_MAP = {'codeuri': 'code.zip', 'contenturi': 'layer1.zip', 'definitionuri': "swagger1.json"} + class BaseTest(TestCase): - output_file_path = "" - stack_name = "" - def setUp(self): - tests_integ_dir = Path(__file__).resolve().parents[1] - self.template_dir = Path(tests_integ_dir, 'resources', 'templates', 'single') - self.output_dir = tests_integ_dir - self.expected_dir = Path(tests_integ_dir, 'resources', 'expected', 'single') + @classmethod + def setUpClass(cls): + BaseTest.tests_integ_dir = Path(__file__).resolve().parents[1] + BaseTest.template_dir = Path(BaseTest.tests_integ_dir, 'resources', 'templates', 'single') + BaseTest.output_dir = BaseTest.tests_integ_dir + BaseTest.expected_dir = Path(BaseTest.tests_integ_dir, 'resources', 'expected', 'single') + code_dir = Path(BaseTest.tests_integ_dir, 'resources', 'code') + BaseTest.s3_client = boto3.client("s3") + BaseTest.s3_client.create_bucket(Bucket=S3_BUCKET) + BaseTest.code_key_to_url = {} + for key, value in CODE_KEY_TO_FILE_MAP.items(): + code_path = Path(code_dir, value) + code_file = code_path.open(mode='rb') + BaseTest.s3_client.put_object(Bucket=S3_BUCKET, Body=code_file, Key=value) + code_url = f"s3://{S3_BUCKET}/{value}" + BaseTest.code_key_to_url[key] = code_url + + def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") def create_and_verify_stack(self, file_name): - input_file_path = str(Path(self.template_dir, file_name + ".yaml")) - self.output_file_path = str(Path(self.output_dir, 'cfn_' + file_name + ".yaml")) - expected_resource_path = str(Path(self.expected_dir, file_name + ".json")) + input_file_path = str(Path(BaseTest.template_dir, file_name + ".yaml")) + self.output_file_path = str(Path(BaseTest.output_dir, 'cfn_' + file_name + ".yaml")) + expected_resource_path = str(Path(BaseTest.expected_dir, file_name + ".json")) self.stack_name = STACK_NAME_PREFIX + file_name.replace('_', '-') - transform_template(input_file_path, self.output_file_path) + self.sub_input_file_path = str(Path(BaseTest.output_dir, 'sub_' + file_name + ".yaml")) + with open(input_file_path, 'r') as f: + data = f.read() + for key, value in BaseTest.code_key_to_url.items(): + data = data.replace(f"${{{key}}}", value) + yaml_doc = yaml.load(data) + + with open(self.sub_input_file_path, 'w') as f: + yaml.dump(yaml_doc, f) + + transform_template(self.sub_input_file_path, self.output_file_path) # deploy to cfn with open(self.output_file_path, 'r') as cfn_file: @@ -57,3 +82,12 @@ def tearDown(self): self.cloudformation_client.delete_stack(StackName=self.stack_name) if os.path.exists(self.output_file_path): os.remove(self.output_file_path) + if os.path.exists(self.sub_input_file_path): + os.remove(self.sub_input_file_path) + + @classmethod + def tearDownClass(cls) -> None: + response = BaseTest.s3_client.list_objects(Bucket=S3_BUCKET) + for content in response['Contents']: + BaseTest.s3_client.delete_object(Key=content['Key'], Bucket=S3_BUCKET) + BaseTest.s3_client.delete_bucket(Bucket=S3_BUCKET) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 91a08ec25..71d98a8fc 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -50,5 +50,6 @@ def verify_stack_resources(expected_file_path, stack_resources): return False return True + def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) diff --git a/tests_integ/resources/code/code.zip b/tests_integ/resources/code/code.zip new file mode 100644 index 0000000000000000000000000000000000000000..3f21de8b6cf8b2ebb0ff6893c9eca604429098b1 GIT binary patch literal 224 zcmWIWW@h1H00E{NE6!sx9 zrD`Z;q~_%0D`{GD)p7-RGcw6BZ zKwMhE&A`agrR*usa;BQO@a@cKADK{zX-l_U zI#IM-XS21k&z!k8-<^5qq{)`NC97xI)g$fKrXMq3rKYBK?8lR>p1q#RAH}{p#@Cog z`PN)AHcsbs&Di8`TK4bcoBDG##n0~475kmjVMxdi%45@DX5foauTF?AeDh;^&6&?H z8cpmQ*tl90`MeWy|LgeAv#I>_B&pEX}0z(GuUUY-_@t{E_vQ= zP5wl=1i6%budFuxt~paYv-k|>XNJ!TpF^+TT+*8Jxp7TOinWN!8Rj$1k)}5%FR%9c zc5knQPk^G^_qVgZzvoV9@C!D4Wu)>pLM8W@@+`rHm3eNDW@SbB`o=HTS2xeUp4a!> z(r&)J^`?X+?m8#EX6+41y5%=_;+j?tYktYLZH0QS$3j$P)sEJ-<$11Jr!wcvsx=p) z(~gD4nqAuK7Bv0rOJBz23k)>lUpw9YuB1rpMy(SO1x`hLWOD;>K|<{(OkCX zy}qt;($0#6YR-D~fK6T*}EZ5<5@*LCPRX}hP?g-llJdKQ-@Ze?k7 zq<{IHdnNj1oX2-Mt$D1z$W6!XcCwW!d$mj9HNBrxQlbtQx-Q?k%duO1Y3gtGjJ0ZS zBl>l2mp)q6WU6QF#~;Ev`;LgexS^obi%DNO&IG9BD8#i)e~`L^DgQue1-o6t`3G!w zTFyTdt6{f4sQ*Fw4`2O(`5(-SBAS*)v?+xpF`Fd`mpw9kb13i4QAL~9{znFFA9T(= zlJThFpe=z-HWesEe!PPt3@*hS2Fx%HI|H%D^VO>-FgXur^MmxqoleMV* zt=(Xs(rG-Kt!DG>hi~L7ejJHC^z-)KZvx_Ccf(_X0z-rEU%Gd7v*DR&w{qh&zHmJY zz5vODIS=>thRiP8_^0lmy;pv)*!E5HD%nru%{Iu3UGn7z^FOb?O)e3eZq;`zJ|H~* z^BVT-zf30==6v?sv@r0)WScI-=PLbMxR_2ZowNI5pP$rS*UqxVcaLX^yfL3xE~gvv z`{k1vwsWs1@ZM|p*POonf%Xr^Acy^;5}!3-!N3M}l3Gjj`aQsWCM zOEU8F;&by;N^?^6!3Ce}+WXPxff%je0~rS?J3+=p`n|PQ1sbOg#HzTAL$O1zpb~C8 z$O<&$y*+({HO`;#IoEOCbB&Mgr3+gg1u--&x-#j~A{$m!`3Vaa3W+lyytE-i1I0@U zxZDsQpP83g5+4t<{x-*yXs?P3=+*~#Gcw6B{85_fEdjPbmNjn8&(`fqu2)y7<^Vie1RF@FkgVI fI)KY6l;8*20t$YNuwrEc1u6>=Rs&rw1?B+&#xxB~ literal 0 HcmV?d00001 diff --git a/tests_integ/resources/code/swagger1.json b/tests_integ/resources/code/swagger1.json new file mode 100644 index 000000000..81d235a10 --- /dev/null +++ b/tests_integ/resources/code/swagger1.json @@ -0,0 +1,340 @@ +{ + "swagger": "2.0", + "info": { + "title": "PetStore", + "description": "Your first API with Amazon API Gateway. This is a sample API that integrates via HTTP with our demo Pet Store endpoints" + }, + "schemes": [ + "https" + ], + "paths": { + "/": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "text/html" + ], + "responses": { + "200": { + "description": "200 response", + "headers": { + "Content-Type": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Content-Type": "'text/html'" + }, + "responseTemplates": { + "text/html": "\n \n \n \n \n

Welcome to your Pet Store API

\n

\n You have succesfully deployed your first API. You are seeing this HTML page because the GET method to the root resource of your API returns this content as a Mock integration.\n

\n

\n The Pet Store API contains the /pets and /pets/{petId} resources. By making a GET request to /pets you can retrieve a list of Pets in your API. If you are looking for a specific pet, for example the pet with ID 1, you can make a GET request to /pets/1.\n

\n

\n You can use a REST client such as Postman to test the POST methods in your API to create a new pet. Use the sample body below to send the POST request:\n

\n
\n{\n    \"type\" : \"cat\",\n    \"price\" : 123.11\n}\n        
\n \n" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + }, + "post": { + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod": "POST", + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'POST,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + }, + "/pets": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "type", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "page", + "in": "query", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod": "GET", + "requestParameters": { + "integration.request.querystring.page": "method.request.querystring.page", + "integration.request.querystring.type": "method.request.querystring.type" + }, + "type": "http" + } + }, + "post": { + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod": "POST", + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'POST,GET,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + }, + "/pets/{petId}": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets/{petId}", + "httpMethod": "GET", + "requestParameters": { + "integration.request.path.petId": "method.request.path.petId" + }, + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + } + }, + "definitions": { + "Empty": { + "type": "object" + } + } +} diff --git a/tests_integ/resources/expected/single/basic_api_with_tags.json b/tests_integ/resources/expected/single/basic_api_with_tags.json new file mode 100644 index 000000000..d636093ca --- /dev/null +++ b/tests_integ/resources/expected/single/basic_api_with_tags.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiStage", "ResourceType":"AWS::ApiGateway::Stage" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_layer.json b/tests_integ/resources/expected/single/basic_layer.json index 123db2316..beba3153c 100644 --- a/tests_integ/resources/expected/single/basic_layer.json +++ b/tests_integ/resources/expected/single/basic_layer.json @@ -1,3 +1,3 @@ [ - { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"}, + { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"} ] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_layer_with_parameters.json b/tests_integ/resources/expected/single/basic_layer_with_parameters.json index 123db2316..beba3153c 100644 --- a/tests_integ/resources/expected/single/basic_layer_with_parameters.json +++ b/tests_integ/resources/expected/single/basic_layer_with_parameters.json @@ -1,3 +1,3 @@ [ - { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"}, + { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"} ] \ No newline at end of file diff --git a/tests_integ/resources/templates/single/basic_function.yaml b/tests_integ/resources/templates/single/basic_function.yaml index 3ec291f00..d3fdf3078 100644 --- a/tests_integ/resources/templates/single/basic_function.yaml +++ b/tests_integ/resources/templates/single/basic_function.yaml @@ -4,10 +4,7 @@ Resources: Properties: Handler: index.handler Runtime: nodejs12.x - InlineCode: | - exports.handler = function(event) { - console.log("Hello World"); - } + CodeUri: ${codeuri} MemorySize: 128 Policies: - AWSLambdaRole diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py index 0a9bc0905..23c4c0240 100644 --- a/tests_integ/single/test_basic_api.py +++ b/tests_integ/single/test_basic_api.py @@ -3,14 +3,14 @@ from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest + class TestBasicApi(BaseTest): @parameterized.expand([ - ("basic_api_inline_openapi"), - ("basic_api_inline_swagger"), - # These three require a template replacement of ${definitionuri} - # ("basic_api_with_cache"), - # ("basic_api_with_tags"), - # ("basic_api.yaml"), + "basic_api_inline_openapi", + "basic_api_inline_swagger", + # "basic_api_with_cache", # no test case in java code base + "basic_api_with_tags", + "basic_api", ]) def test_basic_api(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_application.py b/tests_integ/single/test_basic_application.py index d533299d8..0519e6f05 100644 --- a/tests_integ/single/test_basic_application.py +++ b/tests_integ/single/test_basic_application.py @@ -5,8 +5,8 @@ class TestBasicApplication(BaseTest): @parameterized.expand([ - ("basic_application_sar_location"), - ("basic_application_sar_location_with_intrinsics"), + "basic_application_sar_location", + "basic_application_sar_location_with_intrinsics", ]) def test_basic_application(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index 894357e67..49931c557 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -3,10 +3,25 @@ from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest + class TestBasicFunction(BaseTest): @parameterized.expand([ - ("basic_function"), - ("basic_function_event_destinations") + "basic_function", + "basic_function_event_destinations", + "basic_function_no_envvar", + "basic_function_openapi", + "basic_function_with_kmskeyarn", + "basic_function_with_sns_dlq", + "basic_function_with_sqs_dlq", + "basic_function_with_tags", + # ("basic_function_with_tracing"), # need different set up to create changeset + "basic_http_api", + "basic_layer", + "basic_layer_with_parameters", + "basic_state_machine_inline_definition", + # ("basic_state_machine_with_tags"), # cannot be translated by sam-tran + # ("basic_table_no_param"), # no test case in java code base + # ("basic_table_with_param") # no test case in java code base ]) def test_basic_function(self, file_name): self.create_and_verify_stack(file_name) From d12abf4afe67cf314e4070225190d032d3a38b9b Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 13 Nov 2020 15:46:18 -0800 Subject: [PATCH 018/105] add extra test class and update yaml load call --- tests_integ/helpers/base_test.py | 2 +- tests_integ/single/test_basic_function.py | 5 ----- tests_integ/single/test_basic_http_api.py | 11 +++++++++++ tests_integ/single/test_basic_layer_version.py | 12 ++++++++++++ tests_integ/single/test_basic_state_machine.py | 12 ++++++++++++ 5 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 tests_integ/single/test_basic_http_api.py create mode 100644 tests_integ/single/test_basic_layer_version.py create mode 100644 tests_integ/single/test_basic_state_machine.py diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 81e5a591e..80006c2c0 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -48,7 +48,7 @@ def create_and_verify_stack(self, file_name): data = f.read() for key, value in BaseTest.code_key_to_url.items(): data = data.replace(f"${{{key}}}", value) - yaml_doc = yaml.load(data) + yaml_doc = yaml.load(data, Loader=yaml.FullLoader) with open(self.sub_input_file_path, 'w') as f: yaml.dump(yaml_doc, f) diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index 49931c557..f88446aa7 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -15,11 +15,6 @@ class TestBasicFunction(BaseTest): "basic_function_with_sqs_dlq", "basic_function_with_tags", # ("basic_function_with_tracing"), # need different set up to create changeset - "basic_http_api", - "basic_layer", - "basic_layer_with_parameters", - "basic_state_machine_inline_definition", - # ("basic_state_machine_with_tags"), # cannot be translated by sam-tran # ("basic_table_no_param"), # no test case in java code base # ("basic_table_with_param") # no test case in java code base ]) diff --git a/tests_integ/single/test_basic_http_api.py b/tests_integ/single/test_basic_http_api.py new file mode 100644 index 000000000..0da0c8a12 --- /dev/null +++ b/tests_integ/single/test_basic_http_api.py @@ -0,0 +1,11 @@ +from parameterized import parameterized + +from tests_integ.helpers.base_test import BaseTest + + +class TestBasicHttpApi(BaseTest): + @parameterized.expand([ + "basic_http_api", + ]) + def test_basic_http_api(self, file_name): + self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_layer_version.py b/tests_integ/single/test_basic_layer_version.py new file mode 100644 index 000000000..b6fe20ce6 --- /dev/null +++ b/tests_integ/single/test_basic_layer_version.py @@ -0,0 +1,12 @@ +from parameterized import parameterized + +from tests_integ.helpers.base_test import BaseTest + + +class TestBasicLayerVersion(BaseTest): + @parameterized.expand([ + "basic_layer", + "basic_layer_with_parameters", + ]) + def test_basic_layer_version(self, file_name): + self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_state_machine.py b/tests_integ/single/test_basic_state_machine.py new file mode 100644 index 000000000..ffce10cae --- /dev/null +++ b/tests_integ/single/test_basic_state_machine.py @@ -0,0 +1,12 @@ +from parameterized import parameterized + +from tests_integ.helpers.base_test import BaseTest + + +class TestBasicLayerVersion(BaseTest): + @parameterized.expand([ + "basic_state_machine_inline_definition", + # ("basic_state_machine_with_tags"), # cannot be translated by sam-tran + ]) + def test_basic_state_machine(self, file_name): + self.create_and_verify_stack(file_name) From d8eb9ba44e92a58d669075cd9c238c5bc595ffde Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 13 Nov 2020 16:58:06 -0800 Subject: [PATCH 019/105] fix bug, now allow create bucket in multi regions --- tests_integ/helpers/base_test.py | 31 ++++++++++++++++++------------- tests_integ/helpers/helpers.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 80006c2c0..02465724c 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -6,10 +6,10 @@ import pytest import yaml from samcli.lib.deploy.deployer import Deployer -from tests_integ.helpers.helpers import transform_template, verify_stack_resources +from tests_integ.helpers.helpers import transform_template, verify_stack_resources, create_bucket STACK_NAME_PREFIX = "sam-integ-test-" -S3_BUCKET = "sam-integ-bucket-test245b" # need to add suffix to allow multiple test +S3_BUCKET = "sam-integ-bucket-test245b3x" # need to add random suffix to allow run multiple test set in the sam account CODE_KEY_TO_FILE_MAP = {'codeuri': 'code.zip', 'contenturi': 'layer1.zip', 'definitionuri': "swagger1.json"} @@ -23,14 +23,18 @@ def setUpClass(cls): BaseTest.expected_dir = Path(BaseTest.tests_integ_dir, 'resources', 'expected', 'single') code_dir = Path(BaseTest.tests_integ_dir, 'resources', 'code') - BaseTest.s3_client = boto3.client("s3") - BaseTest.s3_client.create_bucket(Bucket=S3_BUCKET) + session = boto3.session.Session() + my_region = session.region_name + create_bucket(S3_BUCKET, region=my_region) + + s3_client = boto3.client("s3") BaseTest.code_key_to_url = {} - for key, value in CODE_KEY_TO_FILE_MAP.items(): - code_path = Path(code_dir, value) + + for key, file_name in CODE_KEY_TO_FILE_MAP.items(): + code_path = Path(code_dir, file_name) code_file = code_path.open(mode='rb') - BaseTest.s3_client.put_object(Bucket=S3_BUCKET, Body=code_file, Key=value) - code_url = f"s3://{S3_BUCKET}/{value}" + s3_client.put_object(Bucket=S3_BUCKET, Body=code_file, Key=file_name) + code_url = f"s3://{S3_BUCKET}/{file_name}" BaseTest.code_key_to_url[key] = code_url def setUp(self): @@ -46,8 +50,8 @@ def create_and_verify_stack(self, file_name): self.sub_input_file_path = str(Path(BaseTest.output_dir, 'sub_' + file_name + ".yaml")) with open(input_file_path, 'r') as f: data = f.read() - for key, value in BaseTest.code_key_to_url.items(): - data = data.replace(f"${{{key}}}", value) + for key, s3_url in BaseTest.code_key_to_url.items(): + data = data.replace(f"${{{key}}}", s3_url) yaml_doc = yaml.load(data, Loader=yaml.FullLoader) with open(self.sub_input_file_path, 'w') as f: @@ -87,7 +91,8 @@ def tearDown(self): @classmethod def tearDownClass(cls) -> None: - response = BaseTest.s3_client.list_objects(Bucket=S3_BUCKET) + s3_client = boto3.client("s3") + response = s3_client.list_objects(Bucket=S3_BUCKET) for content in response['Contents']: - BaseTest.s3_client.delete_object(Key=content['Key'], Bucket=S3_BUCKET) - BaseTest.s3_client.delete_bucket(Bucket=S3_BUCKET) + s3_client.delete_object(Key=content['Key'], Bucket=S3_BUCKET) + s3_client.delete_bucket(Bucket=S3_BUCKET) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 71d98a8fc..53e9e4a04 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -4,6 +4,7 @@ from functools import reduce import boto3 +from botocore.exceptions import ClientError from samtranslator.model.exceptions import InvalidDocumentException from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader @@ -53,3 +54,32 @@ def verify_stack_resources(expected_file_path, stack_resources): def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) + + +def create_bucket(bucket_name, region=None): + """Create an S3 bucket in a specified region + + copy code from boto3 doc example + + If a region is not specified, the bucket is created in the S3 default + region (us-east-1). + + :param bucket_name: Bucket to create + :param region: String region to create bucket in, e.g., 'us-west-2' + :return: True if bucket created, else False + """ + + # Create bucket + try: + if region is None: + s3_client = boto3.client('s3') + s3_client.create_bucket(Bucket=bucket_name) + else: + s3_client = boto3.client('s3', region_name=region) + location = {'LocationConstraint': region} + s3_client.create_bucket(Bucket=bucket_name, + CreateBucketConfiguration=location) + except ClientError as e: + logging.error(e) + return False + return True From 083942c754bffe48cd01b97416cb42ddc44af2f1 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Fri, 13 Nov 2020 17:03:55 -0800 Subject: [PATCH 020/105] Added random suffix to stack and bucket names to avoid name collisions --- tests_integ/helpers/base_test.py | 24 ++++++++++++------------ tests_integ/helpers/helpers.py | 27 ++++++++++++++------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 02465724c..66b9a18f1 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -6,10 +6,10 @@ import pytest import yaml from samcli.lib.deploy.deployer import Deployer -from tests_integ.helpers.helpers import transform_template, verify_stack_resources, create_bucket +from tests_integ.helpers.helpers import transform_template, verify_stack_resources, generate_suffix, create_bucket -STACK_NAME_PREFIX = "sam-integ-test-" -S3_BUCKET = "sam-integ-bucket-test245b3x" # need to add random suffix to allow run multiple test set in the sam account +STACK_NAME_PREFIX = "sam-integ-stack-" +S3_BUCKET_PREFIX = "sam-integ-bucket-" CODE_KEY_TO_FILE_MAP = {'codeuri': 'code.zip', 'contenturi': 'layer1.zip', 'definitionuri': "swagger1.json"} @@ -23,18 +23,18 @@ def setUpClass(cls): BaseTest.expected_dir = Path(BaseTest.tests_integ_dir, 'resources', 'expected', 'single') code_dir = Path(BaseTest.tests_integ_dir, 'resources', 'code') + BaseTest.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() session = boto3.session.Session() my_region = session.region_name - create_bucket(S3_BUCKET, region=my_region) + create_bucket(BaseTest.s3_bucket_name, region=my_region) s3_client = boto3.client("s3") BaseTest.code_key_to_url = {} for key, file_name in CODE_KEY_TO_FILE_MAP.items(): - code_path = Path(code_dir, file_name) - code_file = code_path.open(mode='rb') - s3_client.put_object(Bucket=S3_BUCKET, Body=code_file, Key=file_name) - code_url = f"s3://{S3_BUCKET}/{file_name}" + code_path = str(Path(code_dir, file_name)) + s3_client.upload_file(code_path, BaseTest.s3_bucket_name, file_name) + code_url = f"s3://{BaseTest.s3_bucket_name}/{file_name}" BaseTest.code_key_to_url[key] = code_url def setUp(self): @@ -45,7 +45,7 @@ def create_and_verify_stack(self, file_name): input_file_path = str(Path(BaseTest.template_dir, file_name + ".yaml")) self.output_file_path = str(Path(BaseTest.output_dir, 'cfn_' + file_name + ".yaml")) expected_resource_path = str(Path(BaseTest.expected_dir, file_name + ".json")) - self.stack_name = STACK_NAME_PREFIX + file_name.replace('_', '-') + self.stack_name = STACK_NAME_PREFIX + file_name.replace('_', '-') + generate_suffix() self.sub_input_file_path = str(Path(BaseTest.output_dir, 'sub_' + file_name + ".yaml")) with open(input_file_path, 'r') as f: @@ -92,7 +92,7 @@ def tearDown(self): @classmethod def tearDownClass(cls) -> None: s3_client = boto3.client("s3") - response = s3_client.list_objects(Bucket=S3_BUCKET) + response = s3_client.list_objects(Bucket=BaseTest.s3_bucket_name) for content in response['Contents']: - s3_client.delete_object(Key=content['Key'], Bucket=S3_BUCKET) - s3_client.delete_bucket(Bucket=S3_BUCKET) + s3_client.delete_object(Key=content['Key'], Bucket=BaseTest.s3_bucket_name) + s3_client.delete_bucket(Bucket=BaseTest.s3_bucket_name) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 53e9e4a04..5598be497 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -1,6 +1,8 @@ import json import logging import re +import random +import string # not deprecated, a bug from pylint https://www.logilab.org/ticket/2481 from functools import reduce import boto3 @@ -11,6 +13,7 @@ from samtranslator.translator.transform import transform from samtranslator.yaml_helper import yaml_parse +RANDOM_SUFFIX_LENGTH = 12 def transform_template(input_file_path, output_file_path): LOG = logging.getLogger(__name__) @@ -51,6 +54,9 @@ def verify_stack_resources(expected_file_path, stack_resources): return False return True +def generate_suffix(): + # Very basic random letters generator + return ''.join(random.choice(string.ascii_lowercase) for i in range(RANDOM_SUFFIX_LENGTH)) def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) @@ -60,6 +66,7 @@ def create_bucket(bucket_name, region=None): """Create an S3 bucket in a specified region copy code from boto3 doc example + MG: removed the try so that the exception bubbles up and interrupts the test If a region is not specified, the bucket is created in the S3 default region (us-east-1). @@ -70,16 +77,10 @@ def create_bucket(bucket_name, region=None): """ # Create bucket - try: - if region is None: - s3_client = boto3.client('s3') - s3_client.create_bucket(Bucket=bucket_name) - else: - s3_client = boto3.client('s3', region_name=region) - location = {'LocationConstraint': region} - s3_client.create_bucket(Bucket=bucket_name, - CreateBucketConfiguration=location) - except ClientError as e: - logging.error(e) - return False - return True + if region is None: + s3_client = boto3.client('s3') + s3_client.create_bucket(Bucket=bucket_name) + else: + s3_client = boto3.client('s3', region_name=region) + location = {'LocationConstraint': region} + s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location) From e68b81e2418f1fbddaf013abe57db286eed7167e Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Sun, 15 Nov 2020 23:53:08 -0800 Subject: [PATCH 021/105] refactor: Moved some stuff into methods in base_test, updated the Makefile black commands to include the tests_integ directories --- Makefile | 4 +- tests_integ/helpers/base_test.py | 74 ++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index be9fd2cb6..d74ba3dfd 100755 --- a/Makefile +++ b/Makefile @@ -12,10 +12,10 @@ test-integ: pytest --no-cov tests_integ/* black: - black setup.py samtranslator/* tests/* bin/* + black setup.py samtranslator/* tests/* tests_integ/* bin/* black-check: - black --check setup.py samtranslator/* tests/* bin/* + black --check setup.py samtranslator/* tests/* tests_integ/* bin/* # Command to run everytime you make changes to verify everything works dev: test diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 66b9a18f1..f44d7e40b 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -18,10 +18,11 @@ class BaseTest(TestCase): @classmethod def setUpClass(cls): BaseTest.tests_integ_dir = Path(__file__).resolve().parents[1] - BaseTest.template_dir = Path(BaseTest.tests_integ_dir, 'resources', 'templates', 'single') + BaseTest.resources_dir = Path(BaseTest.tests_integ_dir, 'resources') + BaseTest.template_dir = Path(BaseTest.resources_dir, 'templates', 'single') BaseTest.output_dir = BaseTest.tests_integ_dir - BaseTest.expected_dir = Path(BaseTest.tests_integ_dir, 'resources', 'expected', 'single') - code_dir = Path(BaseTest.tests_integ_dir, 'resources', 'code') + BaseTest.expected_dir = Path(BaseTest.resources_dir, 'expected', 'single') + code_dir = Path(BaseTest.resources_dir, 'code') BaseTest.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() session = boto3.session.Session() @@ -37,29 +38,60 @@ def setUpClass(cls): code_url = f"s3://{BaseTest.s3_bucket_name}/{file_name}" BaseTest.code_key_to_url[key] = code_url + @classmethod + def tearDownClass(cls) -> None: + BaseTest._clean_bucket() + + + @classmethod + def _clean_bucket(cls): + s3_client = boto3.client("s3") + response = s3_client.list_objects(Bucket=BaseTest.s3_bucket_name) + for content in response['Contents']: + s3_client.delete_object(Key=content['Key'], Bucket=BaseTest.s3_bucket_name) + s3_client.delete_bucket(Bucket=BaseTest.s3_bucket_name) + + def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") + def create_and_verify_stack(self, file_name): input_file_path = str(Path(BaseTest.template_dir, file_name + ".yaml")) self.output_file_path = str(Path(BaseTest.output_dir, 'cfn_' + file_name + ".yaml")) expected_resource_path = str(Path(BaseTest.expected_dir, file_name + ".json")) self.stack_name = STACK_NAME_PREFIX + file_name.replace('_', '-') + generate_suffix() - self.sub_input_file_path = str(Path(BaseTest.output_dir, 'sub_' + file_name + ".yaml")) + self.sub_input_file_path = self._update_template(input_file_path, file_name) + transform_template(self.sub_input_file_path, self.output_file_path) + self._deploy_stack() + self._verify_stack(expected_resource_path) + + + def tearDown(self): + self.cloudformation_client.delete_stack(StackName=self.stack_name) + if os.path.exists(self.output_file_path): + os.remove(self.output_file_path) + if os.path.exists(self.sub_input_file_path): + os.remove(self.sub_input_file_path) + + + def _update_template(self, input_file_path, file_name): + updated_template_path = str(Path(BaseTest.output_dir, 'sub_' + file_name + ".yaml")) with open(input_file_path, 'r') as f: data = f.read() for key, s3_url in BaseTest.code_key_to_url.items(): data = data.replace(f"${{{key}}}", s3_url) yaml_doc = yaml.load(data, Loader=yaml.FullLoader) - with open(self.sub_input_file_path, 'w') as f: + with open(updated_template_path, 'w') as f: yaml.dump(yaml_doc, f) + + return updated_template_path - transform_template(self.sub_input_file_path, self.output_file_path) - # deploy to cfn + def _deploy_stack(self): with open(self.output_file_path, 'r') as cfn_file: result, changeset_type = self.deployer.create_and_wait_for_changeset( stack_name=self.stack_name, @@ -74,25 +106,11 @@ def create_and_verify_stack(self, file_name): self.deployer.execute_changeset(result["Id"], self.stack_name) self.deployer.wait_for_execute(self.stack_name, changeset_type) - # verify - stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) - stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) - # verify if the stack is create successfully or not - self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') - # verify if the stack contain the expected resources - self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) - def tearDown(self): - self.cloudformation_client.delete_stack(StackName=self.stack_name) - if os.path.exists(self.output_file_path): - os.remove(self.output_file_path) - if os.path.exists(self.sub_input_file_path): - os.remove(self.sub_input_file_path) - - @classmethod - def tearDownClass(cls) -> None: - s3_client = boto3.client("s3") - response = s3_client.list_objects(Bucket=BaseTest.s3_bucket_name) - for content in response['Contents']: - s3_client.delete_object(Key=content['Key'], Bucket=BaseTest.s3_bucket_name) - s3_client.delete_bucket(Bucket=BaseTest.s3_bucket_name) + def _verify_stack(self, expected_resource_path): + stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) + stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) + # verify if the stack was successfully created + self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') + # verify if the stack contains the expected resources + self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) From 4ae155dd76f9c3c2fa4dc397e34131312d321e1d Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Mon, 16 Nov 2020 09:39:18 -0800 Subject: [PATCH 022/105] chore: Black formatting --- tests_integ/helpers/base_test.py | 40 ++++++++----------- tests_integ/helpers/helpers.py | 17 ++++---- tests_integ/single/test_basic_api.py | 16 ++++---- tests_integ/single/test_basic_application.py | 11 +++-- tests_integ/single/test_basic_function.py | 28 +++++++------ tests_integ/single/test_basic_http_api.py | 8 ++-- .../single/test_basic_layer_version.py | 10 +++-- .../single/test_basic_state_machine.py | 10 +++-- 8 files changed, 74 insertions(+), 66 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index f44d7e40b..dd2ed87a0 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -10,19 +10,18 @@ STACK_NAME_PREFIX = "sam-integ-stack-" S3_BUCKET_PREFIX = "sam-integ-bucket-" -CODE_KEY_TO_FILE_MAP = {'codeuri': 'code.zip', 'contenturi': 'layer1.zip', 'definitionuri': "swagger1.json"} +CODE_KEY_TO_FILE_MAP = {"codeuri": "code.zip", "contenturi": "layer1.zip", "definitionuri": "swagger1.json"} class BaseTest(TestCase): - @classmethod def setUpClass(cls): BaseTest.tests_integ_dir = Path(__file__).resolve().parents[1] - BaseTest.resources_dir = Path(BaseTest.tests_integ_dir, 'resources') - BaseTest.template_dir = Path(BaseTest.resources_dir, 'templates', 'single') + BaseTest.resources_dir = Path(BaseTest.tests_integ_dir, "resources") + BaseTest.template_dir = Path(BaseTest.resources_dir, "templates", "single") BaseTest.output_dir = BaseTest.tests_integ_dir - BaseTest.expected_dir = Path(BaseTest.resources_dir, 'expected', 'single') - code_dir = Path(BaseTest.resources_dir, 'code') + BaseTest.expected_dir = Path(BaseTest.resources_dir, "expected", "single") + code_dir = Path(BaseTest.resources_dir, "code") BaseTest.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() session = boto3.session.Session() @@ -42,33 +41,29 @@ def setUpClass(cls): def tearDownClass(cls) -> None: BaseTest._clean_bucket() - @classmethod def _clean_bucket(cls): s3_client = boto3.client("s3") response = s3_client.list_objects(Bucket=BaseTest.s3_bucket_name) - for content in response['Contents']: - s3_client.delete_object(Key=content['Key'], Bucket=BaseTest.s3_bucket_name) + for content in response["Contents"]: + s3_client.delete_object(Key=content["Key"], Bucket=BaseTest.s3_bucket_name) s3_client.delete_bucket(Bucket=BaseTest.s3_bucket_name) - def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") - def create_and_verify_stack(self, file_name): input_file_path = str(Path(BaseTest.template_dir, file_name + ".yaml")) - self.output_file_path = str(Path(BaseTest.output_dir, 'cfn_' + file_name + ".yaml")) + self.output_file_path = str(Path(BaseTest.output_dir, "cfn_" + file_name + ".yaml")) expected_resource_path = str(Path(BaseTest.expected_dir, file_name + ".json")) - self.stack_name = STACK_NAME_PREFIX + file_name.replace('_', '-') + generate_suffix() + self.stack_name = STACK_NAME_PREFIX + file_name.replace("_", "-") + generate_suffix() self.sub_input_file_path = self._update_template(input_file_path, file_name) transform_template(self.sub_input_file_path, self.output_file_path) self._deploy_stack() self._verify_stack(expected_resource_path) - def tearDown(self): self.cloudformation_client.delete_stack(StackName=self.stack_name) if os.path.exists(self.output_file_path): @@ -76,28 +71,26 @@ def tearDown(self): if os.path.exists(self.sub_input_file_path): os.remove(self.sub_input_file_path) - def _update_template(self, input_file_path, file_name): - updated_template_path = str(Path(BaseTest.output_dir, 'sub_' + file_name + ".yaml")) - with open(input_file_path, 'r') as f: + updated_template_path = str(Path(BaseTest.output_dir, "sub_" + file_name + ".yaml")) + with open(input_file_path, "r") as f: data = f.read() for key, s3_url in BaseTest.code_key_to_url.items(): data = data.replace(f"${{{key}}}", s3_url) yaml_doc = yaml.load(data, Loader=yaml.FullLoader) - with open(updated_template_path, 'w') as f: + with open(updated_template_path, "w") as f: yaml.dump(yaml_doc, f) - - return updated_template_path + return updated_template_path def _deploy_stack(self): - with open(self.output_file_path, 'r') as cfn_file: + with open(self.output_file_path, "r") as cfn_file: result, changeset_type = self.deployer.create_and_wait_for_changeset( stack_name=self.stack_name, cfn_template=cfn_file.read(), parameter_values=[], - capabilities=['CAPABILITY_IAM', 'CAPABILITY_AUTO_EXPAND'], + capabilities=["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"], role_arn=None, notification_arns=[], s3_uploader=None, @@ -106,11 +99,10 @@ def _deploy_stack(self): self.deployer.execute_changeset(result["Id"], self.stack_name) self.deployer.wait_for_execute(self.stack_name, changeset_type) - def _verify_stack(self, expected_resource_path): stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) # verify if the stack was successfully created - self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') + self.assertEqual(stacks_description["Stacks"][0]["StackStatus"], "CREATE_COMPLETE") # verify if the stack contains the expected resources self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 5598be497..09fdc2e51 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -2,7 +2,7 @@ import logging import re import random -import string # not deprecated, a bug from pylint https://www.logilab.org/ticket/2481 +import string # not deprecated, a bug from pylint https://www.logilab.org/ticket/2481 from functools import reduce import boto3 @@ -15,6 +15,7 @@ RANDOM_SUFFIX_LENGTH = 12 + def transform_template(input_file_path, output_file_path): LOG = logging.getLogger(__name__) iam_client = boto3.client("iam") @@ -38,9 +39,9 @@ def transform_template(input_file_path, output_file_path): def verify_stack_resources(expected_file_path, stack_resources): - with open(expected_file_path, 'r') as expected_data: + with open(expected_file_path, "r") as expected_data: expected_resources = _sort_resources(json.load(expected_data)) - parsed_resources = _sort_resources(stack_resources['StackResourceSummaries']) + parsed_resources = _sort_resources(stack_resources["StackResourceSummaries"]) if len(expected_resources) != len(parsed_resources): return False @@ -54,9 +55,11 @@ def verify_stack_resources(expected_file_path, stack_resources): return False return True + def generate_suffix(): # Very basic random letters generator - return ''.join(random.choice(string.ascii_lowercase) for i in range(RANDOM_SUFFIX_LENGTH)) + return "".join(random.choice(string.ascii_lowercase) for i in range(RANDOM_SUFFIX_LENGTH)) + def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) @@ -78,9 +81,9 @@ def create_bucket(bucket_name, region=None): # Create bucket if region is None: - s3_client = boto3.client('s3') + s3_client = boto3.client("s3") s3_client.create_bucket(Bucket=bucket_name) else: - s3_client = boto3.client('s3', region_name=region) - location = {'LocationConstraint': region} + s3_client = boto3.client("s3", region_name=region) + location = {"LocationConstraint": region} s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location) diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py index 23c4c0240..6ae359ace 100644 --- a/tests_integ/single/test_basic_api.py +++ b/tests_integ/single/test_basic_api.py @@ -5,12 +5,14 @@ class TestBasicApi(BaseTest): - @parameterized.expand([ - "basic_api_inline_openapi", - "basic_api_inline_swagger", - # "basic_api_with_cache", # no test case in java code base - "basic_api_with_tags", - "basic_api", - ]) + @parameterized.expand( + [ + "basic_api_inline_openapi", + "basic_api_inline_swagger", + # "basic_api_with_cache", # no test case in java code base + "basic_api_with_tags", + "basic_api", + ] + ) def test_basic_api(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_application.py b/tests_integ/single/test_basic_application.py index 0519e6f05..38ae79800 100644 --- a/tests_integ/single/test_basic_application.py +++ b/tests_integ/single/test_basic_application.py @@ -3,10 +3,13 @@ from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest + class TestBasicApplication(BaseTest): - @parameterized.expand([ - "basic_application_sar_location", - "basic_application_sar_location_with_intrinsics", - ]) + @parameterized.expand( + [ + "basic_application_sar_location", + "basic_application_sar_location_with_intrinsics", + ] + ) def test_basic_application(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index f88446aa7..9ac1da15b 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -5,18 +5,20 @@ class TestBasicFunction(BaseTest): - @parameterized.expand([ - "basic_function", - "basic_function_event_destinations", - "basic_function_no_envvar", - "basic_function_openapi", - "basic_function_with_kmskeyarn", - "basic_function_with_sns_dlq", - "basic_function_with_sqs_dlq", - "basic_function_with_tags", - # ("basic_function_with_tracing"), # need different set up to create changeset - # ("basic_table_no_param"), # no test case in java code base - # ("basic_table_with_param") # no test case in java code base - ]) + @parameterized.expand( + [ + "basic_function", + "basic_function_event_destinations", + "basic_function_no_envvar", + "basic_function_openapi", + "basic_function_with_kmskeyarn", + "basic_function_with_sns_dlq", + "basic_function_with_sqs_dlq", + "basic_function_with_tags", + # ("basic_function_with_tracing"), # need different set up to create changeset + # ("basic_table_no_param"), # no test case in java code base + # ("basic_table_with_param") # no test case in java code base + ] + ) def test_basic_function(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_http_api.py b/tests_integ/single/test_basic_http_api.py index 0da0c8a12..f1530ec2b 100644 --- a/tests_integ/single/test_basic_http_api.py +++ b/tests_integ/single/test_basic_http_api.py @@ -4,8 +4,10 @@ class TestBasicHttpApi(BaseTest): - @parameterized.expand([ - "basic_http_api", - ]) + @parameterized.expand( + [ + "basic_http_api", + ] + ) def test_basic_http_api(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_layer_version.py b/tests_integ/single/test_basic_layer_version.py index b6fe20ce6..a90dc8e2a 100644 --- a/tests_integ/single/test_basic_layer_version.py +++ b/tests_integ/single/test_basic_layer_version.py @@ -4,9 +4,11 @@ class TestBasicLayerVersion(BaseTest): - @parameterized.expand([ - "basic_layer", - "basic_layer_with_parameters", - ]) + @parameterized.expand( + [ + "basic_layer", + "basic_layer_with_parameters", + ] + ) def test_basic_layer_version(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_state_machine.py b/tests_integ/single/test_basic_state_machine.py index ffce10cae..c11890a83 100644 --- a/tests_integ/single/test_basic_state_machine.py +++ b/tests_integ/single/test_basic_state_machine.py @@ -4,9 +4,11 @@ class TestBasicLayerVersion(BaseTest): - @parameterized.expand([ - "basic_state_machine_inline_definition", - # ("basic_state_machine_with_tags"), # cannot be translated by sam-tran - ]) + @parameterized.expand( + [ + "basic_state_machine_inline_definition", + # ("basic_state_machine_with_tags"), # cannot be translated by sam-tran + ] + ) def test_basic_state_machine(self, file_name): self.create_and_verify_stack(file_name) From 4130f3d04fc9e5e035349a5e36ac0180051e80d8 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Mon, 9 Nov 2020 11:07:32 -0800 Subject: [PATCH 023/105] set up integ test folder structure --- .../single/expected_basic_function.json | 4 ++++ .../templates/single/basic_function.yaml | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 test_integ/resources/expected/single/expected_basic_function.json create mode 100644 test_integ/resources/templates/single/basic_function.yaml diff --git a/test_integ/resources/expected/single/expected_basic_function.json b/test_integ/resources/expected/single/expected_basic_function.json new file mode 100644 index 000000000..1c0f5f116 --- /dev/null +++ b/test_integ/resources/expected/single/expected_basic_function.json @@ -0,0 +1,4 @@ +{ + "MyLambdaFunction": "AWS::Lambda::Function", + "MyLambdaFunctionRole": "AWS::IAM::Role" +} \ No newline at end of file diff --git a/test_integ/resources/templates/single/basic_function.yaml b/test_integ/resources/templates/single/basic_function.yaml new file mode 100644 index 000000000..29ce6c6c3 --- /dev/null +++ b/test_integ/resources/templates/single/basic_function.yaml @@ -0,0 +1,19 @@ +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + InlineCode: | + exports.handler = function(event) { + console.log("Hello World"); + } + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Environment: + Variables: + Name: Value + Name2: Value2 + Name2: Value2 \ No newline at end of file From 923934b37c3b46c9fe65bfdf62306794d4bb9271 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Mon, 9 Nov 2020 15:51:12 -0800 Subject: [PATCH 024/105] rename test_integ to tests_integ to keep it consistent with the unitest folder tests --- .../resources/expected/single/expected_basic_function.json | 0 .../resources/templates/single/basic_function.yaml | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {test_integ => tests_integ}/resources/expected/single/expected_basic_function.json (100%) rename {test_integ => tests_integ}/resources/templates/single/basic_function.yaml (100%) diff --git a/test_integ/resources/expected/single/expected_basic_function.json b/tests_integ/resources/expected/single/expected_basic_function.json similarity index 100% rename from test_integ/resources/expected/single/expected_basic_function.json rename to tests_integ/resources/expected/single/expected_basic_function.json diff --git a/test_integ/resources/templates/single/basic_function.yaml b/tests_integ/resources/templates/single/basic_function.yaml similarity index 100% rename from test_integ/resources/templates/single/basic_function.yaml rename to tests_integ/resources/templates/single/basic_function.yaml From 67c2674f1dfaa8e7f47fa3e08fcd96d9c8cfc478 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Mon, 9 Nov 2020 16:19:57 -0800 Subject: [PATCH 025/105] Added test-integ action to Makefile to launch integration tests --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index c2b980b8f..6f78c4d83 100755 --- a/Makefile +++ b/Makefile @@ -8,6 +8,9 @@ init: test: pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests +test-integ: + pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests_integ + black: black setup.py samtranslator/* tests/* bin/*.py @@ -30,6 +33,7 @@ Usage: $ make [TARGETS] TARGETS init Initialize and install the requirements and dev-requirements for this project. test Run the Unit tests. + test-integ Run the Integration tests. dev Run all development tests after a change. pr Perform all checks before submitting a Pull Request. From ee5402e60f85964cd3478754ef9412f1c24176a2 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 10 Nov 2020 08:40:44 -0800 Subject: [PATCH 026/105] Added all the single templates, fixed an error in basic_function.yaml --- .../resources/templates/single/basic_api.yaml | 6 ++ .../single/basic_api_inline_openapi.yaml | 27 ++++++ .../single/basic_api_inline_swagger.yaml | 27 ++++++ .../single/basic_api_with_cache.yaml | 12 +++ .../templates/single/basic_api_with_tags.yaml | 9 ++ .../single/basic_application_s3_location.yaml | 5 + .../basic_application_sar_location.yaml | 9 ++ ...lication_sar_location_with_intrinsics.yaml | 57 +++++++++++ .../templates/single/basic_function.yaml | 1 - .../basic_function_event_destinations.yaml | 95 +++++++++++++++++++ .../single/basic_function_no_envvar.yaml | 11 +++ .../single/basic_function_openapi.yaml | 18 ++++ .../single/basic_function_with_kmskeyarn.yaml | 32 +++++++ .../single/basic_function_with_sns_dlq.yaml | 14 +++ .../single/basic_function_with_sqs_dlq.yaml | 15 +++ .../single/basic_function_with_tags.yaml | 14 +++ .../single/basic_function_with_tracing.yaml | 36 +++++++ .../templates/single/basic_http_api.yaml | 11 +++ .../templates/single/basic_layer.yaml | 6 ++ .../single/basic_layer_with_parameters.yaml | 46 +++++++++ ...basic_state_machine_inline_definition.yaml | 23 +++++ .../single/basic_state_machine_with_tags.yaml | 33 +++++++ .../single/basic_table_no_param.yaml | 4 + .../single/basic_table_with_param.yaml | 12 +++ 24 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 tests_integ/resources/templates/single/basic_api.yaml create mode 100644 tests_integ/resources/templates/single/basic_api_inline_openapi.yaml create mode 100644 tests_integ/resources/templates/single/basic_api_inline_swagger.yaml create mode 100644 tests_integ/resources/templates/single/basic_api_with_cache.yaml create mode 100644 tests_integ/resources/templates/single/basic_api_with_tags.yaml create mode 100644 tests_integ/resources/templates/single/basic_application_s3_location.yaml create mode 100644 tests_integ/resources/templates/single/basic_application_sar_location.yaml create mode 100644 tests_integ/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_event_destinations.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_no_envvar.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_openapi.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_with_kmskeyarn.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_with_sns_dlq.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_with_sqs_dlq.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_with_tags.yaml create mode 100644 tests_integ/resources/templates/single/basic_function_with_tracing.yaml create mode 100644 tests_integ/resources/templates/single/basic_http_api.yaml create mode 100644 tests_integ/resources/templates/single/basic_layer.yaml create mode 100644 tests_integ/resources/templates/single/basic_layer_with_parameters.yaml create mode 100644 tests_integ/resources/templates/single/basic_state_machine_inline_definition.yaml create mode 100644 tests_integ/resources/templates/single/basic_state_machine_with_tags.yaml create mode 100644 tests_integ/resources/templates/single/basic_table_no_param.yaml create mode 100644 tests_integ/resources/templates/single/basic_table_with_param.yaml diff --git a/tests_integ/resources/templates/single/basic_api.yaml b/tests_integ/resources/templates/single/basic_api.yaml new file mode 100644 index 000000000..0b6322cec --- /dev/null +++ b/tests_integ/resources/templates/single/basic_api.yaml @@ -0,0 +1,6 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: MyNewStageName + DefinitionUri: ${definitionuri} diff --git a/tests_integ/resources/templates/single/basic_api_inline_openapi.yaml b/tests_integ/resources/templates/single/basic_api_inline_openapi.yaml new file mode 100644 index 000000000..7f14ad5dc --- /dev/null +++ b/tests_integ/resources/templates/single/basic_api_inline_openapi.yaml @@ -0,0 +1,27 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: MyNewStageName + DefinitionBody: + # Simple HTTP Proxy API + openapi: "3.0" + info: + version: "2016-09-23T22:23:23Z" + title: "Simple Api" + basePath: "/demo" + schemes: + - "https" + paths: + /http/{proxy+}: + x-amazon-apigateway-any-method: + parameters: + - name: "proxy" + in: "path" + x-amazon-apigateway-integration: + type: "http_proxy" + uri: "http://httpbin.org/{proxy}" + httpMethod: "ANY" + passthroughBehavior: "when_no_match" + requestParameters: + integration.request.path.proxy: "method.request.path.proxy" diff --git a/tests_integ/resources/templates/single/basic_api_inline_swagger.yaml b/tests_integ/resources/templates/single/basic_api_inline_swagger.yaml new file mode 100644 index 000000000..ce9ff2429 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_api_inline_swagger.yaml @@ -0,0 +1,27 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: MyNewStageName + DefinitionBody: + # Simple HTTP Proxy API + swagger: "2.0" + info: + version: "2016-09-23T22:23:23Z" + title: "Simple Api" + basePath: "/demo" + schemes: + - "https" + paths: + /http/{proxy+}: + x-amazon-apigateway-any-method: + parameters: + - name: "proxy" + in: "path" + x-amazon-apigateway-integration: + type: "http_proxy" + uri: "http://httpbin.org/{proxy}" + httpMethod: "ANY" + passthroughBehavior: "when_no_match" + requestParameters: + integration.request.path.proxy: "method.request.path.proxy" diff --git a/tests_integ/resources/templates/single/basic_api_with_cache.yaml b/tests_integ/resources/templates/single/basic_api_with_cache.yaml new file mode 100644 index 000000000..be6779f9e --- /dev/null +++ b/tests_integ/resources/templates/single/basic_api_with_cache.yaml @@ -0,0 +1,12 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionUri: ${definitionuri} + CacheClusterEnabled: true + CacheClusterSize: '1.6' + Variables: + a: foo + b: bar + diff --git a/tests_integ/resources/templates/single/basic_api_with_tags.yaml b/tests_integ/resources/templates/single/basic_api_with_tags.yaml new file mode 100644 index 000000000..c3bce8d8d --- /dev/null +++ b/tests_integ/resources/templates/single/basic_api_with_tags.yaml @@ -0,0 +1,9 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: my-new-stage-name + DefinitionUri: ${definitionuri} + Tags: + TagKey1: TagValue1 + TagKey2: "" diff --git a/tests_integ/resources/templates/single/basic_application_s3_location.yaml b/tests_integ/resources/templates/single/basic_application_s3_location.yaml new file mode 100644 index 000000000..5a6044674 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_application_s3_location.yaml @@ -0,0 +1,5 @@ +Resources: + MyNestedApp: + Type: AWS::Serverless::Application + Properties: + Location: ${templateurl} diff --git a/tests_integ/resources/templates/single/basic_application_sar_location.yaml b/tests_integ/resources/templates/single/basic_application_sar_location.yaml new file mode 100644 index 000000000..5bd59c2e4 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_application_sar_location.yaml @@ -0,0 +1,9 @@ +Resources: + MyNestedApp: + Type: AWS::Serverless::Application + Properties: + Location: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python + SemanticVersion: 1.0.2 + Parameters: + IdentityNameParameter: test diff --git a/tests_integ/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml b/tests_integ/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml new file mode 100644 index 000000000..471af7ae0 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml @@ -0,0 +1,57 @@ +Parameters: + SemanticVersion: + Type: String + Default: 1.0.2 + +Mappings: + SARApplication: + us-east-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python + us-east-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + us-west-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + us-west-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + eu-central-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + eu-west-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + eu-west-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + eu-west-3: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-south-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-northeast-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-northeast-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-southeast-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-southeast-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ca-central-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + sa-east-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + +Resources: + MyNestedApp: + Type: AWS::Serverless::Application + Properties: + Location: + ApplicationId: + Fn::FindInMap: + - SARApplication + - {Ref: 'AWS::Region'} + - ApplicationId + SemanticVersion: + Ref: SemanticVersion + Parameters: + IdentityNameParameter: test + NotificationARNs: + - Ref: MySns + + MySns: + Type: AWS::SNS::Topic \ No newline at end of file diff --git a/tests_integ/resources/templates/single/basic_function.yaml b/tests_integ/resources/templates/single/basic_function.yaml index 29ce6c6c3..3ec291f00 100644 --- a/tests_integ/resources/templates/single/basic_function.yaml +++ b/tests_integ/resources/templates/single/basic_function.yaml @@ -16,4 +16,3 @@ Resources: Variables: Name: Value Name2: Value2 - Name2: Value2 \ No newline at end of file diff --git a/tests_integ/resources/templates/single/basic_function_event_destinations.yaml b/tests_integ/resources/templates/single/basic_function_event_destinations.yaml new file mode 100644 index 000000000..2129313b1 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_event_destinations.yaml @@ -0,0 +1,95 @@ +Conditions: + QueueCreationDisabled: + Fn::Equals: + - false + - true +Resources: + MyTestFunction: + Type: AWS::Serverless::Function + Properties: + EventInvokeConfig: + MaximumEventAgeInSeconds: 70 + MaximumRetryAttempts: 1 + DestinationConfig: + OnSuccess: + Type: SQS + Destination: + Fn::If: + - QueueCreationDisabled + - Fn::GetAtt: + - DestinationSQS + - Arn + - Ref: 'AWS::NoValue' + OnFailure: + Type: Lambda + Destination: + Fn::GetAtt: + - DestinationLambda + - Arn + InlineCode: | + exports.handler = function(event, context, callback) { + var event_received_at = new Date().toISOString(); + console.log('Event received at: ' + event_received_at); + console.log('Received event:', JSON.stringify(event, null, 2)); + if (event.Success) { + console.log("Success"); + context.callbackWaitsForEmptyEventLoop = false; + callback(null); + } else { + console.log("Failure"); + context.callbackWaitsForEmptyEventLoop = false; + callback(new Error("Failure from event, Success = false, I am failing!"), 'Destination Function Error Thrown'); + } + }; + Handler: index.handler + Runtime: nodejs10.x + MemorySize: 1024 + MyTestFunction2: + Type: AWS::Serverless::Function + Properties: + AutoPublishAlias: live + EventInvokeConfig: + MaximumEventAgeInSeconds: 80 + MaximumRetryAttempts: 2 + DestinationConfig: + OnSuccess: + Type: SNS + OnFailure: + Type: EventBridge + Destination: + Fn::Sub: arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/default + InlineCode: | + exports.handler = function(event, context, callback) { + var event_received_at = new Date().toISOString(); + console.log('Event received at: ' + event_received_at); + console.log('Received event:', JSON.stringify(event, null, 2)); + if (event.Success) { + console.log("Success"); + context.callbackWaitsForEmptyEventLoop = false; + callback(null); + } else { + console.log("Failure"); + context.callbackWaitsForEmptyEventLoop = false; + callback(new Error("Failure from event, Success = false, I am failing!"), 'Destination Function Error Thrown'); + } + }; + Handler: index.handler + Runtime: nodejs10.x + MemorySize: 1024 + DestinationLambda: + Type: AWS::Serverless::Function + Properties: + InlineCode: | + exports.handler = async (event) => { + const response = { + statusCode: 200, + body: JSON.stringify('Hello from Lambda!'), + }; + return response; + }; + Handler: index.handler + Runtime: nodejs10.x + MemorySize: 1024 + DestinationSQS: + Condition: QueueCreationDisabled + Type: AWS::SQS::Queue diff --git a/tests_integ/resources/templates/single/basic_function_no_envvar.yaml b/tests_integ/resources/templates/single/basic_function_no_envvar.yaml new file mode 100644 index 000000000..6ecedfd92 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_no_envvar.yaml @@ -0,0 +1,11 @@ +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess diff --git a/tests_integ/resources/templates/single/basic_function_openapi.yaml b/tests_integ/resources/templates/single/basic_function_openapi.yaml new file mode 100644 index 000000000..359f7d58b --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_openapi.yaml @@ -0,0 +1,18 @@ +Globals: + Api: + OpenApiVersion: 3.0.1 +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Environment: + Variables: + Name: Value + Name2: Value2 diff --git a/tests_integ/resources/templates/single/basic_function_with_kmskeyarn.yaml b/tests_integ/resources/templates/single/basic_function_with_kmskeyarn.yaml new file mode 100644 index 000000000..91a9fc05f --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_with_kmskeyarn.yaml @@ -0,0 +1,32 @@ +Resources: + BasicFunctionWithKmsKeyArn: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Environment: + Variables: + Key: Value + KmsKeyArn: + Fn::GetAtt: [MyKey, Arn] + + + MyKey: + Type: "AWS::KMS::Key" + Properties: + Description: "A sample key" + KeyPolicy: + Version: "2012-10-17" + Id: "key-default-1" + Statement: + - + Sid: "Allow administration of the key" + Effect: "Allow" + Principal: + AWS: + Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: + - "kms:*" + Resource: "*" \ No newline at end of file diff --git a/tests_integ/resources/templates/single/basic_function_with_sns_dlq.yaml b/tests_integ/resources/templates/single/basic_function_with_sns_dlq.yaml new file mode 100644 index 000000000..0d4435fef --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_with_sns_dlq.yaml @@ -0,0 +1,14 @@ +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + DeadLetterQueue: + Type: SNS + TargetArn: + Ref: "MyTopic" + + MyTopic: + Type: AWS::SNS::Topic diff --git a/tests_integ/resources/templates/single/basic_function_with_sqs_dlq.yaml b/tests_integ/resources/templates/single/basic_function_with_sqs_dlq.yaml new file mode 100644 index 000000000..26aa16b01 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_with_sqs_dlq.yaml @@ -0,0 +1,15 @@ + +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + DeadLetterQueue: + Type: SQS + TargetArn: + Fn::GetAtt: ["MyQueue", "Arn"] + + MyQueue: + Type: AWS::SQS::Queue diff --git a/tests_integ/resources/templates/single/basic_function_with_tags.yaml b/tests_integ/resources/templates/single/basic_function_with_tags.yaml new file mode 100644 index 000000000..cc815a52e --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_with_tags.yaml @@ -0,0 +1,14 @@ +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Tags: + TagKey1: TagValue1 + TagKey2: "" diff --git a/tests_integ/resources/templates/single/basic_function_with_tracing.yaml b/tests_integ/resources/templates/single/basic_function_with_tracing.yaml new file mode 100644 index 000000000..f9fa0809c --- /dev/null +++ b/tests_integ/resources/templates/single/basic_function_with_tracing.yaml @@ -0,0 +1,36 @@ +Parameters: + Bucket: + Type: String + CodeKey: + Type: String + SwaggerKey: + Type: String + TracingParamPassThrough: + Type: String + Default: PassThrough + +Resources: + ActiveTracingFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Tracing: Active + + PassThroughTracingFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Tracing: + Ref: TracingParamPassThrough diff --git a/tests_integ/resources/templates/single/basic_http_api.yaml b/tests_integ/resources/templates/single/basic_http_api.yaml new file mode 100644 index 000000000..27e3c340d --- /dev/null +++ b/tests_integ/resources/templates/single/basic_http_api.yaml @@ -0,0 +1,11 @@ +Resources: + MyApi: + Type: AWS::Serverless::HttpApi + Properties: + DefinitionBody: + info: + version: '1.0' + title: + Ref: AWS::StackName + openapi: 3.0.1 + paths: {} \ No newline at end of file diff --git a/tests_integ/resources/templates/single/basic_layer.yaml b/tests_integ/resources/templates/single/basic_layer.yaml new file mode 100644 index 000000000..6b308fd91 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_layer.yaml @@ -0,0 +1,6 @@ +Resources: + MyLayerVersion: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: ${contenturi} + RetentionPolicy: Delete diff --git a/tests_integ/resources/templates/single/basic_layer_with_parameters.yaml b/tests_integ/resources/templates/single/basic_layer_with_parameters.yaml new file mode 100644 index 000000000..f5eb8fdb8 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_layer_with_parameters.yaml @@ -0,0 +1,46 @@ +Parameters: + Retention: + Type: String + Default: Retain + License: + Type: String + Default: MIT-0 + Runtimes: + Type: CommaDelimitedList + Default: nodejs12.x,nodejs10.x + LayerName: + Type: String + Default: MyNamedLayerVersion + Description: + Type: String + Default: Some description about this layer goes here + +Resources: + MyLayerVersion: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: ${contenturi} + LayerName: + Ref: LayerName + RetentionPolicy: + Ref: Retention + CompatibleRuntimes: + Ref: Runtimes + LicenseInfo: + Ref: License + Description: + Ref: Description + +Outputs: + MyLayerArn: + Value: + Ref: MyLayerVersion + License: + Value: + Ref: License + Description: + Value: + Ref: Description + LayerName: + Value: + Ref: LayerName \ No newline at end of file diff --git a/tests_integ/resources/templates/single/basic_state_machine_inline_definition.yaml b/tests_integ/resources/templates/single/basic_state_machine_inline_definition.yaml new file mode 100644 index 000000000..8fb8a16cd --- /dev/null +++ b/tests_integ/resources/templates/single/basic_state_machine_inline_definition.yaml @@ -0,0 +1,23 @@ +Resources: + MyBasicStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + Type: STANDARD + Definition: + Comment: A Hello World example of the Amazon States Language using Pass states + StartAt: Hello + States: + Hello: + Type: Pass + Result: Hello + Next: World + World: + Type: Pass + Result: World + End: true + Policies: + - Version: '2012-10-17' + Statement: + - Effect: Deny + Action: "*" + Resource: "*" diff --git a/tests_integ/resources/templates/single/basic_state_machine_with_tags.yaml b/tests_integ/resources/templates/single/basic_state_machine_with_tags.yaml new file mode 100644 index 000000000..815652641 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_state_machine_with_tags.yaml @@ -0,0 +1,33 @@ +Resources: + MyStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + Definition: + Comment: A Hello World example of the Amazon States Language using Pass states + StartAt: Hello + States: + Hello: + Type: Pass + Result: Hello + Next: World + World: + Type: Pass + Result: World + End: true + Policies: + - Version: "2012-10-17" + Statement: + - Effect: Deny + Action: "*" + Resource: "*" + Tags: + TagOne: ValueOne + TagTwo: ValueTwo + Tracing: + Enabled: true + +Outputs: + MyStateMachineArn: + Description: ARN of the state machine + Value: + Ref: MyStateMachine diff --git a/tests_integ/resources/templates/single/basic_table_no_param.yaml b/tests_integ/resources/templates/single/basic_table_no_param.yaml new file mode 100644 index 000000000..ae4fe8164 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_table_no_param.yaml @@ -0,0 +1,4 @@ +Resources: + MyApi: + Type: AWS::Serverless::SimpleTable + # SimpleTable does NOT require any parameters diff --git a/tests_integ/resources/templates/single/basic_table_with_param.yaml b/tests_integ/resources/templates/single/basic_table_with_param.yaml new file mode 100644 index 000000000..c0a7039c3 --- /dev/null +++ b/tests_integ/resources/templates/single/basic_table_with_param.yaml @@ -0,0 +1,12 @@ +Resources: + MyApi: + Type: AWS::Serverless::SimpleTable + Properties: + + PrimaryKey: + Name: mynewid + Type: Number + + ProvisionedThroughput: + ReadCapacityUnits: 2 + WriteCapacityUnits: 2 From 0faf6e47b18ee59637fd21162633c51556febad1 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 10 Nov 2020 08:45:37 -0800 Subject: [PATCH 027/105] Added test_basic_function.py from the POC, untouched; removed the "expected" prefix of the basic function result as we already are in the expected directory --- ...asic_function.json => basic_function.json} | 0 tests_integ/single/test_basic_function.py | 108 ++++++++++++++++++ 2 files changed, 108 insertions(+) rename tests_integ/resources/expected/single/{expected_basic_function.json => basic_function.json} (100%) create mode 100644 tests_integ/single/test_basic_function.py diff --git a/tests_integ/resources/expected/single/expected_basic_function.json b/tests_integ/resources/expected/single/basic_function.json similarity index 100% rename from tests_integ/resources/expected/single/expected_basic_function.json rename to tests_integ/resources/expected/single/basic_function.json diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py new file mode 100644 index 000000000..5a035b987 --- /dev/null +++ b/tests_integ/single/test_basic_function.py @@ -0,0 +1,108 @@ +import json +import logging +import os +import sys +from functools import reduce +from unittest.case import TestCase + +import boto3 +from samcli.lib.deploy.deployer import Deployer +from samtranslator.model.exceptions import InvalidDocumentException +from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader +from samtranslator.translator.transform import transform +from samtranslator.yaml_helper import yaml_parse + +LOG = logging.getLogger(__name__) +iam_client = boto3.client("iam") +my_path = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, my_path + "/..") + +STACK_NAME = "foss-integ-test" +INPUT_TEMPLATE = "basic_function.yaml" +TRANSLATED_CFN_TEMPLATE = "cfn_basic_function.yaml" +EXPECTED_JSON_FILE = "expected_basic_function.json" + + +# can we import this from sam-translate.py? +def transform_template(input_file_path, output_file_path): + with open(input_file_path, "r") as f: + sam_template = yaml_parse(f) + + try: + cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) + cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) + + with open(output_file_path, "w") as f: + f.write(cloud_formation_template_prettified) + + print("Wrote transformed CloudFormation template to: " + output_file_path) + except InvalidDocumentException as e: + errorMessage = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) + LOG.error(errorMessage) + errors = map(lambda cause: cause.message, e.causes) + LOG.error(errors) + + +def verify_stack_resources(expected_file_path, stack_resources): + with open(expected_file_path, 'r') as expected_data: + expected_resources = json.load(expected_data) + parsed_resources = _parse_stack_resources(stack_resources) + return expected_resources == parsed_resources + + +def _parse_stack_resources(stack_resources): + logic_id_to_resource_type = {} + for resource in stack_resources['StackResourceSummaries']: + logic_id_to_resource_type[resource['LogicalResourceId']] = resource['ResourceType'] + return logic_id_to_resource_type + + +class TestBasicFunction(TestCase): + # set up before every tests run: + # upload test code to s3 + # replace the template's codeuri with s3 location + + # move client set up to here and create template folder to store translated cfn template, or upload template to s3? + # def setUp(self): + # pass + + def test_basic_function(self): + # make stack + # transform sam template to cfn template + cwd = os.getcwd() + input_file_path = os.path.join(cwd, 'resources', 'templates', 'single', INPUT_TEMPLATE) + output_file_path = os.path.join(cwd, TRANSLATED_CFN_TEMPLATE) + expected_resource_path = os.path.join(cwd, EXPECTED_JSON_FILE) + transform_template(input_file_path, output_file_path) + + # cfn part + cloudformation_client = boto3.client("cloudformation") + deployer = Deployer(cloudformation_client, changeset_prefix="foss-integ") + + # deploy to cfn + with open(output_file_path, 'r') as cfn_file: + result, changeset_type = deployer.create_and_wait_for_changeset( + stack_name=STACK_NAME, + cfn_template=cfn_file.read(), + parameter_values=[], + capabilities=['CAPABILITY_IAM'], + role_arn=None, + notification_arns=[], + s3_uploader=None, + tags=[], + ) + deployer.execute_changeset(result["Id"], STACK_NAME) + deployer.wait_for_execute(STACK_NAME, changeset_type) + + # verify + stacks_description = cloudformation_client.describe_stacks(StackName=STACK_NAME) + stack_resources = cloudformation_client.list_stack_resources(StackName=STACK_NAME) + # verify if the stack is create successfully or not + self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') + # verify if the stack contain the expected resources + self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) + + # clean up + # delete stack and delete translated cfn template + # def tearDown(self): + # pass From dedd33b40a053e7ed55e9574fe2042d8a33cd9d1 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Tue, 10 Nov 2020 16:04:08 -0800 Subject: [PATCH 028/105] set up basic code structure for sam integration test --- tests_integ/helpers/__init__.py | 0 tests_integ/helpers/helpers.py | 46 ++++++++++ tests_integ/single/__init__.py | 0 tests_integ/single/test_basic_function.py | 106 ++++++---------------- 4 files changed, 76 insertions(+), 76 deletions(-) create mode 100644 tests_integ/helpers/__init__.py create mode 100644 tests_integ/helpers/helpers.py create mode 100644 tests_integ/single/__init__.py diff --git a/tests_integ/helpers/__init__.py b/tests_integ/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py new file mode 100644 index 000000000..85d32789f --- /dev/null +++ b/tests_integ/helpers/helpers.py @@ -0,0 +1,46 @@ +import json +import logging +from functools import reduce + +import boto3 + +from samtranslator.model.exceptions import InvalidDocumentException +from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader +from samtranslator.translator.transform import transform +from samtranslator.yaml_helper import yaml_parse + + +def transform_template(input_file_path, output_file_path): + LOG = logging.getLogger(__name__) + iam_client = boto3.client("iam") + + with open(input_file_path, "r") as f: + sam_template = yaml_parse(f) + + try: + cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) + cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) + + with open(output_file_path, "w") as f: + f.write(cloud_formation_template_prettified) + + print("Wrote transformed CloudFormation template to: " + output_file_path) + except InvalidDocumentException as e: + errorMessage = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) + LOG.error(errorMessage) + errors = map(lambda cause: cause.message, e.causes) + LOG.error(errors) + + +def verify_stack_resources(expected_file_path, stack_resources): + with open(expected_file_path, 'r') as expected_data: + expected_resources = json.load(expected_data) + parsed_resources = _parse_stack_resources(stack_resources) + return expected_resources == parsed_resources + + +def _parse_stack_resources(stack_resources): + logic_id_to_resource_type = {} + for resource in stack_resources['StackResourceSummaries']: + logic_id_to_resource_type[resource['LogicalResourceId']] = resource['ResourceType'] + return logic_id_to_resource_type \ No newline at end of file diff --git a/tests_integ/single/__init__.py b/tests_integ/single/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index 5a035b987..b8e90837e 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -1,88 +1,41 @@ -import json -import logging import os -import sys -from functools import reduce +from pathlib import Path from unittest.case import TestCase import boto3 +import pytest +from parameterized import parameterized from samcli.lib.deploy.deployer import Deployer -from samtranslator.model.exceptions import InvalidDocumentException -from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader -from samtranslator.translator.transform import transform -from samtranslator.yaml_helper import yaml_parse +from tests_integ.helpers.helpers import transform_template, verify_stack_resources -LOG = logging.getLogger(__name__) -iam_client = boto3.client("iam") -my_path = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, my_path + "/..") - -STACK_NAME = "foss-integ-test" -INPUT_TEMPLATE = "basic_function.yaml" -TRANSLATED_CFN_TEMPLATE = "cfn_basic_function.yaml" -EXPECTED_JSON_FILE = "expected_basic_function.json" - - -# can we import this from sam-translate.py? -def transform_template(input_file_path, output_file_path): - with open(input_file_path, "r") as f: - sam_template = yaml_parse(f) - - try: - cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) - cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) - - with open(output_file_path, "w") as f: - f.write(cloud_formation_template_prettified) - - print("Wrote transformed CloudFormation template to: " + output_file_path) - except InvalidDocumentException as e: - errorMessage = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) - LOG.error(errorMessage) - errors = map(lambda cause: cause.message, e.causes) - LOG.error(errors) - - -def verify_stack_resources(expected_file_path, stack_resources): - with open(expected_file_path, 'r') as expected_data: - expected_resources = json.load(expected_data) - parsed_resources = _parse_stack_resources(stack_resources) - return expected_resources == parsed_resources - - -def _parse_stack_resources(stack_resources): - logic_id_to_resource_type = {} - for resource in stack_resources['StackResourceSummaries']: - logic_id_to_resource_type[resource['LogicalResourceId']] = resource['ResourceType'] - return logic_id_to_resource_type +STACK_NAME_PREFIX = "sam-integ-test-" class TestBasicFunction(TestCase): # set up before every tests run: - # upload test code to s3 - # replace the template's codeuri with s3 location + def setUp(self): + tests_integ_dir = Path(__file__).resolve().parents[1] + self.template_dir = Path(tests_integ_dir, 'resources', 'templates', 'single') + self.output_dir = tests_integ_dir + self.expected_dir = Path(tests_integ_dir, 'resources', 'expected', 'single') - # move client set up to here and create template folder to store translated cfn template, or upload template to s3? - # def setUp(self): - # pass + self.cloudformation_client = boto3.client("cloudformation") + self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") - def test_basic_function(self): - # make stack - # transform sam template to cfn template - cwd = os.getcwd() - input_file_path = os.path.join(cwd, 'resources', 'templates', 'single', INPUT_TEMPLATE) - output_file_path = os.path.join(cwd, TRANSLATED_CFN_TEMPLATE) - expected_resource_path = os.path.join(cwd, EXPECTED_JSON_FILE) - transform_template(input_file_path, output_file_path) + @parameterized.expand([("basic_function.yaml", "cfn_basic_function.yaml", "basic_function.json")]) + @pytest.mark.flaky(reruns=1) # why we need this one? + def test_basic_function(self, input_file_name, out_put_file_name, expected_name): + input_file_path = str(Path(self.template_dir, input_file_name)) + self.output_file_path = str(Path(self.output_dir, out_put_file_name)) + expected_resource_path = str(Path(self.expected_dir, expected_name)) + self.stack_name = STACK_NAME_PREFIX + input_file_name.split('.')[0].replace('_', '-') - # cfn part - cloudformation_client = boto3.client("cloudformation") - deployer = Deployer(cloudformation_client, changeset_prefix="foss-integ") + transform_template(input_file_path, self.output_file_path) # deploy to cfn - with open(output_file_path, 'r') as cfn_file: - result, changeset_type = deployer.create_and_wait_for_changeset( - stack_name=STACK_NAME, + with open(self.output_file_path, 'r') as cfn_file: + result, changeset_type = self.deployer.create_and_wait_for_changeset( + stack_name=self.stack_name, cfn_template=cfn_file.read(), parameter_values=[], capabilities=['CAPABILITY_IAM'], @@ -91,12 +44,12 @@ def test_basic_function(self): s3_uploader=None, tags=[], ) - deployer.execute_changeset(result["Id"], STACK_NAME) - deployer.wait_for_execute(STACK_NAME, changeset_type) + self.deployer.execute_changeset(result["Id"], self.stack_name) + self.deployer.wait_for_execute(self.stack_name, changeset_type) # verify - stacks_description = cloudformation_client.describe_stacks(StackName=STACK_NAME) - stack_resources = cloudformation_client.list_stack_resources(StackName=STACK_NAME) + stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) + stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) # verify if the stack is create successfully or not self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') # verify if the stack contain the expected resources @@ -104,5 +57,6 @@ def test_basic_function(self): # clean up # delete stack and delete translated cfn template - # def tearDown(self): - # pass + def tearDown(self): + self.cloudformation_client.delete_stack(StackName=self.stack_name) + os.remove(self.output_file_path) From b4eb18ddcb23c16a0841419c406934d2ac44e11d Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 10 Nov 2020 15:44:55 -0800 Subject: [PATCH 029/105] Added basic api inline openapi test as a standalone file to be merged later into a single basic test file, added utils/stack.py --- .../single/basic_api_inline_openapi.json | 5 + tests_integ/single/__init__.py | 1 + .../single/basic_api_inline_openapi.py | 98 +++++++++++++++++++ tests_integ/single/utils/__init__.py | 0 tests_integ/single/utils/stack.py | 24 +++++ 5 files changed, 128 insertions(+) create mode 100644 tests_integ/resources/expected/single/basic_api_inline_openapi.json create mode 100644 tests_integ/single/basic_api_inline_openapi.py create mode 100755 tests_integ/single/utils/__init__.py create mode 100644 tests_integ/single/utils/stack.py diff --git a/tests_integ/resources/expected/single/basic_api_inline_openapi.json b/tests_integ/resources/expected/single/basic_api_inline_openapi.json new file mode 100644 index 000000000..b6ca8490f --- /dev/null +++ b/tests_integ/resources/expected/single/basic_api_inline_openapi.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi"}, + { "LogicalResourceId":"MyApiDeployment[0-9a-e]{10}", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/tests_integ/single/__init__.py b/tests_integ/single/__init__.py index e69de29bb..8b1378917 100644 --- a/tests_integ/single/__init__.py +++ b/tests_integ/single/__init__.py @@ -0,0 +1 @@ + diff --git a/tests_integ/single/basic_api_inline_openapi.py b/tests_integ/single/basic_api_inline_openapi.py new file mode 100644 index 000000000..958a759ae --- /dev/null +++ b/tests_integ/single/basic_api_inline_openapi.py @@ -0,0 +1,98 @@ +import json +import logging +import os +import sys +from functools import reduce +from unittest.case import TestCase + +import boto3 +from samcli.lib.deploy.deployer import Deployer +from samtranslator.model.exceptions import InvalidDocumentException +from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader +from samtranslator.translator.transform import transform +from samtranslator.yaml_helper import yaml_parse +from utils import stack + +LOG = logging.getLogger(__name__) +iam_client = boto3.client("iam") +my_path = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, my_path + "/..") + +STACK_NAME = "foss-integ-test" +INPUT_TEMPLATE = "basic_api_inline_openapi.yaml" +TRANSLATED_CFN_TEMPLATE = "cfn_basic_api_inline_openapi.yaml" +EXPECTED_JSON_FILE = "basic_api_inline_openapi.json" + +# can we import this from sam-translate.py? +def transform_template(input_file_path, output_file_path): + with open(input_file_path, "r") as f: + sam_template = yaml_parse(f) + + try: + cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) + cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) + + with open(output_file_path, "w") as f: + f.write(cloud_formation_template_prettified) + + print("Wrote transformed CloudFormation template to: " + output_file_path) + except InvalidDocumentException as e: + errorMessage = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) + LOG.error(errorMessage) + errors = map(lambda cause: cause.message, e.causes) + LOG.error(errors) + +class TestBasicFunction(TestCase): + # set up before every tests run: + # upload test code to s3 + # replace the template's codeuri with s3 location + + # move client set up to here and create template folder to store translated cfn template, or upload template to s3? + # def setUp(self): + # pass + + def test_basic_function(self): + # make stack + # transform sam template to cfn template + cwd = os.getcwd() + input_file_path = os.path.join(cwd, 'tests_integ', 'resources', 'templates', 'single', INPUT_TEMPLATE) + output_file_path = os.path.join(cwd, TRANSLATED_CFN_TEMPLATE) + expected_resource_path = os.path.join(cwd, 'tests_integ', 'resources', 'expected', 'single', EXPECTED_JSON_FILE) + transform_template(input_file_path, output_file_path) + + # cfn part + cloudformation_client = boto3.client("cloudformation") + deployer = Deployer(cloudformation_client, changeset_prefix="foss-integ") + + # deploy to cfn + with open(output_file_path, 'r') as cfn_file: + result, changeset_type = deployer.create_and_wait_for_changeset( + stack_name=STACK_NAME, + cfn_template=cfn_file.read(), + parameter_values=[], + capabilities=['CAPABILITY_IAM'], + role_arn=None, + notification_arns=[], + s3_uploader=None, + tags=[], + ) + deployer.execute_changeset(result["Id"], STACK_NAME) + deployer.wait_for_execute(STACK_NAME, changeset_type) + + # verify + stacks_description = cloudformation_client.describe_stacks(StackName=STACK_NAME) + stack_resources = cloudformation_client.list_stack_resources(StackName=STACK_NAME) + # verify if the stack is create successfully or not + self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') + # verify if the stack contain the expected resources + self.assertTrue(stack.verify_stack_resources(expected_resource_path, stack_resources)) + + cloudformation_client.delete_stack(StackName=STACK_NAME) + + # clean up + # delete stack and delete translated cfn template + # def tearDown(self): + # pass + +#test = TestBasicFunction() +#test.test_basic_function() diff --git a/tests_integ/single/utils/__init__.py b/tests_integ/single/utils/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/tests_integ/single/utils/stack.py b/tests_integ/single/utils/stack.py new file mode 100644 index 000000000..ff35cfbdf --- /dev/null +++ b/tests_integ/single/utils/stack.py @@ -0,0 +1,24 @@ +import json +import re + +def verify_stack_resources(expected_file_path, stack_resources): + with open(expected_file_path, 'r') as expected_data: + expected_resources = _sort_resources(json.load(expected_data)) + parsed_resources = _sort_resources(stack_resources['StackResourceSummaries']) + + if len(expected_resources) != len(parsed_resources): + return False + + for i, in enumerate(expected_resources): + exp = expected_resources[i] + parsed = parsed_resources[i] + if parsed["ResourceStatus"] != "CREATE_COMPLETE": + return False + if re.fullmatch(exp["LogicalResourceId"], parsed["LogicalResourceId"]) is None: + return False + if exp["ResourceType"] != parsed["ResourceType"]: + return False + return True + +def _sort_resources(resources): + return sorted(resources, key=lambda d: d["LogicalResourceId"]) From f7fc08bbcc68783056f197935d38f971a243a722 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 10 Nov 2020 15:47:25 -0800 Subject: [PATCH 030/105] Renamed basic api inline openapi test with the test prefix --- ...sic_api_inline_openapi.py => test_basic_api_inline_openapi.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests_integ/single/{basic_api_inline_openapi.py => test_basic_api_inline_openapi.py} (100%) diff --git a/tests_integ/single/basic_api_inline_openapi.py b/tests_integ/single/test_basic_api_inline_openapi.py similarity index 100% rename from tests_integ/single/basic_api_inline_openapi.py rename to tests_integ/single/test_basic_api_inline_openapi.py From 0a9621435be14bdbd3792ed2d05b5ac316c41020 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 10 Nov 2020 15:58:25 -0800 Subject: [PATCH 031/105] fix: Reverted to range instead of enumerate --- tests_integ/single/utils/stack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests_integ/single/utils/stack.py b/tests_integ/single/utils/stack.py index ff35cfbdf..e6d4ca7c5 100644 --- a/tests_integ/single/utils/stack.py +++ b/tests_integ/single/utils/stack.py @@ -9,7 +9,7 @@ def verify_stack_resources(expected_file_path, stack_resources): if len(expected_resources) != len(parsed_resources): return False - for i, in enumerate(expected_resources): + for i in range(len(expected_resources)): exp = expected_resources[i] parsed = parsed_resources[i] if parsed["ResourceStatus"] != "CREATE_COMPLETE": From a88b80b72c75cce0850e894b4fd25053758a7cfa Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Thu, 12 Nov 2020 11:13:42 -0800 Subject: [PATCH 032/105] Updated helpers and expected basic_function file for the new verification. Updated the makefile to not check coverage for integration tests --- Makefile | 4 +-- tests_integ/helpers/helpers.py | 31 ++++++++++++------- .../expected/single/basic_function.json | 8 ++--- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index 6f78c4d83..e14ecc2e4 100755 --- a/Makefile +++ b/Makefile @@ -6,10 +6,10 @@ init: pip install -e '.[dev]' test: - pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests + pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests/* test-integ: - pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests_integ + pytest --no-cov tests_integ/* black: black setup.py samtranslator/* tests/* bin/*.py diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 85d32789f..89c32cdfe 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -1,5 +1,6 @@ import json import logging +import re from functools import reduce import boto3 @@ -31,16 +32,24 @@ def transform_template(input_file_path, output_file_path): errors = map(lambda cause: cause.message, e.causes) LOG.error(errors) - def verify_stack_resources(expected_file_path, stack_resources): with open(expected_file_path, 'r') as expected_data: - expected_resources = json.load(expected_data) - parsed_resources = _parse_stack_resources(stack_resources) - return expected_resources == parsed_resources - - -def _parse_stack_resources(stack_resources): - logic_id_to_resource_type = {} - for resource in stack_resources['StackResourceSummaries']: - logic_id_to_resource_type[resource['LogicalResourceId']] = resource['ResourceType'] - return logic_id_to_resource_type \ No newline at end of file + expected_resources = _sort_resources(json.load(expected_data)) + parsed_resources = _sort_resources(stack_resources['StackResourceSummaries']) + + if len(expected_resources) != len(parsed_resources): + return False + + for i in range(len(expected_resources)): + exp = expected_resources[i] + parsed = parsed_resources[i] + if parsed["ResourceStatus"] != "CREATE_COMPLETE": + return False + if re.fullmatch(exp["LogicalResourceId"], parsed["LogicalResourceId"]) is None: + return False + if exp["ResourceType"] != parsed["ResourceType"]: + return False + return True + +def _sort_resources(resources): + return sorted(resources, key=lambda d: d["LogicalResourceId"]) diff --git a/tests_integ/resources/expected/single/basic_function.json b/tests_integ/resources/expected/single/basic_function.json index 1c0f5f116..4e83eaf6b 100644 --- a/tests_integ/resources/expected/single/basic_function.json +++ b/tests_integ/resources/expected/single/basic_function.json @@ -1,4 +1,4 @@ -{ - "MyLambdaFunction": "AWS::Lambda::Function", - "MyLambdaFunctionRole": "AWS::IAM::Role" -} \ No newline at end of file +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } +] From 65bb94ae4e13bf38fb3b95cbe161c6a78db8e879 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Thu, 12 Nov 2020 15:32:41 -0800 Subject: [PATCH 033/105] add expected json file and fix some bugs --- tests_integ/__init__.py | 0 tests_integ/helpers/helpers.py | 9 ++++++++- .../expected/single/basic_api_inline_openapi.json | 2 +- .../single/basic_function_event_destinations.json | 14 ++++++++++++++ .../expected/single/basic_function_no_envvar.json | 4 ++++ .../expected/single/basic_function_openapi.json | 4 ++++ .../single/basic_function_with_kmskeyarn.json | 5 +++++ .../single/basic_function_with_sns_dlq.json | 5 +++++ .../single/basic_function_with_sqs_dlq.json | 5 +++++ .../expected/single/basic_function_with_tags.json | 4 ++++ .../single/basic_function_with_tracing.json | 6 ++++++ .../resources/expected/single/basic_http_api.json | 4 ++++ .../resources/expected/single/basic_layer.json | 3 +++ .../single/basic_layer_with_parameters.json | 3 +++ .../basic_state_machine_inline_definition.json | 4 ++++ .../single/basic_state_machine_with_tags.json | 0 16 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests_integ/__init__.py create mode 100644 tests_integ/resources/expected/single/basic_function_event_destinations.json create mode 100644 tests_integ/resources/expected/single/basic_function_no_envvar.json create mode 100644 tests_integ/resources/expected/single/basic_function_openapi.json create mode 100644 tests_integ/resources/expected/single/basic_function_with_kmskeyarn.json create mode 100644 tests_integ/resources/expected/single/basic_function_with_sns_dlq.json create mode 100644 tests_integ/resources/expected/single/basic_function_with_sqs_dlq.json create mode 100644 tests_integ/resources/expected/single/basic_function_with_tags.json create mode 100644 tests_integ/resources/expected/single/basic_function_with_tracing.json create mode 100644 tests_integ/resources/expected/single/basic_http_api.json create mode 100644 tests_integ/resources/expected/single/basic_layer.json create mode 100644 tests_integ/resources/expected/single/basic_layer_with_parameters.json create mode 100644 tests_integ/resources/expected/single/basic_state_machine_inline_definition.json create mode 100644 tests_integ/resources/expected/single/basic_state_machine_with_tags.json diff --git a/tests_integ/__init__.py b/tests_integ/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 89c32cdfe..24183d9fb 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -32,6 +32,7 @@ def transform_template(input_file_path, output_file_path): errors = map(lambda cause: cause.message, e.causes) LOG.error(errors) + def verify_stack_resources(expected_file_path, stack_resources): with open(expected_file_path, 'r') as expected_data: expected_resources = _sort_resources(json.load(expected_data)) @@ -44,12 +45,18 @@ def verify_stack_resources(expected_file_path, stack_resources): exp = expected_resources[i] parsed = parsed_resources[i] if parsed["ResourceStatus"] != "CREATE_COMPLETE": + print("---------not complete") return False - if re.fullmatch(exp["LogicalResourceId"], parsed["LogicalResourceId"]) is None: + if not re.fullmatch(exp["LogicalResourceId"]+"([0-9a-f]{10})?", parsed["LogicalResourceId"]): + print("---------id not match") + print(exp["LogicalResourceId"]) + print(parsed["LogicalResourceId"]) return False if exp["ResourceType"] != parsed["ResourceType"]: + print("---------resource not match") return False return True + def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) diff --git a/tests_integ/resources/expected/single/basic_api_inline_openapi.json b/tests_integ/resources/expected/single/basic_api_inline_openapi.json index b6ca8490f..218baf53c 100644 --- a/tests_integ/resources/expected/single/basic_api_inline_openapi.json +++ b/tests_integ/resources/expected/single/basic_api_inline_openapi.json @@ -1,5 +1,5 @@ [ { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi"}, - { "LogicalResourceId":"MyApiDeployment[0-9a-e]{10}", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } ] diff --git a/tests_integ/resources/expected/single/basic_function_event_destinations.json b/tests_integ/resources/expected/single/basic_function_event_destinations.json new file mode 100644 index 000000000..3588c9d13 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_event_destinations.json @@ -0,0 +1,14 @@ +[ + { "LogicalResourceId":"MyTestFunction2", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyTestFunction2EventInvokeConfigOnSuccessTopic", "ResourceType":"AWS::SNS::Topic" }, + { "LogicalResourceId":"MyTestFunctionRole", "ResourceType":"AWS::IAM::Role"}, + { "LogicalResourceId":"MyTestFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyTestFunction2Aliaslive", "ResourceType":"AWS::Lambda::Alias"}, + { "LogicalResourceId":"MyTestFunctionEventInvokeConfigOnSuccessQueue", "ResourceType":"AWS::SQS::Queue"}, + { "LogicalResourceId":"MyTestFunction2Role", "ResourceType":"AWS::IAM::Role"}, + { "LogicalResourceId":"DestinationLambda", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyTestFunction2EventInvokeConfig", "ResourceType":"AWS::Lambda::EventInvokeConfig"}, + { "LogicalResourceId":"MyTestFunction2Version", "ResourceType":"AWS::Lambda::Version"}, + { "LogicalResourceId":"MyTestFunctionEventInvokeConfig", "ResourceType":"AWS::Lambda::EventInvokeConfig"}, + { "LogicalResourceId":"DestinationLambdaRole", "ResourceType":"AWS::IAM::Role"} +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_function_no_envvar.json b/tests_integ/resources/expected/single/basic_function_no_envvar.json new file mode 100644 index 000000000..98d62e4bb --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_no_envvar.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_function_openapi.json b/tests_integ/resources/expected/single/basic_function_openapi.json new file mode 100644 index 000000000..98d62e4bb --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_openapi.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_function_with_kmskeyarn.json b/tests_integ/resources/expected/single/basic_function_with_kmskeyarn.json new file mode 100644 index 000000000..5cba0276b --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_with_kmskeyarn.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"BasicFunctionWithKmsKeyArn", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"BasicFunctionWithKmsKeyArnRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"MyKey", "ResourceType":"AWS::KMS::Key" } +] diff --git a/tests_integ/resources/expected/single/basic_function_with_sns_dlq.json b/tests_integ/resources/expected/single/basic_function_with_sns_dlq.json new file mode 100644 index 000000000..3ded5dd12 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_with_sns_dlq.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyFunctionRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"MyTopic", "ResourceType":"AWS::SNS::Topic" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_function_with_sqs_dlq.json b/tests_integ/resources/expected/single/basic_function_with_sqs_dlq.json new file mode 100644 index 000000000..a29c8734b --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_with_sqs_dlq.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyFunctionRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"MyQueue", "ResourceType":"AWS::SQS::Queue" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_function_with_tags.json b/tests_integ/resources/expected/single/basic_function_with_tags.json new file mode 100644 index 000000000..4e83eaf6b --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_with_tags.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } +] diff --git a/tests_integ/resources/expected/single/basic_function_with_tracing.json b/tests_integ/resources/expected/single/basic_function_with_tracing.json new file mode 100644 index 000000000..e47b43ddd --- /dev/null +++ b/tests_integ/resources/expected/single/basic_function_with_tracing.json @@ -0,0 +1,6 @@ +[ + { "LogicalResourceId":"ActiveTracingFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"ActiveTracingFunctionRole", "ResourceType":"AWS::IAM::Role"}, + { "LogicalResourceId":"PassThroughTracingFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"PassThroughTracingFunctionRole", "ResourceType":"AWS::IAM::Role" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_http_api.json b/tests_integ/resources/expected/single/basic_http_api.json new file mode 100644 index 000000000..7fac895cb --- /dev/null +++ b/tests_integ/resources/expected/single/basic_http_api.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGatewayV2::Api"}, + { "LogicalResourceId":"MyApiApiGatewayDefaultStage", "ResourceType":"AWS::ApiGatewayV2::Stage" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_layer.json b/tests_integ/resources/expected/single/basic_layer.json new file mode 100644 index 000000000..123db2316 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_layer.json @@ -0,0 +1,3 @@ +[ + { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"}, +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_layer_with_parameters.json b/tests_integ/resources/expected/single/basic_layer_with_parameters.json new file mode 100644 index 000000000..123db2316 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_layer_with_parameters.json @@ -0,0 +1,3 @@ +[ + { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"}, +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_state_machine_inline_definition.json b/tests_integ/resources/expected/single/basic_state_machine_inline_definition.json new file mode 100644 index 000000000..4d778a7e4 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_state_machine_inline_definition.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyBasicStateMachine", "ResourceType":"AWS::StepFunctions::StateMachine"}, + { "LogicalResourceId":"MyBasicStateMachineRole", "ResourceType":"AWS::IAM::Role" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_state_machine_with_tags.json b/tests_integ/resources/expected/single/basic_state_machine_with_tags.json new file mode 100644 index 000000000..e69de29bb From 6a9fd897617963fcbcc7767baaa4285d20a697d6 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Thu, 12 Nov 2020 15:38:37 -0800 Subject: [PATCH 034/105] remove unnecessary print statement --- tests_integ/helpers/helpers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 24183d9fb..2d6663c9b 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -45,15 +45,10 @@ def verify_stack_resources(expected_file_path, stack_resources): exp = expected_resources[i] parsed = parsed_resources[i] if parsed["ResourceStatus"] != "CREATE_COMPLETE": - print("---------not complete") return False if not re.fullmatch(exp["LogicalResourceId"]+"([0-9a-f]{10})?", parsed["LogicalResourceId"]): - print("---------id not match") - print(exp["LogicalResourceId"]) - print(parsed["LogicalResourceId"]) return False if exp["ResourceType"] != parsed["ResourceType"]: - print("---------resource not match") return False return True From dd9bec1b48439a6fc699d5aedbf8ff0bd3641075 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Thu, 12 Nov 2020 15:41:06 -0800 Subject: [PATCH 035/105] update basic function test --- tests_integ/single/test_basic_function.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index b8e90837e..6b42a9f6d 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -22,11 +22,12 @@ def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") - @parameterized.expand([("basic_function.yaml", "cfn_basic_function.yaml", "basic_function.json")]) - @pytest.mark.flaky(reruns=1) # why we need this one? - def test_basic_function(self, input_file_name, out_put_file_name, expected_name): + @parameterized.expand([("basic_function.yaml", "basic_function.json"), + ("basic_function_event_destinations.yaml", "basic_function_event_destinations.json") + ]) + def test_basic_function(self, input_file_name, expected_name): input_file_path = str(Path(self.template_dir, input_file_name)) - self.output_file_path = str(Path(self.output_dir, out_put_file_name)) + self.output_file_path = str(Path(self.output_dir, 'cfn_' + input_file_name)) expected_resource_path = str(Path(self.expected_dir, expected_name)) self.stack_name = STACK_NAME_PREFIX + input_file_name.split('.')[0].replace('_', '-') @@ -59,4 +60,5 @@ def test_basic_function(self, input_file_name, out_put_file_name, expected_name) # delete stack and delete translated cfn template def tearDown(self): self.cloudformation_client.delete_stack(StackName=self.stack_name) - os.remove(self.output_file_path) + if os.path.exists(self.output_file_path): + os.remove(self.output_file_path) From 8c900d2373f46193657ddba6f25699f8b50be4b4 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Thu, 12 Nov 2020 16:39:59 -0800 Subject: [PATCH 036/105] Added missing expected files, updated runner --- .../resources/expected/single/basic_api.json | 5 + .../single/basic_api_inline_swagger.json | 5 + .../single/basic_api_inline_with_cache.json | 5 + .../single/basic_api_inline_with_tags.json | 5 + .../basic_application_sar_location.json | 3 + ...lication_sar_location_with_intrinsics.json | 4 + .../expected/single/basic_function.json | 2 +- .../single/test_basic_api_inline_openapi.py | 112 +++++++----------- 8 files changed, 72 insertions(+), 69 deletions(-) create mode 100644 tests_integ/resources/expected/single/basic_api.json create mode 100644 tests_integ/resources/expected/single/basic_api_inline_swagger.json create mode 100644 tests_integ/resources/expected/single/basic_api_inline_with_cache.json create mode 100644 tests_integ/resources/expected/single/basic_api_inline_with_tags.json create mode 100644 tests_integ/resources/expected/single/basic_application_sar_location.json create mode 100644 tests_integ/resources/expected/single/basic_application_sar_location_with_intrinsics.json diff --git a/tests_integ/resources/expected/single/basic_api.json b/tests_integ/resources/expected/single/basic_api.json new file mode 100644 index 000000000..d45cb8ce3 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_api.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/tests_integ/resources/expected/single/basic_api_inline_swagger.json b/tests_integ/resources/expected/single/basic_api_inline_swagger.json new file mode 100644 index 000000000..d45cb8ce3 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_api_inline_swagger.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/tests_integ/resources/expected/single/basic_api_inline_with_cache.json b/tests_integ/resources/expected/single/basic_api_inline_with_cache.json new file mode 100644 index 000000000..d45cb8ce3 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_api_inline_with_cache.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/tests_integ/resources/expected/single/basic_api_inline_with_tags.json b/tests_integ/resources/expected/single/basic_api_inline_with_tags.json new file mode 100644 index 000000000..84d8e643f --- /dev/null +++ b/tests_integ/resources/expected/single/basic_api_inline_with_tags.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/tests_integ/resources/expected/single/basic_application_sar_location.json b/tests_integ/resources/expected/single/basic_application_sar_location.json new file mode 100644 index 000000000..a033c90b7 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_application_sar_location.json @@ -0,0 +1,3 @@ +[ + { "LogicalResourceId":"MyNestedApp", "ResourceType":"AWS::CloudFormation::Stack" } +] diff --git a/tests_integ/resources/expected/single/basic_application_sar_location_with_intrinsics.json b/tests_integ/resources/expected/single/basic_application_sar_location_with_intrinsics.json new file mode 100644 index 000000000..884f93d12 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_application_sar_location_with_intrinsics.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyNestedApp", "ResourceType":"AWS::CloudFormation::Stack" }, + { "LogicalResourceId":"MySns", "ResourceType":"AWS::SNS::Topic" } +] diff --git a/tests_integ/resources/expected/single/basic_function.json b/tests_integ/resources/expected/single/basic_function.json index 4e83eaf6b..2cb129f54 100644 --- a/tests_integ/resources/expected/single/basic_function.json +++ b/tests_integ/resources/expected/single/basic_function.json @@ -1,4 +1,4 @@ [ - { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function" }, { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } ] diff --git a/tests_integ/single/test_basic_api_inline_openapi.py b/tests_integ/single/test_basic_api_inline_openapi.py index 958a759ae..82e7b1a9e 100644 --- a/tests_integ/single/test_basic_api_inline_openapi.py +++ b/tests_integ/single/test_basic_api_inline_openapi.py @@ -1,98 +1,74 @@ -import json -import logging import os -import sys -from functools import reduce +from pathlib import Path from unittest.case import TestCase import boto3 +import pytest +from parameterized import parameterized from samcli.lib.deploy.deployer import Deployer -from samtranslator.model.exceptions import InvalidDocumentException -from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader -from samtranslator.translator.transform import transform -from samtranslator.yaml_helper import yaml_parse -from utils import stack +from tests_integ.helpers.helpers import transform_template, verify_stack_resources -LOG = logging.getLogger(__name__) -iam_client = boto3.client("iam") -my_path = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, my_path + "/..") +STACK_NAME_PREFIX = "sam-integ-test-" -STACK_NAME = "foss-integ-test" -INPUT_TEMPLATE = "basic_api_inline_openapi.yaml" -TRANSLATED_CFN_TEMPLATE = "cfn_basic_api_inline_openapi.yaml" -EXPECTED_JSON_FILE = "basic_api_inline_openapi.json" - -# can we import this from sam-translate.py? -def transform_template(input_file_path, output_file_path): - with open(input_file_path, "r") as f: - sam_template = yaml_parse(f) - - try: - cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) - cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) - - with open(output_file_path, "w") as f: - f.write(cloud_formation_template_prettified) - - print("Wrote transformed CloudFormation template to: " + output_file_path) - except InvalidDocumentException as e: - errorMessage = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) - LOG.error(errorMessage) - errors = map(lambda cause: cause.message, e.causes) - LOG.error(errors) - -class TestBasicFunction(TestCase): +class TestBasicApiInlineOpenapi(TestCase): # set up before every tests run: - # upload test code to s3 - # replace the template's codeuri with s3 location - - # move client set up to here and create template folder to store translated cfn template, or upload template to s3? - # def setUp(self): - # pass - - def test_basic_function(self): - # make stack - # transform sam template to cfn template - cwd = os.getcwd() - input_file_path = os.path.join(cwd, 'tests_integ', 'resources', 'templates', 'single', INPUT_TEMPLATE) - output_file_path = os.path.join(cwd, TRANSLATED_CFN_TEMPLATE) - expected_resource_path = os.path.join(cwd, 'tests_integ', 'resources', 'expected', 'single', EXPECTED_JSON_FILE) - transform_template(input_file_path, output_file_path) + def setUp(self): + tests_integ_dir = Path(__file__).resolve().parents[1] + self.template_dir = Path(tests_integ_dir, 'resources', 'templates', 'single') + self.output_dir = tests_integ_dir + self.expected_dir = Path(tests_integ_dir, 'resources', 'expected', 'single') + + self.cloudformation_client = boto3.client("cloudformation") + self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") + + @parameterized.expand([ + ("basic_api_inline_openapi.yaml", "cfn_basic_api_inline_openapi.yaml", "basic_api_inline_openapi.json"), + ("basic_api_inline_swagger.yaml", "cfn_basic_api_inline_swagger.yaml", "basic_api_inline_swagger.json"), + # These three require a template replacement of ${definitionuri} + # ("basic_api_with_cache.yaml", "cfn_basic_api_with_cache.yaml", "basic_api_with_cache.json"), + # ("basic_api_with_tags.yaml", "cfn_basic_api_with_tags.yaml", "basic_api_with_tags.json"), + # ("basic_api.yaml", "cfn_basic_api.yaml", "basic_api.json"), + ("basic_application_sar_location.yaml", "cfn_basic_application_sar_location.yaml", "basic_application_sar_location.json"), + ("basic_application_sar_location_with_intrinsics.yaml", "cfn_basic_application_sar_location_with_intrinsics.yaml", "basic_application_sar_location_with_intrinsics.json"), + ]) + def test_basic_api(self, input_file_name, out_put_file_name, expected_name): + input_file_path = str(Path(self.template_dir, input_file_name)) + self.output_file_path = str(Path(self.output_dir, out_put_file_name)) + expected_resource_path = str(Path(self.expected_dir, expected_name)) + self.stack_name = STACK_NAME_PREFIX + input_file_name.split('.')[0].replace('_', '-') + + transform_template(input_file_path, self.output_file_path) # cfn part cloudformation_client = boto3.client("cloudformation") deployer = Deployer(cloudformation_client, changeset_prefix="foss-integ") # deploy to cfn - with open(output_file_path, 'r') as cfn_file: + with open(self.output_file_path, 'r') as cfn_file: result, changeset_type = deployer.create_and_wait_for_changeset( - stack_name=STACK_NAME, + stack_name=self.stack_name, cfn_template=cfn_file.read(), parameter_values=[], - capabilities=['CAPABILITY_IAM'], + capabilities=['CAPABILITY_IAM', 'CAPABILITY_AUTO_EXPAND'], role_arn=None, notification_arns=[], s3_uploader=None, tags=[], ) - deployer.execute_changeset(result["Id"], STACK_NAME) - deployer.wait_for_execute(STACK_NAME, changeset_type) + deployer.execute_changeset(result["Id"], self.stack_name) + deployer.wait_for_execute(self.stack_name, changeset_type) # verify - stacks_description = cloudformation_client.describe_stacks(StackName=STACK_NAME) - stack_resources = cloudformation_client.list_stack_resources(StackName=STACK_NAME) + stacks_description = cloudformation_client.describe_stacks(StackName=self.stack_name) + stack_resources = cloudformation_client.list_stack_resources(StackName=self.stack_name) # verify if the stack is create successfully or not self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') # verify if the stack contain the expected resources - self.assertTrue(stack.verify_stack_resources(expected_resource_path, stack_resources)) - - cloudformation_client.delete_stack(StackName=STACK_NAME) + self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) # clean up # delete stack and delete translated cfn template - # def tearDown(self): - # pass - -#test = TestBasicFunction() -#test.test_basic_function() + def tearDown(self): + self.cloudformation_client.delete_stack(StackName=self.stack_name) + if os.path.exists(self.output_file_path): + os.remove(self.output_file_path) From a5bfc512de86b3fa4a52a4d27d92b7ee25a4ed03 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Thu, 12 Nov 2020 16:52:33 -0800 Subject: [PATCH 037/105] Removed the ResourceStatus check for each resource --- tests_integ/helpers/helpers.py | 5 +---- .../resources/expected/single/basic_api_inline_openapi.json | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 2d6663c9b..91a08ec25 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -44,14 +44,11 @@ def verify_stack_resources(expected_file_path, stack_resources): for i in range(len(expected_resources)): exp = expected_resources[i] parsed = parsed_resources[i] - if parsed["ResourceStatus"] != "CREATE_COMPLETE": - return False - if not re.fullmatch(exp["LogicalResourceId"]+"([0-9a-f]{10})?", parsed["LogicalResourceId"]): + if not re.fullmatch(exp["LogicalResourceId"] + "([0-9a-f]{10})?", parsed["LogicalResourceId"]): return False if exp["ResourceType"] != parsed["ResourceType"]: return False return True - def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) diff --git a/tests_integ/resources/expected/single/basic_api_inline_openapi.json b/tests_integ/resources/expected/single/basic_api_inline_openapi.json index 218baf53c..d45cb8ce3 100644 --- a/tests_integ/resources/expected/single/basic_api_inline_openapi.json +++ b/tests_integ/resources/expected/single/basic_api_inline_openapi.json @@ -1,5 +1,5 @@ [ - { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi"}, + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } ] From bb016f5f7fa6542a1a3cfa47953b4a9b8b958dec Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Thu, 12 Nov 2020 17:22:18 -0800 Subject: [PATCH 038/105] Refactored the test to inherit a BaseTest class, moved the basic_application tests to their own file, fixed some names --- tests_integ/helpers/base_test.py | 59 +++++++++++++++ tests_integ/single/test_basic_api.py | 16 ++++ .../single/test_basic_api_inline_openapi.py | 74 ------------------- tests_integ/single/test_basic_application.py | 12 +++ tests_integ/single/test_basic_function.py | 70 +++--------------- tests_integ/single/utils/__init__.py | 0 tests_integ/single/utils/stack.py | 24 ------ 7 files changed, 96 insertions(+), 159 deletions(-) create mode 100644 tests_integ/helpers/base_test.py create mode 100644 tests_integ/single/test_basic_api.py delete mode 100644 tests_integ/single/test_basic_api_inline_openapi.py create mode 100644 tests_integ/single/test_basic_application.py delete mode 100755 tests_integ/single/utils/__init__.py delete mode 100644 tests_integ/single/utils/stack.py diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py new file mode 100644 index 000000000..e23b79705 --- /dev/null +++ b/tests_integ/helpers/base_test.py @@ -0,0 +1,59 @@ +import os +from pathlib import Path +from unittest.case import TestCase + +import boto3 +import pytest +from samcli.lib.deploy.deployer import Deployer +from tests_integ.helpers.helpers import transform_template, verify_stack_resources + +STACK_NAME_PREFIX = "sam-integ-test-" + +class BaseTest(TestCase): + output_file_path = "" + stack_name = "" + + def setUp(self): + tests_integ_dir = Path(__file__).resolve().parents[1] + self.template_dir = Path(tests_integ_dir, 'resources', 'templates', 'single') + self.output_dir = tests_integ_dir + self.expected_dir = Path(tests_integ_dir, 'resources', 'expected', 'single') + + self.cloudformation_client = boto3.client("cloudformation") + self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") + + def create_and_verify_stack(self, file_name): + input_file_path = str(Path(self.template_dir, file_name + ".yaml")) + self.output_file_path = str(Path(self.output_dir, 'cfn_' + file_name + ".yaml")) + expected_resource_path = str(Path(self.expected_dir, file_name + ".json")) + self.stack_name = STACK_NAME_PREFIX + file_name.replace('_', '-') + + transform_template(input_file_path, self.output_file_path) + + # deploy to cfn + with open(self.output_file_path, 'r') as cfn_file: + result, changeset_type = self.deployer.create_and_wait_for_changeset( + stack_name=self.stack_name, + cfn_template=cfn_file.read(), + parameter_values=[], + capabilities=['CAPABILITY_IAM', 'CAPABILITY_AUTO_EXPAND'], + role_arn=None, + notification_arns=[], + s3_uploader=None, + tags=[], + ) + self.deployer.execute_changeset(result["Id"], self.stack_name) + self.deployer.wait_for_execute(self.stack_name, changeset_type) + + # verify + stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) + stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) + # verify if the stack is create successfully or not + self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') + # verify if the stack contain the expected resources + self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) + + def tearDown(self): + self.cloudformation_client.delete_stack(StackName=self.stack_name) + if os.path.exists(self.output_file_path): + os.remove(self.output_file_path) diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py new file mode 100644 index 000000000..0a9bc0905 --- /dev/null +++ b/tests_integ/single/test_basic_api.py @@ -0,0 +1,16 @@ +import os + +from parameterized import parameterized +from tests_integ.helpers.base_test import BaseTest + +class TestBasicApi(BaseTest): + @parameterized.expand([ + ("basic_api_inline_openapi"), + ("basic_api_inline_swagger"), + # These three require a template replacement of ${definitionuri} + # ("basic_api_with_cache"), + # ("basic_api_with_tags"), + # ("basic_api.yaml"), + ]) + def test_basic_api(self, file_name): + self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_api_inline_openapi.py b/tests_integ/single/test_basic_api_inline_openapi.py deleted file mode 100644 index 82e7b1a9e..000000000 --- a/tests_integ/single/test_basic_api_inline_openapi.py +++ /dev/null @@ -1,74 +0,0 @@ -import os -from pathlib import Path -from unittest.case import TestCase - -import boto3 -import pytest -from parameterized import parameterized -from samcli.lib.deploy.deployer import Deployer -from tests_integ.helpers.helpers import transform_template, verify_stack_resources - -STACK_NAME_PREFIX = "sam-integ-test-" - -class TestBasicApiInlineOpenapi(TestCase): - # set up before every tests run: - def setUp(self): - tests_integ_dir = Path(__file__).resolve().parents[1] - self.template_dir = Path(tests_integ_dir, 'resources', 'templates', 'single') - self.output_dir = tests_integ_dir - self.expected_dir = Path(tests_integ_dir, 'resources', 'expected', 'single') - - self.cloudformation_client = boto3.client("cloudformation") - self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") - - @parameterized.expand([ - ("basic_api_inline_openapi.yaml", "cfn_basic_api_inline_openapi.yaml", "basic_api_inline_openapi.json"), - ("basic_api_inline_swagger.yaml", "cfn_basic_api_inline_swagger.yaml", "basic_api_inline_swagger.json"), - # These three require a template replacement of ${definitionuri} - # ("basic_api_with_cache.yaml", "cfn_basic_api_with_cache.yaml", "basic_api_with_cache.json"), - # ("basic_api_with_tags.yaml", "cfn_basic_api_with_tags.yaml", "basic_api_with_tags.json"), - # ("basic_api.yaml", "cfn_basic_api.yaml", "basic_api.json"), - ("basic_application_sar_location.yaml", "cfn_basic_application_sar_location.yaml", "basic_application_sar_location.json"), - ("basic_application_sar_location_with_intrinsics.yaml", "cfn_basic_application_sar_location_with_intrinsics.yaml", "basic_application_sar_location_with_intrinsics.json"), - ]) - def test_basic_api(self, input_file_name, out_put_file_name, expected_name): - input_file_path = str(Path(self.template_dir, input_file_name)) - self.output_file_path = str(Path(self.output_dir, out_put_file_name)) - expected_resource_path = str(Path(self.expected_dir, expected_name)) - self.stack_name = STACK_NAME_PREFIX + input_file_name.split('.')[0].replace('_', '-') - - transform_template(input_file_path, self.output_file_path) - - # cfn part - cloudformation_client = boto3.client("cloudformation") - deployer = Deployer(cloudformation_client, changeset_prefix="foss-integ") - - # deploy to cfn - with open(self.output_file_path, 'r') as cfn_file: - result, changeset_type = deployer.create_and_wait_for_changeset( - stack_name=self.stack_name, - cfn_template=cfn_file.read(), - parameter_values=[], - capabilities=['CAPABILITY_IAM', 'CAPABILITY_AUTO_EXPAND'], - role_arn=None, - notification_arns=[], - s3_uploader=None, - tags=[], - ) - deployer.execute_changeset(result["Id"], self.stack_name) - deployer.wait_for_execute(self.stack_name, changeset_type) - - # verify - stacks_description = cloudformation_client.describe_stacks(StackName=self.stack_name) - stack_resources = cloudformation_client.list_stack_resources(StackName=self.stack_name) - # verify if the stack is create successfully or not - self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') - # verify if the stack contain the expected resources - self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) - - # clean up - # delete stack and delete translated cfn template - def tearDown(self): - self.cloudformation_client.delete_stack(StackName=self.stack_name) - if os.path.exists(self.output_file_path): - os.remove(self.output_file_path) diff --git a/tests_integ/single/test_basic_application.py b/tests_integ/single/test_basic_application.py new file mode 100644 index 000000000..d533299d8 --- /dev/null +++ b/tests_integ/single/test_basic_application.py @@ -0,0 +1,12 @@ +import os + +from parameterized import parameterized +from tests_integ.helpers.base_test import BaseTest + +class TestBasicApplication(BaseTest): + @parameterized.expand([ + ("basic_application_sar_location"), + ("basic_application_sar_location_with_intrinsics"), + ]) + def test_basic_application(self, file_name): + self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index 6b42a9f6d..894357e67 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -1,64 +1,12 @@ import os -from pathlib import Path -from unittest.case import TestCase -import boto3 -import pytest from parameterized import parameterized -from samcli.lib.deploy.deployer import Deployer -from tests_integ.helpers.helpers import transform_template, verify_stack_resources - -STACK_NAME_PREFIX = "sam-integ-test-" - - -class TestBasicFunction(TestCase): - # set up before every tests run: - def setUp(self): - tests_integ_dir = Path(__file__).resolve().parents[1] - self.template_dir = Path(tests_integ_dir, 'resources', 'templates', 'single') - self.output_dir = tests_integ_dir - self.expected_dir = Path(tests_integ_dir, 'resources', 'expected', 'single') - - self.cloudformation_client = boto3.client("cloudformation") - self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") - - @parameterized.expand([("basic_function.yaml", "basic_function.json"), - ("basic_function_event_destinations.yaml", "basic_function_event_destinations.json") - ]) - def test_basic_function(self, input_file_name, expected_name): - input_file_path = str(Path(self.template_dir, input_file_name)) - self.output_file_path = str(Path(self.output_dir, 'cfn_' + input_file_name)) - expected_resource_path = str(Path(self.expected_dir, expected_name)) - self.stack_name = STACK_NAME_PREFIX + input_file_name.split('.')[0].replace('_', '-') - - transform_template(input_file_path, self.output_file_path) - - # deploy to cfn - with open(self.output_file_path, 'r') as cfn_file: - result, changeset_type = self.deployer.create_and_wait_for_changeset( - stack_name=self.stack_name, - cfn_template=cfn_file.read(), - parameter_values=[], - capabilities=['CAPABILITY_IAM'], - role_arn=None, - notification_arns=[], - s3_uploader=None, - tags=[], - ) - self.deployer.execute_changeset(result["Id"], self.stack_name) - self.deployer.wait_for_execute(self.stack_name, changeset_type) - - # verify - stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) - stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) - # verify if the stack is create successfully or not - self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') - # verify if the stack contain the expected resources - self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) - - # clean up - # delete stack and delete translated cfn template - def tearDown(self): - self.cloudformation_client.delete_stack(StackName=self.stack_name) - if os.path.exists(self.output_file_path): - os.remove(self.output_file_path) +from tests_integ.helpers.base_test import BaseTest + +class TestBasicFunction(BaseTest): + @parameterized.expand([ + ("basic_function"), + ("basic_function_event_destinations") + ]) + def test_basic_function(self, file_name): + self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/utils/__init__.py b/tests_integ/single/utils/__init__.py deleted file mode 100755 index e69de29bb..000000000 diff --git a/tests_integ/single/utils/stack.py b/tests_integ/single/utils/stack.py deleted file mode 100644 index e6d4ca7c5..000000000 --- a/tests_integ/single/utils/stack.py +++ /dev/null @@ -1,24 +0,0 @@ -import json -import re - -def verify_stack_resources(expected_file_path, stack_resources): - with open(expected_file_path, 'r') as expected_data: - expected_resources = _sort_resources(json.load(expected_data)) - parsed_resources = _sort_resources(stack_resources['StackResourceSummaries']) - - if len(expected_resources) != len(parsed_resources): - return False - - for i in range(len(expected_resources)): - exp = expected_resources[i] - parsed = parsed_resources[i] - if parsed["ResourceStatus"] != "CREATE_COMPLETE": - return False - if re.fullmatch(exp["LogicalResourceId"], parsed["LogicalResourceId"]) is None: - return False - if exp["ResourceType"] != parsed["ResourceType"]: - return False - return True - -def _sort_resources(resources): - return sorted(resources, key=lambda d: d["LogicalResourceId"]) From 6c85d6d992af10440effac13aa2fb7f0f789fa3f Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 13 Nov 2020 02:55:42 -0800 Subject: [PATCH 039/105] add upload zip file and resolve uri with url feature, need refactoring later --- tests_integ/helpers/base_test.py | 56 ++- tests_integ/helpers/helpers.py | 1 + tests_integ/resources/code/code.zip | Bin 0 -> 224 bytes tests_integ/resources/code/layer1.zip | Bin 0 -> 1963 bytes tests_integ/resources/code/swagger1.json | 340 ++++++++++++++++++ .../expected/single/basic_api_with_tags.json | 5 + .../expected/single/basic_layer.json | 2 +- .../single/basic_layer_with_parameters.json | 2 +- .../templates/single/basic_function.yaml | 5 +- tests_integ/single/test_basic_api.py | 12 +- tests_integ/single/test_basic_application.py | 4 +- tests_integ/single/test_basic_function.py | 19 +- 12 files changed, 419 insertions(+), 27 deletions(-) create mode 100644 tests_integ/resources/code/code.zip create mode 100644 tests_integ/resources/code/layer1.zip create mode 100644 tests_integ/resources/code/swagger1.json create mode 100644 tests_integ/resources/expected/single/basic_api_with_tags.json diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index e23b79705..81e5a591e 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -4,31 +4,56 @@ import boto3 import pytest +import yaml from samcli.lib.deploy.deployer import Deployer from tests_integ.helpers.helpers import transform_template, verify_stack_resources STACK_NAME_PREFIX = "sam-integ-test-" +S3_BUCKET = "sam-integ-bucket-test245b" # need to add suffix to allow multiple test +CODE_KEY_TO_FILE_MAP = {'codeuri': 'code.zip', 'contenturi': 'layer1.zip', 'definitionuri': "swagger1.json"} + class BaseTest(TestCase): - output_file_path = "" - stack_name = "" - def setUp(self): - tests_integ_dir = Path(__file__).resolve().parents[1] - self.template_dir = Path(tests_integ_dir, 'resources', 'templates', 'single') - self.output_dir = tests_integ_dir - self.expected_dir = Path(tests_integ_dir, 'resources', 'expected', 'single') + @classmethod + def setUpClass(cls): + BaseTest.tests_integ_dir = Path(__file__).resolve().parents[1] + BaseTest.template_dir = Path(BaseTest.tests_integ_dir, 'resources', 'templates', 'single') + BaseTest.output_dir = BaseTest.tests_integ_dir + BaseTest.expected_dir = Path(BaseTest.tests_integ_dir, 'resources', 'expected', 'single') + code_dir = Path(BaseTest.tests_integ_dir, 'resources', 'code') + BaseTest.s3_client = boto3.client("s3") + BaseTest.s3_client.create_bucket(Bucket=S3_BUCKET) + BaseTest.code_key_to_url = {} + for key, value in CODE_KEY_TO_FILE_MAP.items(): + code_path = Path(code_dir, value) + code_file = code_path.open(mode='rb') + BaseTest.s3_client.put_object(Bucket=S3_BUCKET, Body=code_file, Key=value) + code_url = f"s3://{S3_BUCKET}/{value}" + BaseTest.code_key_to_url[key] = code_url + + def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") def create_and_verify_stack(self, file_name): - input_file_path = str(Path(self.template_dir, file_name + ".yaml")) - self.output_file_path = str(Path(self.output_dir, 'cfn_' + file_name + ".yaml")) - expected_resource_path = str(Path(self.expected_dir, file_name + ".json")) + input_file_path = str(Path(BaseTest.template_dir, file_name + ".yaml")) + self.output_file_path = str(Path(BaseTest.output_dir, 'cfn_' + file_name + ".yaml")) + expected_resource_path = str(Path(BaseTest.expected_dir, file_name + ".json")) self.stack_name = STACK_NAME_PREFIX + file_name.replace('_', '-') - transform_template(input_file_path, self.output_file_path) + self.sub_input_file_path = str(Path(BaseTest.output_dir, 'sub_' + file_name + ".yaml")) + with open(input_file_path, 'r') as f: + data = f.read() + for key, value in BaseTest.code_key_to_url.items(): + data = data.replace(f"${{{key}}}", value) + yaml_doc = yaml.load(data) + + with open(self.sub_input_file_path, 'w') as f: + yaml.dump(yaml_doc, f) + + transform_template(self.sub_input_file_path, self.output_file_path) # deploy to cfn with open(self.output_file_path, 'r') as cfn_file: @@ -57,3 +82,12 @@ def tearDown(self): self.cloudformation_client.delete_stack(StackName=self.stack_name) if os.path.exists(self.output_file_path): os.remove(self.output_file_path) + if os.path.exists(self.sub_input_file_path): + os.remove(self.sub_input_file_path) + + @classmethod + def tearDownClass(cls) -> None: + response = BaseTest.s3_client.list_objects(Bucket=S3_BUCKET) + for content in response['Contents']: + BaseTest.s3_client.delete_object(Key=content['Key'], Bucket=S3_BUCKET) + BaseTest.s3_client.delete_bucket(Bucket=S3_BUCKET) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 91a08ec25..71d98a8fc 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -50,5 +50,6 @@ def verify_stack_resources(expected_file_path, stack_resources): return False return True + def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) diff --git a/tests_integ/resources/code/code.zip b/tests_integ/resources/code/code.zip new file mode 100644 index 0000000000000000000000000000000000000000..3f21de8b6cf8b2ebb0ff6893c9eca604429098b1 GIT binary patch literal 224 zcmWIWW@h1H00E{NE6!sx9 zrD`Z;q~_%0D`{GD)p7-RGcw6BZ zKwMhE&A`agrR*usa;BQO@a@cKADK{zX-l_U zI#IM-XS21k&z!k8-<^5qq{)`NC97xI)g$fKrXMq3rKYBK?8lR>p1q#RAH}{p#@Cog z`PN)AHcsbs&Di8`TK4bcoBDG##n0~475kmjVMxdi%45@DX5foauTF?AeDh;^&6&?H z8cpmQ*tl90`MeWy|LgeAv#I>_B&pEX}0z(GuUUY-_@t{E_vQ= zP5wl=1i6%budFuxt~paYv-k|>XNJ!TpF^+TT+*8Jxp7TOinWN!8Rj$1k)}5%FR%9c zc5knQPk^G^_qVgZzvoV9@C!D4Wu)>pLM8W@@+`rHm3eNDW@SbB`o=HTS2xeUp4a!> z(r&)J^`?X+?m8#EX6+41y5%=_;+j?tYktYLZH0QS$3j$P)sEJ-<$11Jr!wcvsx=p) z(~gD4nqAuK7Bv0rOJBz23k)>lUpw9YuB1rpMy(SO1x`hLWOD;>K|<{(OkCX zy}qt;($0#6YR-D~fK6T*}EZ5<5@*LCPRX}hP?g-llJdKQ-@Ze?k7 zq<{IHdnNj1oX2-Mt$D1z$W6!XcCwW!d$mj9HNBrxQlbtQx-Q?k%duO1Y3gtGjJ0ZS zBl>l2mp)q6WU6QF#~;Ev`;LgexS^obi%DNO&IG9BD8#i)e~`L^DgQue1-o6t`3G!w zTFyTdt6{f4sQ*Fw4`2O(`5(-SBAS*)v?+xpF`Fd`mpw9kb13i4QAL~9{znFFA9T(= zlJThFpe=z-HWesEe!PPt3@*hS2Fx%HI|H%D^VO>-FgXur^MmxqoleMV* zt=(Xs(rG-Kt!DG>hi~L7ejJHC^z-)KZvx_Ccf(_X0z-rEU%Gd7v*DR&w{qh&zHmJY zz5vODIS=>thRiP8_^0lmy;pv)*!E5HD%nru%{Iu3UGn7z^FOb?O)e3eZq;`zJ|H~* z^BVT-zf30==6v?sv@r0)WScI-=PLbMxR_2ZowNI5pP$rS*UqxVcaLX^yfL3xE~gvv z`{k1vwsWs1@ZM|p*POonf%Xr^Acy^;5}!3-!N3M}l3Gjj`aQsWCM zOEU8F;&by;N^?^6!3Ce}+WXPxff%je0~rS?J3+=p`n|PQ1sbOg#HzTAL$O1zpb~C8 z$O<&$y*+({HO`;#IoEOCbB&Mgr3+gg1u--&x-#j~A{$m!`3Vaa3W+lyytE-i1I0@U zxZDsQpP83g5+4t<{x-*yXs?P3=+*~#Gcw6B{85_fEdjPbmNjn8&(`fqu2)y7<^Vie1RF@FkgVI fI)KY6l;8*20t$YNuwrEc1u6>=Rs&rw1?B+&#xxB~ literal 0 HcmV?d00001 diff --git a/tests_integ/resources/code/swagger1.json b/tests_integ/resources/code/swagger1.json new file mode 100644 index 000000000..81d235a10 --- /dev/null +++ b/tests_integ/resources/code/swagger1.json @@ -0,0 +1,340 @@ +{ + "swagger": "2.0", + "info": { + "title": "PetStore", + "description": "Your first API with Amazon API Gateway. This is a sample API that integrates via HTTP with our demo Pet Store endpoints" + }, + "schemes": [ + "https" + ], + "paths": { + "/": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "text/html" + ], + "responses": { + "200": { + "description": "200 response", + "headers": { + "Content-Type": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Content-Type": "'text/html'" + }, + "responseTemplates": { + "text/html": "\n \n \n \n \n

Welcome to your Pet Store API

\n

\n You have succesfully deployed your first API. You are seeing this HTML page because the GET method to the root resource of your API returns this content as a Mock integration.\n

\n

\n The Pet Store API contains the /pets and /pets/{petId} resources. By making a GET request to /pets you can retrieve a list of Pets in your API. If you are looking for a specific pet, for example the pet with ID 1, you can make a GET request to /pets/1.\n

\n

\n You can use a REST client such as Postman to test the POST methods in your API to create a new pet. Use the sample body below to send the POST request:\n

\n
\n{\n    \"type\" : \"cat\",\n    \"price\" : 123.11\n}\n        
\n \n" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + }, + "post": { + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod": "POST", + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'POST,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + }, + "/pets": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "type", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "page", + "in": "query", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod": "GET", + "requestParameters": { + "integration.request.querystring.page": "method.request.querystring.page", + "integration.request.querystring.type": "method.request.querystring.type" + }, + "type": "http" + } + }, + "post": { + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod": "POST", + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'POST,GET,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + }, + "/pets/{petId}": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets/{petId}", + "httpMethod": "GET", + "requestParameters": { + "integration.request.path.petId": "method.request.path.petId" + }, + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + } + }, + "definitions": { + "Empty": { + "type": "object" + } + } +} diff --git a/tests_integ/resources/expected/single/basic_api_with_tags.json b/tests_integ/resources/expected/single/basic_api_with_tags.json new file mode 100644 index 000000000..d636093ca --- /dev/null +++ b/tests_integ/resources/expected/single/basic_api_with_tags.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiStage", "ResourceType":"AWS::ApiGateway::Stage" } +] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_layer.json b/tests_integ/resources/expected/single/basic_layer.json index 123db2316..beba3153c 100644 --- a/tests_integ/resources/expected/single/basic_layer.json +++ b/tests_integ/resources/expected/single/basic_layer.json @@ -1,3 +1,3 @@ [ - { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"}, + { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"} ] \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_layer_with_parameters.json b/tests_integ/resources/expected/single/basic_layer_with_parameters.json index 123db2316..beba3153c 100644 --- a/tests_integ/resources/expected/single/basic_layer_with_parameters.json +++ b/tests_integ/resources/expected/single/basic_layer_with_parameters.json @@ -1,3 +1,3 @@ [ - { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"}, + { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"} ] \ No newline at end of file diff --git a/tests_integ/resources/templates/single/basic_function.yaml b/tests_integ/resources/templates/single/basic_function.yaml index 3ec291f00..d3fdf3078 100644 --- a/tests_integ/resources/templates/single/basic_function.yaml +++ b/tests_integ/resources/templates/single/basic_function.yaml @@ -4,10 +4,7 @@ Resources: Properties: Handler: index.handler Runtime: nodejs12.x - InlineCode: | - exports.handler = function(event) { - console.log("Hello World"); - } + CodeUri: ${codeuri} MemorySize: 128 Policies: - AWSLambdaRole diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py index 0a9bc0905..23c4c0240 100644 --- a/tests_integ/single/test_basic_api.py +++ b/tests_integ/single/test_basic_api.py @@ -3,14 +3,14 @@ from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest + class TestBasicApi(BaseTest): @parameterized.expand([ - ("basic_api_inline_openapi"), - ("basic_api_inline_swagger"), - # These three require a template replacement of ${definitionuri} - # ("basic_api_with_cache"), - # ("basic_api_with_tags"), - # ("basic_api.yaml"), + "basic_api_inline_openapi", + "basic_api_inline_swagger", + # "basic_api_with_cache", # no test case in java code base + "basic_api_with_tags", + "basic_api", ]) def test_basic_api(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_application.py b/tests_integ/single/test_basic_application.py index d533299d8..0519e6f05 100644 --- a/tests_integ/single/test_basic_application.py +++ b/tests_integ/single/test_basic_application.py @@ -5,8 +5,8 @@ class TestBasicApplication(BaseTest): @parameterized.expand([ - ("basic_application_sar_location"), - ("basic_application_sar_location_with_intrinsics"), + "basic_application_sar_location", + "basic_application_sar_location_with_intrinsics", ]) def test_basic_application(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index 894357e67..49931c557 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -3,10 +3,25 @@ from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest + class TestBasicFunction(BaseTest): @parameterized.expand([ - ("basic_function"), - ("basic_function_event_destinations") + "basic_function", + "basic_function_event_destinations", + "basic_function_no_envvar", + "basic_function_openapi", + "basic_function_with_kmskeyarn", + "basic_function_with_sns_dlq", + "basic_function_with_sqs_dlq", + "basic_function_with_tags", + # ("basic_function_with_tracing"), # need different set up to create changeset + "basic_http_api", + "basic_layer", + "basic_layer_with_parameters", + "basic_state_machine_inline_definition", + # ("basic_state_machine_with_tags"), # cannot be translated by sam-tran + # ("basic_table_no_param"), # no test case in java code base + # ("basic_table_with_param") # no test case in java code base ]) def test_basic_function(self, file_name): self.create_and_verify_stack(file_name) From 896ef40063240a446b3b31ceb03e7bcb6c21afa9 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 13 Nov 2020 15:46:18 -0800 Subject: [PATCH 040/105] add extra test class and update yaml load call --- tests_integ/helpers/base_test.py | 2 +- tests_integ/single/test_basic_function.py | 5 ----- tests_integ/single/test_basic_http_api.py | 11 +++++++++++ tests_integ/single/test_basic_layer_version.py | 12 ++++++++++++ tests_integ/single/test_basic_state_machine.py | 12 ++++++++++++ 5 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 tests_integ/single/test_basic_http_api.py create mode 100644 tests_integ/single/test_basic_layer_version.py create mode 100644 tests_integ/single/test_basic_state_machine.py diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 81e5a591e..80006c2c0 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -48,7 +48,7 @@ def create_and_verify_stack(self, file_name): data = f.read() for key, value in BaseTest.code_key_to_url.items(): data = data.replace(f"${{{key}}}", value) - yaml_doc = yaml.load(data) + yaml_doc = yaml.load(data, Loader=yaml.FullLoader) with open(self.sub_input_file_path, 'w') as f: yaml.dump(yaml_doc, f) diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index 49931c557..f88446aa7 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -15,11 +15,6 @@ class TestBasicFunction(BaseTest): "basic_function_with_sqs_dlq", "basic_function_with_tags", # ("basic_function_with_tracing"), # need different set up to create changeset - "basic_http_api", - "basic_layer", - "basic_layer_with_parameters", - "basic_state_machine_inline_definition", - # ("basic_state_machine_with_tags"), # cannot be translated by sam-tran # ("basic_table_no_param"), # no test case in java code base # ("basic_table_with_param") # no test case in java code base ]) diff --git a/tests_integ/single/test_basic_http_api.py b/tests_integ/single/test_basic_http_api.py new file mode 100644 index 000000000..0da0c8a12 --- /dev/null +++ b/tests_integ/single/test_basic_http_api.py @@ -0,0 +1,11 @@ +from parameterized import parameterized + +from tests_integ.helpers.base_test import BaseTest + + +class TestBasicHttpApi(BaseTest): + @parameterized.expand([ + "basic_http_api", + ]) + def test_basic_http_api(self, file_name): + self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_layer_version.py b/tests_integ/single/test_basic_layer_version.py new file mode 100644 index 000000000..b6fe20ce6 --- /dev/null +++ b/tests_integ/single/test_basic_layer_version.py @@ -0,0 +1,12 @@ +from parameterized import parameterized + +from tests_integ.helpers.base_test import BaseTest + + +class TestBasicLayerVersion(BaseTest): + @parameterized.expand([ + "basic_layer", + "basic_layer_with_parameters", + ]) + def test_basic_layer_version(self, file_name): + self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_state_machine.py b/tests_integ/single/test_basic_state_machine.py new file mode 100644 index 000000000..ffce10cae --- /dev/null +++ b/tests_integ/single/test_basic_state_machine.py @@ -0,0 +1,12 @@ +from parameterized import parameterized + +from tests_integ.helpers.base_test import BaseTest + + +class TestBasicLayerVersion(BaseTest): + @parameterized.expand([ + "basic_state_machine_inline_definition", + # ("basic_state_machine_with_tags"), # cannot be translated by sam-tran + ]) + def test_basic_state_machine(self, file_name): + self.create_and_verify_stack(file_name) From b45a890f9bfa2bc1f52f4f5bd385d57a83050f60 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 13 Nov 2020 16:58:06 -0800 Subject: [PATCH 041/105] fix bug, now allow create bucket in multi regions --- tests_integ/helpers/base_test.py | 31 ++++++++++++++++++------------- tests_integ/helpers/helpers.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 80006c2c0..02465724c 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -6,10 +6,10 @@ import pytest import yaml from samcli.lib.deploy.deployer import Deployer -from tests_integ.helpers.helpers import transform_template, verify_stack_resources +from tests_integ.helpers.helpers import transform_template, verify_stack_resources, create_bucket STACK_NAME_PREFIX = "sam-integ-test-" -S3_BUCKET = "sam-integ-bucket-test245b" # need to add suffix to allow multiple test +S3_BUCKET = "sam-integ-bucket-test245b3x" # need to add random suffix to allow run multiple test set in the sam account CODE_KEY_TO_FILE_MAP = {'codeuri': 'code.zip', 'contenturi': 'layer1.zip', 'definitionuri': "swagger1.json"} @@ -23,14 +23,18 @@ def setUpClass(cls): BaseTest.expected_dir = Path(BaseTest.tests_integ_dir, 'resources', 'expected', 'single') code_dir = Path(BaseTest.tests_integ_dir, 'resources', 'code') - BaseTest.s3_client = boto3.client("s3") - BaseTest.s3_client.create_bucket(Bucket=S3_BUCKET) + session = boto3.session.Session() + my_region = session.region_name + create_bucket(S3_BUCKET, region=my_region) + + s3_client = boto3.client("s3") BaseTest.code_key_to_url = {} - for key, value in CODE_KEY_TO_FILE_MAP.items(): - code_path = Path(code_dir, value) + + for key, file_name in CODE_KEY_TO_FILE_MAP.items(): + code_path = Path(code_dir, file_name) code_file = code_path.open(mode='rb') - BaseTest.s3_client.put_object(Bucket=S3_BUCKET, Body=code_file, Key=value) - code_url = f"s3://{S3_BUCKET}/{value}" + s3_client.put_object(Bucket=S3_BUCKET, Body=code_file, Key=file_name) + code_url = f"s3://{S3_BUCKET}/{file_name}" BaseTest.code_key_to_url[key] = code_url def setUp(self): @@ -46,8 +50,8 @@ def create_and_verify_stack(self, file_name): self.sub_input_file_path = str(Path(BaseTest.output_dir, 'sub_' + file_name + ".yaml")) with open(input_file_path, 'r') as f: data = f.read() - for key, value in BaseTest.code_key_to_url.items(): - data = data.replace(f"${{{key}}}", value) + for key, s3_url in BaseTest.code_key_to_url.items(): + data = data.replace(f"${{{key}}}", s3_url) yaml_doc = yaml.load(data, Loader=yaml.FullLoader) with open(self.sub_input_file_path, 'w') as f: @@ -87,7 +91,8 @@ def tearDown(self): @classmethod def tearDownClass(cls) -> None: - response = BaseTest.s3_client.list_objects(Bucket=S3_BUCKET) + s3_client = boto3.client("s3") + response = s3_client.list_objects(Bucket=S3_BUCKET) for content in response['Contents']: - BaseTest.s3_client.delete_object(Key=content['Key'], Bucket=S3_BUCKET) - BaseTest.s3_client.delete_bucket(Bucket=S3_BUCKET) + s3_client.delete_object(Key=content['Key'], Bucket=S3_BUCKET) + s3_client.delete_bucket(Bucket=S3_BUCKET) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 71d98a8fc..53e9e4a04 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -4,6 +4,7 @@ from functools import reduce import boto3 +from botocore.exceptions import ClientError from samtranslator.model.exceptions import InvalidDocumentException from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader @@ -53,3 +54,32 @@ def verify_stack_resources(expected_file_path, stack_resources): def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) + + +def create_bucket(bucket_name, region=None): + """Create an S3 bucket in a specified region + + copy code from boto3 doc example + + If a region is not specified, the bucket is created in the S3 default + region (us-east-1). + + :param bucket_name: Bucket to create + :param region: String region to create bucket in, e.g., 'us-west-2' + :return: True if bucket created, else False + """ + + # Create bucket + try: + if region is None: + s3_client = boto3.client('s3') + s3_client.create_bucket(Bucket=bucket_name) + else: + s3_client = boto3.client('s3', region_name=region) + location = {'LocationConstraint': region} + s3_client.create_bucket(Bucket=bucket_name, + CreateBucketConfiguration=location) + except ClientError as e: + logging.error(e) + return False + return True From ae775b7ff826ae78c335d11611e4effd366fcde8 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Fri, 13 Nov 2020 17:03:55 -0800 Subject: [PATCH 042/105] Added random suffix to stack and bucket names to avoid name collisions --- tests_integ/helpers/base_test.py | 24 ++++++++++++------------ tests_integ/helpers/helpers.py | 27 ++++++++++++++------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 02465724c..66b9a18f1 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -6,10 +6,10 @@ import pytest import yaml from samcli.lib.deploy.deployer import Deployer -from tests_integ.helpers.helpers import transform_template, verify_stack_resources, create_bucket +from tests_integ.helpers.helpers import transform_template, verify_stack_resources, generate_suffix, create_bucket -STACK_NAME_PREFIX = "sam-integ-test-" -S3_BUCKET = "sam-integ-bucket-test245b3x" # need to add random suffix to allow run multiple test set in the sam account +STACK_NAME_PREFIX = "sam-integ-stack-" +S3_BUCKET_PREFIX = "sam-integ-bucket-" CODE_KEY_TO_FILE_MAP = {'codeuri': 'code.zip', 'contenturi': 'layer1.zip', 'definitionuri': "swagger1.json"} @@ -23,18 +23,18 @@ def setUpClass(cls): BaseTest.expected_dir = Path(BaseTest.tests_integ_dir, 'resources', 'expected', 'single') code_dir = Path(BaseTest.tests_integ_dir, 'resources', 'code') + BaseTest.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() session = boto3.session.Session() my_region = session.region_name - create_bucket(S3_BUCKET, region=my_region) + create_bucket(BaseTest.s3_bucket_name, region=my_region) s3_client = boto3.client("s3") BaseTest.code_key_to_url = {} for key, file_name in CODE_KEY_TO_FILE_MAP.items(): - code_path = Path(code_dir, file_name) - code_file = code_path.open(mode='rb') - s3_client.put_object(Bucket=S3_BUCKET, Body=code_file, Key=file_name) - code_url = f"s3://{S3_BUCKET}/{file_name}" + code_path = str(Path(code_dir, file_name)) + s3_client.upload_file(code_path, BaseTest.s3_bucket_name, file_name) + code_url = f"s3://{BaseTest.s3_bucket_name}/{file_name}" BaseTest.code_key_to_url[key] = code_url def setUp(self): @@ -45,7 +45,7 @@ def create_and_verify_stack(self, file_name): input_file_path = str(Path(BaseTest.template_dir, file_name + ".yaml")) self.output_file_path = str(Path(BaseTest.output_dir, 'cfn_' + file_name + ".yaml")) expected_resource_path = str(Path(BaseTest.expected_dir, file_name + ".json")) - self.stack_name = STACK_NAME_PREFIX + file_name.replace('_', '-') + self.stack_name = STACK_NAME_PREFIX + file_name.replace('_', '-') + generate_suffix() self.sub_input_file_path = str(Path(BaseTest.output_dir, 'sub_' + file_name + ".yaml")) with open(input_file_path, 'r') as f: @@ -92,7 +92,7 @@ def tearDown(self): @classmethod def tearDownClass(cls) -> None: s3_client = boto3.client("s3") - response = s3_client.list_objects(Bucket=S3_BUCKET) + response = s3_client.list_objects(Bucket=BaseTest.s3_bucket_name) for content in response['Contents']: - s3_client.delete_object(Key=content['Key'], Bucket=S3_BUCKET) - s3_client.delete_bucket(Bucket=S3_BUCKET) + s3_client.delete_object(Key=content['Key'], Bucket=BaseTest.s3_bucket_name) + s3_client.delete_bucket(Bucket=BaseTest.s3_bucket_name) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 53e9e4a04..5598be497 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -1,6 +1,8 @@ import json import logging import re +import random +import string # not deprecated, a bug from pylint https://www.logilab.org/ticket/2481 from functools import reduce import boto3 @@ -11,6 +13,7 @@ from samtranslator.translator.transform import transform from samtranslator.yaml_helper import yaml_parse +RANDOM_SUFFIX_LENGTH = 12 def transform_template(input_file_path, output_file_path): LOG = logging.getLogger(__name__) @@ -51,6 +54,9 @@ def verify_stack_resources(expected_file_path, stack_resources): return False return True +def generate_suffix(): + # Very basic random letters generator + return ''.join(random.choice(string.ascii_lowercase) for i in range(RANDOM_SUFFIX_LENGTH)) def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) @@ -60,6 +66,7 @@ def create_bucket(bucket_name, region=None): """Create an S3 bucket in a specified region copy code from boto3 doc example + MG: removed the try so that the exception bubbles up and interrupts the test If a region is not specified, the bucket is created in the S3 default region (us-east-1). @@ -70,16 +77,10 @@ def create_bucket(bucket_name, region=None): """ # Create bucket - try: - if region is None: - s3_client = boto3.client('s3') - s3_client.create_bucket(Bucket=bucket_name) - else: - s3_client = boto3.client('s3', region_name=region) - location = {'LocationConstraint': region} - s3_client.create_bucket(Bucket=bucket_name, - CreateBucketConfiguration=location) - except ClientError as e: - logging.error(e) - return False - return True + if region is None: + s3_client = boto3.client('s3') + s3_client.create_bucket(Bucket=bucket_name) + else: + s3_client = boto3.client('s3', region_name=region) + location = {'LocationConstraint': region} + s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location) From b61a189553502df31212dadf3d098cd506c2b7c5 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Sun, 15 Nov 2020 23:53:08 -0800 Subject: [PATCH 043/105] refactor: Moved some stuff into methods in base_test, updated the Makefile black commands to include the tests_integ directories --- Makefile | 4 +- tests_integ/helpers/base_test.py | 74 ++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index e14ecc2e4..78a98c6be 100755 --- a/Makefile +++ b/Makefile @@ -12,10 +12,10 @@ test-integ: pytest --no-cov tests_integ/* black: - black setup.py samtranslator/* tests/* bin/*.py + black setup.py samtranslator/* tests/* tests_integ/* bin/*.py black-check: - black --check setup.py samtranslator/* tests/* bin/*.py + black --check setup.py samtranslator/* tests/* tests_integ/* bin/*.py # Command to run everytime you make changes to verify everything works dev: test diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 66b9a18f1..f44d7e40b 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -18,10 +18,11 @@ class BaseTest(TestCase): @classmethod def setUpClass(cls): BaseTest.tests_integ_dir = Path(__file__).resolve().parents[1] - BaseTest.template_dir = Path(BaseTest.tests_integ_dir, 'resources', 'templates', 'single') + BaseTest.resources_dir = Path(BaseTest.tests_integ_dir, 'resources') + BaseTest.template_dir = Path(BaseTest.resources_dir, 'templates', 'single') BaseTest.output_dir = BaseTest.tests_integ_dir - BaseTest.expected_dir = Path(BaseTest.tests_integ_dir, 'resources', 'expected', 'single') - code_dir = Path(BaseTest.tests_integ_dir, 'resources', 'code') + BaseTest.expected_dir = Path(BaseTest.resources_dir, 'expected', 'single') + code_dir = Path(BaseTest.resources_dir, 'code') BaseTest.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() session = boto3.session.Session() @@ -37,29 +38,60 @@ def setUpClass(cls): code_url = f"s3://{BaseTest.s3_bucket_name}/{file_name}" BaseTest.code_key_to_url[key] = code_url + @classmethod + def tearDownClass(cls) -> None: + BaseTest._clean_bucket() + + + @classmethod + def _clean_bucket(cls): + s3_client = boto3.client("s3") + response = s3_client.list_objects(Bucket=BaseTest.s3_bucket_name) + for content in response['Contents']: + s3_client.delete_object(Key=content['Key'], Bucket=BaseTest.s3_bucket_name) + s3_client.delete_bucket(Bucket=BaseTest.s3_bucket_name) + + def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") + def create_and_verify_stack(self, file_name): input_file_path = str(Path(BaseTest.template_dir, file_name + ".yaml")) self.output_file_path = str(Path(BaseTest.output_dir, 'cfn_' + file_name + ".yaml")) expected_resource_path = str(Path(BaseTest.expected_dir, file_name + ".json")) self.stack_name = STACK_NAME_PREFIX + file_name.replace('_', '-') + generate_suffix() - self.sub_input_file_path = str(Path(BaseTest.output_dir, 'sub_' + file_name + ".yaml")) + self.sub_input_file_path = self._update_template(input_file_path, file_name) + transform_template(self.sub_input_file_path, self.output_file_path) + self._deploy_stack() + self._verify_stack(expected_resource_path) + + + def tearDown(self): + self.cloudformation_client.delete_stack(StackName=self.stack_name) + if os.path.exists(self.output_file_path): + os.remove(self.output_file_path) + if os.path.exists(self.sub_input_file_path): + os.remove(self.sub_input_file_path) + + + def _update_template(self, input_file_path, file_name): + updated_template_path = str(Path(BaseTest.output_dir, 'sub_' + file_name + ".yaml")) with open(input_file_path, 'r') as f: data = f.read() for key, s3_url in BaseTest.code_key_to_url.items(): data = data.replace(f"${{{key}}}", s3_url) yaml_doc = yaml.load(data, Loader=yaml.FullLoader) - with open(self.sub_input_file_path, 'w') as f: + with open(updated_template_path, 'w') as f: yaml.dump(yaml_doc, f) + + return updated_template_path - transform_template(self.sub_input_file_path, self.output_file_path) - # deploy to cfn + def _deploy_stack(self): with open(self.output_file_path, 'r') as cfn_file: result, changeset_type = self.deployer.create_and_wait_for_changeset( stack_name=self.stack_name, @@ -74,25 +106,11 @@ def create_and_verify_stack(self, file_name): self.deployer.execute_changeset(result["Id"], self.stack_name) self.deployer.wait_for_execute(self.stack_name, changeset_type) - # verify - stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) - stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) - # verify if the stack is create successfully or not - self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') - # verify if the stack contain the expected resources - self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) - def tearDown(self): - self.cloudformation_client.delete_stack(StackName=self.stack_name) - if os.path.exists(self.output_file_path): - os.remove(self.output_file_path) - if os.path.exists(self.sub_input_file_path): - os.remove(self.sub_input_file_path) - - @classmethod - def tearDownClass(cls) -> None: - s3_client = boto3.client("s3") - response = s3_client.list_objects(Bucket=BaseTest.s3_bucket_name) - for content in response['Contents']: - s3_client.delete_object(Key=content['Key'], Bucket=BaseTest.s3_bucket_name) - s3_client.delete_bucket(Bucket=BaseTest.s3_bucket_name) + def _verify_stack(self, expected_resource_path): + stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) + stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) + # verify if the stack was successfully created + self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') + # verify if the stack contains the expected resources + self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) From ff18eee1da2c4da9aa30cd537cdb10466e726bb3 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Mon, 16 Nov 2020 09:39:18 -0800 Subject: [PATCH 044/105] chore: Black formatting --- tests_integ/helpers/base_test.py | 40 ++++++++----------- tests_integ/helpers/helpers.py | 17 ++++---- tests_integ/single/test_basic_api.py | 16 ++++---- tests_integ/single/test_basic_application.py | 11 +++-- tests_integ/single/test_basic_function.py | 28 +++++++------ tests_integ/single/test_basic_http_api.py | 8 ++-- .../single/test_basic_layer_version.py | 10 +++-- .../single/test_basic_state_machine.py | 10 +++-- 8 files changed, 74 insertions(+), 66 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index f44d7e40b..dd2ed87a0 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -10,19 +10,18 @@ STACK_NAME_PREFIX = "sam-integ-stack-" S3_BUCKET_PREFIX = "sam-integ-bucket-" -CODE_KEY_TO_FILE_MAP = {'codeuri': 'code.zip', 'contenturi': 'layer1.zip', 'definitionuri': "swagger1.json"} +CODE_KEY_TO_FILE_MAP = {"codeuri": "code.zip", "contenturi": "layer1.zip", "definitionuri": "swagger1.json"} class BaseTest(TestCase): - @classmethod def setUpClass(cls): BaseTest.tests_integ_dir = Path(__file__).resolve().parents[1] - BaseTest.resources_dir = Path(BaseTest.tests_integ_dir, 'resources') - BaseTest.template_dir = Path(BaseTest.resources_dir, 'templates', 'single') + BaseTest.resources_dir = Path(BaseTest.tests_integ_dir, "resources") + BaseTest.template_dir = Path(BaseTest.resources_dir, "templates", "single") BaseTest.output_dir = BaseTest.tests_integ_dir - BaseTest.expected_dir = Path(BaseTest.resources_dir, 'expected', 'single') - code_dir = Path(BaseTest.resources_dir, 'code') + BaseTest.expected_dir = Path(BaseTest.resources_dir, "expected", "single") + code_dir = Path(BaseTest.resources_dir, "code") BaseTest.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() session = boto3.session.Session() @@ -42,33 +41,29 @@ def setUpClass(cls): def tearDownClass(cls) -> None: BaseTest._clean_bucket() - @classmethod def _clean_bucket(cls): s3_client = boto3.client("s3") response = s3_client.list_objects(Bucket=BaseTest.s3_bucket_name) - for content in response['Contents']: - s3_client.delete_object(Key=content['Key'], Bucket=BaseTest.s3_bucket_name) + for content in response["Contents"]: + s3_client.delete_object(Key=content["Key"], Bucket=BaseTest.s3_bucket_name) s3_client.delete_bucket(Bucket=BaseTest.s3_bucket_name) - def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") - def create_and_verify_stack(self, file_name): input_file_path = str(Path(BaseTest.template_dir, file_name + ".yaml")) - self.output_file_path = str(Path(BaseTest.output_dir, 'cfn_' + file_name + ".yaml")) + self.output_file_path = str(Path(BaseTest.output_dir, "cfn_" + file_name + ".yaml")) expected_resource_path = str(Path(BaseTest.expected_dir, file_name + ".json")) - self.stack_name = STACK_NAME_PREFIX + file_name.replace('_', '-') + generate_suffix() + self.stack_name = STACK_NAME_PREFIX + file_name.replace("_", "-") + generate_suffix() self.sub_input_file_path = self._update_template(input_file_path, file_name) transform_template(self.sub_input_file_path, self.output_file_path) self._deploy_stack() self._verify_stack(expected_resource_path) - def tearDown(self): self.cloudformation_client.delete_stack(StackName=self.stack_name) if os.path.exists(self.output_file_path): @@ -76,28 +71,26 @@ def tearDown(self): if os.path.exists(self.sub_input_file_path): os.remove(self.sub_input_file_path) - def _update_template(self, input_file_path, file_name): - updated_template_path = str(Path(BaseTest.output_dir, 'sub_' + file_name + ".yaml")) - with open(input_file_path, 'r') as f: + updated_template_path = str(Path(BaseTest.output_dir, "sub_" + file_name + ".yaml")) + with open(input_file_path, "r") as f: data = f.read() for key, s3_url in BaseTest.code_key_to_url.items(): data = data.replace(f"${{{key}}}", s3_url) yaml_doc = yaml.load(data, Loader=yaml.FullLoader) - with open(updated_template_path, 'w') as f: + with open(updated_template_path, "w") as f: yaml.dump(yaml_doc, f) - - return updated_template_path + return updated_template_path def _deploy_stack(self): - with open(self.output_file_path, 'r') as cfn_file: + with open(self.output_file_path, "r") as cfn_file: result, changeset_type = self.deployer.create_and_wait_for_changeset( stack_name=self.stack_name, cfn_template=cfn_file.read(), parameter_values=[], - capabilities=['CAPABILITY_IAM', 'CAPABILITY_AUTO_EXPAND'], + capabilities=["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"], role_arn=None, notification_arns=[], s3_uploader=None, @@ -106,11 +99,10 @@ def _deploy_stack(self): self.deployer.execute_changeset(result["Id"], self.stack_name) self.deployer.wait_for_execute(self.stack_name, changeset_type) - def _verify_stack(self, expected_resource_path): stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) # verify if the stack was successfully created - self.assertEqual(stacks_description['Stacks'][0]['StackStatus'], 'CREATE_COMPLETE') + self.assertEqual(stacks_description["Stacks"][0]["StackStatus"], "CREATE_COMPLETE") # verify if the stack contains the expected resources self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 5598be497..09fdc2e51 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -2,7 +2,7 @@ import logging import re import random -import string # not deprecated, a bug from pylint https://www.logilab.org/ticket/2481 +import string # not deprecated, a bug from pylint https://www.logilab.org/ticket/2481 from functools import reduce import boto3 @@ -15,6 +15,7 @@ RANDOM_SUFFIX_LENGTH = 12 + def transform_template(input_file_path, output_file_path): LOG = logging.getLogger(__name__) iam_client = boto3.client("iam") @@ -38,9 +39,9 @@ def transform_template(input_file_path, output_file_path): def verify_stack_resources(expected_file_path, stack_resources): - with open(expected_file_path, 'r') as expected_data: + with open(expected_file_path, "r") as expected_data: expected_resources = _sort_resources(json.load(expected_data)) - parsed_resources = _sort_resources(stack_resources['StackResourceSummaries']) + parsed_resources = _sort_resources(stack_resources["StackResourceSummaries"]) if len(expected_resources) != len(parsed_resources): return False @@ -54,9 +55,11 @@ def verify_stack_resources(expected_file_path, stack_resources): return False return True + def generate_suffix(): # Very basic random letters generator - return ''.join(random.choice(string.ascii_lowercase) for i in range(RANDOM_SUFFIX_LENGTH)) + return "".join(random.choice(string.ascii_lowercase) for i in range(RANDOM_SUFFIX_LENGTH)) + def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) @@ -78,9 +81,9 @@ def create_bucket(bucket_name, region=None): # Create bucket if region is None: - s3_client = boto3.client('s3') + s3_client = boto3.client("s3") s3_client.create_bucket(Bucket=bucket_name) else: - s3_client = boto3.client('s3', region_name=region) - location = {'LocationConstraint': region} + s3_client = boto3.client("s3", region_name=region) + location = {"LocationConstraint": region} s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location) diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py index 23c4c0240..6ae359ace 100644 --- a/tests_integ/single/test_basic_api.py +++ b/tests_integ/single/test_basic_api.py @@ -5,12 +5,14 @@ class TestBasicApi(BaseTest): - @parameterized.expand([ - "basic_api_inline_openapi", - "basic_api_inline_swagger", - # "basic_api_with_cache", # no test case in java code base - "basic_api_with_tags", - "basic_api", - ]) + @parameterized.expand( + [ + "basic_api_inline_openapi", + "basic_api_inline_swagger", + # "basic_api_with_cache", # no test case in java code base + "basic_api_with_tags", + "basic_api", + ] + ) def test_basic_api(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_application.py b/tests_integ/single/test_basic_application.py index 0519e6f05..38ae79800 100644 --- a/tests_integ/single/test_basic_application.py +++ b/tests_integ/single/test_basic_application.py @@ -3,10 +3,13 @@ from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest + class TestBasicApplication(BaseTest): - @parameterized.expand([ - "basic_application_sar_location", - "basic_application_sar_location_with_intrinsics", - ]) + @parameterized.expand( + [ + "basic_application_sar_location", + "basic_application_sar_location_with_intrinsics", + ] + ) def test_basic_application(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index f88446aa7..9ac1da15b 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -5,18 +5,20 @@ class TestBasicFunction(BaseTest): - @parameterized.expand([ - "basic_function", - "basic_function_event_destinations", - "basic_function_no_envvar", - "basic_function_openapi", - "basic_function_with_kmskeyarn", - "basic_function_with_sns_dlq", - "basic_function_with_sqs_dlq", - "basic_function_with_tags", - # ("basic_function_with_tracing"), # need different set up to create changeset - # ("basic_table_no_param"), # no test case in java code base - # ("basic_table_with_param") # no test case in java code base - ]) + @parameterized.expand( + [ + "basic_function", + "basic_function_event_destinations", + "basic_function_no_envvar", + "basic_function_openapi", + "basic_function_with_kmskeyarn", + "basic_function_with_sns_dlq", + "basic_function_with_sqs_dlq", + "basic_function_with_tags", + # ("basic_function_with_tracing"), # need different set up to create changeset + # ("basic_table_no_param"), # no test case in java code base + # ("basic_table_with_param") # no test case in java code base + ] + ) def test_basic_function(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_http_api.py b/tests_integ/single/test_basic_http_api.py index 0da0c8a12..f1530ec2b 100644 --- a/tests_integ/single/test_basic_http_api.py +++ b/tests_integ/single/test_basic_http_api.py @@ -4,8 +4,10 @@ class TestBasicHttpApi(BaseTest): - @parameterized.expand([ - "basic_http_api", - ]) + @parameterized.expand( + [ + "basic_http_api", + ] + ) def test_basic_http_api(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_layer_version.py b/tests_integ/single/test_basic_layer_version.py index b6fe20ce6..a90dc8e2a 100644 --- a/tests_integ/single/test_basic_layer_version.py +++ b/tests_integ/single/test_basic_layer_version.py @@ -4,9 +4,11 @@ class TestBasicLayerVersion(BaseTest): - @parameterized.expand([ - "basic_layer", - "basic_layer_with_parameters", - ]) + @parameterized.expand( + [ + "basic_layer", + "basic_layer_with_parameters", + ] + ) def test_basic_layer_version(self, file_name): self.create_and_verify_stack(file_name) diff --git a/tests_integ/single/test_basic_state_machine.py b/tests_integ/single/test_basic_state_machine.py index ffce10cae..c11890a83 100644 --- a/tests_integ/single/test_basic_state_machine.py +++ b/tests_integ/single/test_basic_state_machine.py @@ -4,9 +4,11 @@ class TestBasicLayerVersion(BaseTest): - @parameterized.expand([ - "basic_state_machine_inline_definition", - # ("basic_state_machine_with_tags"), # cannot be translated by sam-tran - ]) + @parameterized.expand( + [ + "basic_state_machine_inline_definition", + # ("basic_state_machine_with_tags"), # cannot be translated by sam-tran + ] + ) def test_basic_state_machine(self, file_name): self.create_and_verify_stack(file_name) From e7bdf6c22893f2670dffc8029cd750f4a20ba65b Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 17 Nov 2020 16:04:27 -0800 Subject: [PATCH 045/105] refactor: No more references to BaseTest, using cls and self instead --- tests_integ/helpers/base_test.py | 49 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index dd2ed87a0..50d139168 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -16,47 +16,46 @@ class BaseTest(TestCase): @classmethod def setUpClass(cls): - BaseTest.tests_integ_dir = Path(__file__).resolve().parents[1] - BaseTest.resources_dir = Path(BaseTest.tests_integ_dir, "resources") - BaseTest.template_dir = Path(BaseTest.resources_dir, "templates", "single") - BaseTest.output_dir = BaseTest.tests_integ_dir - BaseTest.expected_dir = Path(BaseTest.resources_dir, "expected", "single") - code_dir = Path(BaseTest.resources_dir, "code") - - BaseTest.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() + cls.tests_integ_dir = Path(__file__).resolve().parents[1] + cls.resources_dir = Path(cls.tests_integ_dir, "resources") + cls.template_dir = Path(cls.resources_dir, "templates", "single") + cls.output_dir = cls.tests_integ_dir + cls.expected_dir = Path(cls.resources_dir, "expected", "single") + code_dir = Path(cls.resources_dir, "code") + + cls.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() session = boto3.session.Session() my_region = session.region_name - create_bucket(BaseTest.s3_bucket_name, region=my_region) + create_bucket(cls.s3_bucket_name, region=my_region) - s3_client = boto3.client("s3") - BaseTest.code_key_to_url = {} + cls.s3_client = boto3.client("s3") + cls.code_key_to_url = {} for key, file_name in CODE_KEY_TO_FILE_MAP.items(): code_path = str(Path(code_dir, file_name)) - s3_client.upload_file(code_path, BaseTest.s3_bucket_name, file_name) - code_url = f"s3://{BaseTest.s3_bucket_name}/{file_name}" - BaseTest.code_key_to_url[key] = code_url + cls.s3_client.upload_file(code_path, cls.s3_bucket_name, file_name) + code_url = f"s3://{cls.s3_bucket_name}/{file_name}" + cls.code_key_to_url[key] = code_url @classmethod - def tearDownClass(cls) -> None: - BaseTest._clean_bucket() + def tearDownClass(cls): + cls._clean_bucket() @classmethod def _clean_bucket(cls): - s3_client = boto3.client("s3") - response = s3_client.list_objects(Bucket=BaseTest.s3_bucket_name) + response = cls.s3_client.list_objects_v2(Bucket=cls.s3_bucket_name) for content in response["Contents"]: - s3_client.delete_object(Key=content["Key"], Bucket=BaseTest.s3_bucket_name) - s3_client.delete_bucket(Bucket=BaseTest.s3_bucket_name) + cls.s3_client.delete_object(Key=content["Key"], Bucket=cls.s3_bucket_name) + cls.s3_client.delete_bucket(Bucket=cls.s3_bucket_name) def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") def create_and_verify_stack(self, file_name): - input_file_path = str(Path(BaseTest.template_dir, file_name + ".yaml")) - self.output_file_path = str(Path(BaseTest.output_dir, "cfn_" + file_name + ".yaml")) - expected_resource_path = str(Path(BaseTest.expected_dir, file_name + ".json")) + input_file_path = str(Path(self.template_dir, file_name + ".yaml")) + self.output_file_path = str(Path(self.output_dir, "cfn_" + file_name + ".yaml")) + expected_resource_path = str(Path(self.expected_dir, file_name + ".json")) self.stack_name = STACK_NAME_PREFIX + file_name.replace("_", "-") + generate_suffix() self.sub_input_file_path = self._update_template(input_file_path, file_name) @@ -72,10 +71,10 @@ def tearDown(self): os.remove(self.sub_input_file_path) def _update_template(self, input_file_path, file_name): - updated_template_path = str(Path(BaseTest.output_dir, "sub_" + file_name + ".yaml")) + updated_template_path = str(Path(self.output_dir, "sub_" + file_name + ".yaml")) with open(input_file_path, "r") as f: data = f.read() - for key, s3_url in BaseTest.code_key_to_url.items(): + for key, s3_url in self.code_key_to_url.items(): data = data.replace(f"${{{key}}}", s3_url) yaml_doc = yaml.load(data, Loader=yaml.FullLoader) From c3037ae0962079ff28cd1a594bafc891d3b1fb37 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Tue, 17 Nov 2020 16:23:57 -0800 Subject: [PATCH 046/105] fix bug for creating bucket in us-east-1 --- tests_integ/helpers/base_test.py | 63 ++++++++++++------- tests_integ/helpers/helpers.py | 6 +- tests_integ/single/test_basic_api.py | 3 - tests_integ/single/test_basic_application.py | 2 - tests_integ/single/test_basic_function.py | 4 -- tests_integ/single/test_basic_http_api.py | 1 - .../single/test_basic_layer_version.py | 1 - .../single/test_basic_state_machine.py | 1 - 8 files changed, 45 insertions(+), 36 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index dd2ed87a0..086645973 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -1,3 +1,4 @@ +import logging import os from pathlib import Path from unittest.case import TestCase @@ -5,9 +6,11 @@ import boto3 import pytest import yaml +from botocore.exceptions import ClientError from samcli.lib.deploy.deployer import Deployer from tests_integ.helpers.helpers import transform_template, verify_stack_resources, generate_suffix, create_bucket +LOG = logging.getLogger(__name__) STACK_NAME_PREFIX = "sam-integ-stack-" S3_BUCKET_PREFIX = "sam-integ-bucket-" CODE_KEY_TO_FILE_MAP = {"codeuri": "code.zip", "contenturi": "layer1.zip", "definitionuri": "swagger1.json"} @@ -18,36 +21,17 @@ class BaseTest(TestCase): def setUpClass(cls): BaseTest.tests_integ_dir = Path(__file__).resolve().parents[1] BaseTest.resources_dir = Path(BaseTest.tests_integ_dir, "resources") - BaseTest.template_dir = Path(BaseTest.resources_dir, "templates", "single") + BaseTest.template_dir = Path(BaseTest.resources_dir, "templates", "single") # need to replace the single with variable to be extenable BaseTest.output_dir = BaseTest.tests_integ_dir BaseTest.expected_dir = Path(BaseTest.resources_dir, "expected", "single") code_dir = Path(BaseTest.resources_dir, "code") BaseTest.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() - session = boto3.session.Session() - my_region = session.region_name - create_bucket(BaseTest.s3_bucket_name, region=my_region) - - s3_client = boto3.client("s3") - BaseTest.code_key_to_url = {} - - for key, file_name in CODE_KEY_TO_FILE_MAP.items(): - code_path = str(Path(code_dir, file_name)) - s3_client.upload_file(code_path, BaseTest.s3_bucket_name, file_name) - code_url = f"s3://{BaseTest.s3_bucket_name}/{file_name}" - BaseTest.code_key_to_url[key] = code_url + BaseTest.code_key_to_url = _upload_resources(code_dir, BaseTest.s3_bucket_name, CODE_KEY_TO_FILE_MAP) @classmethod def tearDownClass(cls) -> None: - BaseTest._clean_bucket() - - @classmethod - def _clean_bucket(cls): - s3_client = boto3.client("s3") - response = s3_client.list_objects(Bucket=BaseTest.s3_bucket_name) - for content in response["Contents"]: - s3_client.delete_object(Key=content["Key"], Bucket=BaseTest.s3_bucket_name) - s3_client.delete_bucket(Bucket=BaseTest.s3_bucket_name) + _clean_bucket(BaseTest.s3_bucket_name) def setUp(self): self.cloudformation_client = boto3.client("cloudformation") @@ -106,3 +90,38 @@ def _verify_stack(self, expected_resource_path): self.assertEqual(stacks_description["Stacks"][0]["StackStatus"], "CREATE_COMPLETE") # verify if the stack contains the expected resources self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) + + +def _upload_resources(code_dir, s3_bucket_name, key_to_file_map): + session = boto3.session.Session() + my_region = session.region_name + create_bucket(s3_bucket_name, region=my_region) + + s3_client = boto3.client("s3") + code_key_to_url = {} + + try: + for key, file_name in key_to_file_map.items(): + code_path = str(Path(code_dir, file_name)) + LOG.debug(f"Uploading file {file_name} to s3 bucket {s3_bucket_name}.") + s3_client.upload_file(code_path, s3_bucket_name, file_name) + LOG.debug(f"{file_name} uploaded successfully.") + code_url = f"s3://{s3_bucket_name}/{file_name}" + code_key_to_url[key] = code_url + except ClientError as error: + LOG.error('upload failed') + LOG.error('Error code: ' + error.response['Error']['Code']) + _clean_bucket(s3_bucket_name) + raise error + return code_key_to_url + + +def _clean_bucket(s3_bucket_name): + s3_client = boto3.client("s3") + s3 = boto3.resource('s3') + bucket = s3.Bucket(s3_bucket_name) + object_summary_iterator = bucket.objects.all() + + for object_summary in object_summary_iterator: + s3_client.delete_object(Key=object_summary.key, Bucket=s3_bucket_name) + s3_client.delete_bucket(Bucket=s3_bucket_name) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 09fdc2e51..5b809266b 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -4,6 +4,7 @@ import random import string # not deprecated, a bug from pylint https://www.logilab.org/ticket/2481 from functools import reduce +from pathlib import Path import boto3 from botocore.exceptions import ClientError @@ -65,7 +66,7 @@ def _sort_resources(resources): return sorted(resources, key=lambda d: d["LogicalResourceId"]) -def create_bucket(bucket_name, region=None): +def create_bucket(bucket_name, region): """Create an S3 bucket in a specified region copy code from boto3 doc example @@ -80,10 +81,11 @@ def create_bucket(bucket_name, region=None): """ # Create bucket - if region is None: + if region is None or region == 'us-east-1': s3_client = boto3.client("s3") s3_client.create_bucket(Bucket=bucket_name) else: s3_client = boto3.client("s3", region_name=region) location = {"LocationConstraint": region} s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location) + diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py index 6ae359ace..e98278a55 100644 --- a/tests_integ/single/test_basic_api.py +++ b/tests_integ/single/test_basic_api.py @@ -1,5 +1,3 @@ -import os - from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest @@ -9,7 +7,6 @@ class TestBasicApi(BaseTest): [ "basic_api_inline_openapi", "basic_api_inline_swagger", - # "basic_api_with_cache", # no test case in java code base "basic_api_with_tags", "basic_api", ] diff --git a/tests_integ/single/test_basic_application.py b/tests_integ/single/test_basic_application.py index 38ae79800..44213f8e6 100644 --- a/tests_integ/single/test_basic_application.py +++ b/tests_integ/single/test_basic_application.py @@ -1,5 +1,3 @@ -import os - from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index 9ac1da15b..6c45ae9a7 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -1,5 +1,3 @@ -import os - from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest @@ -16,8 +14,6 @@ class TestBasicFunction(BaseTest): "basic_function_with_sqs_dlq", "basic_function_with_tags", # ("basic_function_with_tracing"), # need different set up to create changeset - # ("basic_table_no_param"), # no test case in java code base - # ("basic_table_with_param") # no test case in java code base ] ) def test_basic_function(self, file_name): diff --git a/tests_integ/single/test_basic_http_api.py b/tests_integ/single/test_basic_http_api.py index f1530ec2b..31a31fcb9 100644 --- a/tests_integ/single/test_basic_http_api.py +++ b/tests_integ/single/test_basic_http_api.py @@ -1,5 +1,4 @@ from parameterized import parameterized - from tests_integ.helpers.base_test import BaseTest diff --git a/tests_integ/single/test_basic_layer_version.py b/tests_integ/single/test_basic_layer_version.py index a90dc8e2a..0dd958757 100644 --- a/tests_integ/single/test_basic_layer_version.py +++ b/tests_integ/single/test_basic_layer_version.py @@ -1,5 +1,4 @@ from parameterized import parameterized - from tests_integ.helpers.base_test import BaseTest diff --git a/tests_integ/single/test_basic_state_machine.py b/tests_integ/single/test_basic_state_machine.py index c11890a83..276445f1b 100644 --- a/tests_integ/single/test_basic_state_machine.py +++ b/tests_integ/single/test_basic_state_machine.py @@ -1,5 +1,4 @@ from parameterized import parameterized - from tests_integ.helpers.base_test import BaseTest From da61baf5573d08d9c1d3783f1a8f72da41fc6d78 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Tue, 17 Nov 2020 16:54:57 -0800 Subject: [PATCH 047/105] update _upload_resources and _clean_bucket after resolving merge conflict --- tests_integ/helpers/base_test.py | 74 +++++++++++++------------------- 1 file changed, 29 insertions(+), 45 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 33b7c3005..5a8d25290 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -32,13 +32,7 @@ def setUpClass(cls): create_bucket(cls.s3_bucket_name, region=my_region) cls.s3_client = boto3.client("s3") - cls.code_key_to_url = {} - - for key, file_name in CODE_KEY_TO_FILE_MAP.items(): - code_path = str(Path(code_dir, file_name)) - cls.s3_client.upload_file(code_path, cls.s3_bucket_name, file_name) - code_url = f"s3://{cls.s3_bucket_name}/{file_name}" - cls.code_key_to_url[key] = code_url + cls.code_key_to_url = cls._upload_resources(code_dir, cls.s3_bucket_name, CODE_KEY_TO_FILE_MAP) @classmethod def tearDownClass(cls): @@ -46,11 +40,36 @@ def tearDownClass(cls): @classmethod def _clean_bucket(cls): - response = cls.s3_client.list_objects_v2(Bucket=cls.s3_bucket_name) - for content in response["Contents"]: - cls.s3_client.delete_object(Key=content["Key"], Bucket=cls.s3_bucket_name) + s3 = boto3.resource('s3') + bucket = s3.Bucket(cls.s3_bucket_name) + object_summary_iterator = bucket.objects.all() + + for object_summary in object_summary_iterator: + cls.s3_client.delete_object(Key=object_summary.key, Bucket=cls.s3_bucket_name) cls.s3_client.delete_bucket(Bucket=cls.s3_bucket_name) + @classmethod + def _upload_resources(cls, code_dir, s3_bucket_name, key_to_file_map): + session = boto3.session.Session() + my_region = session.region_name + create_bucket(s3_bucket_name, region=my_region) + code_key_to_url = {} + + try: + for key, file_name in key_to_file_map.items(): + code_path = str(Path(code_dir, file_name)) + LOG.debug(f"Uploading file {file_name} to s3 bucket {s3_bucket_name}.") + cls.s3_client.upload_file(code_path, s3_bucket_name, file_name) + LOG.debug(f"{file_name} uploaded successfully.") + code_url = f"s3://{s3_bucket_name}/{file_name}" + code_key_to_url[key] = code_url + except ClientError as error: + LOG.error('upload failed') + LOG.error('Error code: ' + error.response['Error']['Code']) + cls._clean_bucket(s3_bucket_name) + raise error + return code_key_to_url + def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") @@ -108,38 +127,3 @@ def _verify_stack(self, expected_resource_path): self.assertEqual(stacks_description["Stacks"][0]["StackStatus"], "CREATE_COMPLETE") # verify if the stack contains the expected resources self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) - - -def _upload_resources(code_dir, s3_bucket_name, key_to_file_map): - session = boto3.session.Session() - my_region = session.region_name - create_bucket(s3_bucket_name, region=my_region) - - s3_client = boto3.client("s3") - code_key_to_url = {} - - try: - for key, file_name in key_to_file_map.items(): - code_path = str(Path(code_dir, file_name)) - LOG.debug(f"Uploading file {file_name} to s3 bucket {s3_bucket_name}.") - s3_client.upload_file(code_path, s3_bucket_name, file_name) - LOG.debug(f"{file_name} uploaded successfully.") - code_url = f"s3://{s3_bucket_name}/{file_name}" - code_key_to_url[key] = code_url - except ClientError as error: - LOG.error('upload failed') - LOG.error('Error code: ' + error.response['Error']['Code']) - _clean_bucket(s3_bucket_name) - raise error - return code_key_to_url - - -def _clean_bucket(s3_bucket_name): - s3_client = boto3.client("s3") - s3 = boto3.resource('s3') - bucket = s3.Bucket(s3_bucket_name) - object_summary_iterator = bucket.objects.all() - - for object_summary in object_summary_iterator: - s3_client.delete_object(Key=object_summary.key, Bucket=s3_bucket_name) - s3_client.delete_bucket(Bucket=s3_bucket_name) From dab73e76cb904a84646ecc4f159f54d4deef8a6d Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 17 Nov 2020 17:38:59 -0800 Subject: [PATCH 048/105] refactor: In BaseTest, moved some stuff to class attributes, added doc --- tests_integ/helpers/base_test.py | 88 ++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 5a8d25290..5a6ce5b24 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -24,15 +24,13 @@ def setUpClass(cls): cls.template_dir = Path(cls.resources_dir, "templates", "single") cls.output_dir = cls.tests_integ_dir cls.expected_dir = Path(cls.resources_dir, "expected", "single") - code_dir = Path(cls.resources_dir, "code") - + cls.code_dir = Path(cls.resources_dir, "code") cls.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() - session = boto3.session.Session() - my_region = session.region_name - create_bucket(cls.s3_bucket_name, region=my_region) - + cls.session = boto3.session.Session() + cls.my_region = cls.session.region_name cls.s3_client = boto3.client("s3") - cls.code_key_to_url = cls._upload_resources(code_dir, cls.s3_bucket_name, CODE_KEY_TO_FILE_MAP) + + cls._upload_resources() @classmethod def tearDownClass(cls): @@ -40,6 +38,9 @@ def tearDownClass(cls): @classmethod def _clean_bucket(cls): + """ + Empties and deletes the bucket used for the tests + """ s3 = boto3.resource('s3') bucket = s3.Bucket(cls.s3_bucket_name) object_summary_iterator = bucket.objects.all() @@ -49,50 +50,70 @@ def _clean_bucket(cls): cls.s3_client.delete_bucket(Bucket=cls.s3_bucket_name) @classmethod - def _upload_resources(cls, code_dir, s3_bucket_name, key_to_file_map): - session = boto3.session.Session() - my_region = session.region_name - create_bucket(s3_bucket_name, region=my_region) + def _upload_resources(cls): + """ + Creates the bucket and uploads the files used by the tests to it + """ + create_bucket(cls.s3_bucket_name, region=cls.my_region) code_key_to_url = {} try: - for key, file_name in key_to_file_map.items(): - code_path = str(Path(code_dir, file_name)) - LOG.debug(f"Uploading file {file_name} to s3 bucket {s3_bucket_name}.") - cls.s3_client.upload_file(code_path, s3_bucket_name, file_name) + for key, file_name in CODE_KEY_TO_FILE_MAP.items(): + code_path = str(Path(cls.code_dir, file_name)) + LOG.debug(f"Uploading file {file_name} to s3 bucket {cls.s3_bucket_name}.") + cls.s3_client.upload_file(code_path, cls.s3_bucket_name, file_name) LOG.debug(f"{file_name} uploaded successfully.") - code_url = f"s3://{s3_bucket_name}/{file_name}" + code_url = f"s3://{cls.s3_bucket_name}/{file_name}" code_key_to_url[key] = code_url except ClientError as error: LOG.error('upload failed') LOG.error('Error code: ' + error.response['Error']['Code']) - cls._clean_bucket(s3_bucket_name) + cls._clean_bucket() raise error - return code_key_to_url + + cls.code_key_to_url = code_key_to_url def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") + def tearDown(self): + self.cloudformation_client.delete_stack(StackName=self.stack_name) + if os.path.exists(self.output_file_path): + os.remove(self.output_file_path) + if os.path.exists(self.sub_input_file_path): + os.remove(self.sub_input_file_path) + def create_and_verify_stack(self, file_name): - input_file_path = str(Path(self.template_dir, file_name + ".yaml")) + """ + Creates the Cloud Formation stack and verifies it against the expected + result + + Parameters + ---------- + file_name : string + Template file name + """ self.output_file_path = str(Path(self.output_dir, "cfn_" + file_name + ".yaml")) expected_resource_path = str(Path(self.expected_dir, file_name + ".json")) self.stack_name = STACK_NAME_PREFIX + file_name.replace("_", "-") + generate_suffix() - self.sub_input_file_path = self._update_template(input_file_path, file_name) + self._update_template(file_name) transform_template(self.sub_input_file_path, self.output_file_path) self._deploy_stack() self._verify_stack(expected_resource_path) - def tearDown(self): - self.cloudformation_client.delete_stack(StackName=self.stack_name) - if os.path.exists(self.output_file_path): - os.remove(self.output_file_path) - if os.path.exists(self.sub_input_file_path): - os.remove(self.sub_input_file_path) + def _update_template(self, file_name): + """ + Updates a template before converting it to a cloud formation template + and saves it to sub_input_file_path - def _update_template(self, input_file_path, file_name): + Parameters + ---------- + file_name : string + Template file name + """ + input_file_path = str(Path(self.template_dir, file_name + ".yaml")) updated_template_path = str(Path(self.output_dir, "sub_" + file_name + ".yaml")) with open(input_file_path, "r") as f: data = f.read() @@ -103,9 +124,12 @@ def _update_template(self, input_file_path, file_name): with open(updated_template_path, "w") as f: yaml.dump(yaml_doc, f) - return updated_template_path + self.sub_input_file_path = updated_template_path def _deploy_stack(self): + """ + Deploys the current cloud formation stack + """ with open(self.output_file_path, "r") as cfn_file: result, changeset_type = self.deployer.create_and_wait_for_changeset( stack_name=self.stack_name, @@ -121,6 +145,14 @@ def _deploy_stack(self): self.deployer.wait_for_execute(self.stack_name, changeset_type) def _verify_stack(self, expected_resource_path): + """ + Gets and compares the Cloud Formation stack against the expect result file + + Parameters: + ---------- + expected_resource_path: string + Absolute path to the expected result file + """ stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) # verify if the stack was successfully created From 3e3f38739f44c91133025d447ac52c105966dd0a Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Wed, 18 Nov 2020 11:36:08 -0800 Subject: [PATCH 049/105] refactor: Moved the resource file maps to their own py file, file_resources.py. Updated the log messages to include more information, added methods to BaseTest to get a file S3 URI from file name and variable name --- tests_integ/helpers/base_test.py | 61 +++++-- tests_integ/helpers/file_resources.py | 12 ++ tests_integ/resources/code/swagger2.json | 215 +++++++++++++++++++++++ 3 files changed, 272 insertions(+), 16 deletions(-) create mode 100644 tests_integ/helpers/file_resources.py create mode 100644 tests_integ/resources/code/swagger2.json diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 5a6ce5b24..6e763f72c 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -9,11 +9,11 @@ from botocore.exceptions import ClientError from samcli.lib.deploy.deployer import Deployer from tests_integ.helpers.helpers import transform_template, verify_stack_resources, generate_suffix, create_bucket +from tests_integ.helpers.file_resources import FILE_TO_S3_URI_MAP, CODE_KEY_TO_FILE_MAP LOG = logging.getLogger(__name__) STACK_NAME_PREFIX = "sam-integ-stack-" S3_BUCKET_PREFIX = "sam-integ-bucket-" -CODE_KEY_TO_FILE_MAP = {"codeuri": "code.zip", "contenturi": "layer1.zip", "definitionuri": "swagger1.json"} class BaseTest(TestCase): @@ -22,7 +22,7 @@ def setUpClass(cls): cls.tests_integ_dir = Path(__file__).resolve().parents[1] cls.resources_dir = Path(cls.tests_integ_dir, "resources") cls.template_dir = Path(cls.resources_dir, "templates", "single") - cls.output_dir = cls.tests_integ_dir + cls.output_dir = Path(cls.tests_integ_dir, "tmp") cls.expected_dir = Path(cls.resources_dir, "expected", "single") cls.code_dir = Path(cls.resources_dir, "code") cls.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() @@ -30,6 +30,9 @@ def setUpClass(cls): cls.my_region = cls.session.region_name cls.s3_client = boto3.client("s3") + if not os.path.exists(cls.output_dir): + os.mkdir(cls.output_dir) + cls._upload_resources() @classmethod @@ -46,8 +49,14 @@ def _clean_bucket(cls): object_summary_iterator = bucket.objects.all() for object_summary in object_summary_iterator: - cls.s3_client.delete_object(Key=object_summary.key, Bucket=cls.s3_bucket_name) - cls.s3_client.delete_bucket(Bucket=cls.s3_bucket_name) + try: + cls.s3_client.delete_object(Key=object_summary.key, Bucket=cls.s3_bucket_name) + except ClientError as e: + LOG.error("Unable to delete object %s from bucket %s", object_summary.key, cls.s3_bucket_name, exc_info=e) + try: + cls.s3_client.delete_bucket(Bucket=cls.s3_bucket_name) + except ClientError as e: + LOG.error("Unable to delete bucket %s", cls.s3_bucket_name, exc_info=e) @classmethod def _upload_resources(cls): @@ -55,24 +64,22 @@ def _upload_resources(cls): Creates the bucket and uploads the files used by the tests to it """ create_bucket(cls.s3_bucket_name, region=cls.my_region) - code_key_to_url = {} + + current_file_name = "" try: - for key, file_name in CODE_KEY_TO_FILE_MAP.items(): + for file_name, _ in FILE_TO_S3_URI_MAP.items(): + current_file_name = file_name code_path = str(Path(cls.code_dir, file_name)) - LOG.debug(f"Uploading file {file_name} to s3 bucket {cls.s3_bucket_name}.") + LOG.debug("Uploading file %s to bucket %s", file_name, cls.s3_bucket_name) cls.s3_client.upload_file(code_path, cls.s3_bucket_name, file_name) - LOG.debug(f"{file_name} uploaded successfully.") - code_url = f"s3://{cls.s3_bucket_name}/{file_name}" - code_key_to_url[key] = code_url + LOG.debug("File %s uploaded successfully to bucket %s", file_name, cls.s3_bucket_name) + FILE_TO_S3_URI_MAP[file_name] = f"s3://{cls.s3_bucket_name}/{file_name}" except ClientError as error: - LOG.error('upload failed') - LOG.error('Error code: ' + error.response['Error']['Code']) + LOG.error("Upload of file %s to bucket %s failed", current_file_name, cls.s3_bucket_name, exc_info=error) cls._clean_bucket() raise error - cls.code_key_to_url = code_key_to_url - def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") @@ -103,6 +110,28 @@ def create_and_verify_stack(self, file_name): self._deploy_stack() self._verify_stack(expected_resource_path) + def get_s3_uri(self, file_name): + """ + Returns the S3 URI of a resource file + + Parameters + ---------- + file_name : string + Resource file name + """ + return FILE_TO_S3_URI_MAP[file_name] + + def get_code_key_s3_uri(self, code_key): + """ + Returns the S3 URI of a code key for template replacement + + Parameters + ---------- + code_key : string + Template code key + """ + return FILE_TO_S3_URI_MAP[CODE_KEY_TO_FILE_MAP[code_key]] + def _update_template(self, file_name): """ Updates a template before converting it to a cloud formation template @@ -117,8 +146,8 @@ def _update_template(self, file_name): updated_template_path = str(Path(self.output_dir, "sub_" + file_name + ".yaml")) with open(input_file_path, "r") as f: data = f.read() - for key, s3_url in self.code_key_to_url.items(): - data = data.replace(f"${{{key}}}", s3_url) + for key, _ in CODE_KEY_TO_FILE_MAP.items(): + data = data.replace(f"${{{key}}}", self.get_code_key_s3_uri(key)) yaml_doc = yaml.load(data, Loader=yaml.FullLoader) with open(updated_template_path, "w") as f: diff --git a/tests_integ/helpers/file_resources.py b/tests_integ/helpers/file_resources.py new file mode 100644 index 000000000..c48963176 --- /dev/null +++ b/tests_integ/helpers/file_resources.py @@ -0,0 +1,12 @@ +FILE_TO_S3_URI_MAP = { + "code.zip": "", + "layer1.zip": "", + "swagger1.json": "", + "swagger2.json": "" +} + +CODE_KEY_TO_FILE_MAP = { + "codeuri": "code.zip", + "contenturi": "layer1.zip", + "definitionuri": "swagger1.json" +} diff --git a/tests_integ/resources/code/swagger2.json b/tests_integ/resources/code/swagger2.json new file mode 100644 index 000000000..7db53a271 --- /dev/null +++ b/tests_integ/resources/code/swagger2.json @@ -0,0 +1,215 @@ +{ + "swagger": "2.0", + "info": { + "title": "PetStore", + "description": "Your first API with Amazon API Gateway. This is a sample API that integrates via HTTP with our demo Pet Store endpoints" + }, + "schemes": [ + "https" + ], + "paths": { + "/": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "text/html" + ], + "responses": { + "200": { + "description": "200 response", + "headers": { + "Content-Type": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Content-Type": "'text/html'" + }, + "responseTemplates": { + "text/html": "\n \n \n \n \n

Welcome to your Pet Store API

\n

\n You have succesfully deployed your first API. You are seeing this HTML page because the GET method to the root resource of your API returns this content as a Mock integration.\n

\n

\n The Pet Store API contains the /pets and /pets/{petId} resources. By making a GET request to /pets you can retrieve a list of Pets in your API. If you are looking for a specific pet, for example the pet with ID 1, you can make a GET request to /pets/1.\n

\n

\n You can use a REST client such as Postman to test the POST methods in your API to create a new pet. Use the sample body below to send the POST request:\n

\n
\n{\n    \"type\" : \"cat\",\n    \"price\" : 123.11\n}\n        
\n \n" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + }, + "post": { + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod": "POST", + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'POST,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + }, + "/pets/{petId}": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets/{petId}", + "httpMethod": "GET", + "requestParameters": { + "integration.request.path.petId": "method.request.path.petId" + }, + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + } + }, + "definitions": { + "Empty": { + "type": "object" + } + } +} From cf1d3f282669c782b4d755524058012999bd2bcd Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Wed, 18 Nov 2020 15:38:41 -0800 Subject: [PATCH 050/105] feat: Fully implemented basic_api test and various refactors --- tests_integ/helpers/base_test.py | 60 ++++++++++++++++++---------- tests_integ/single/test_basic_api.py | 18 +++++++-- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 6e763f72c..8d28ca7ef 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -102,13 +102,16 @@ def create_and_verify_stack(self, file_name): Template file name """ self.output_file_path = str(Path(self.output_dir, "cfn_" + file_name + ".yaml")) - expected_resource_path = str(Path(self.expected_dir, file_name + ".json")) - self.stack_name = STACK_NAME_PREFIX + file_name.replace("_", "-") + generate_suffix() + self.expected_resource_path = str(Path(self.expected_dir, file_name + ".json")) + self.stack_name = STACK_NAME_PREFIX + file_name.replace("_", "-") + "-" + generate_suffix() - self._update_template(file_name) + self._fill_template(file_name) + self.transform_template() + self.deploy_stack() + self.verify_stack() + + def transform_template(self): transform_template(self.sub_input_file_path, self.output_file_path) - self._deploy_stack() - self._verify_stack(expected_resource_path) def get_s3_uri(self, file_name): """ @@ -131,11 +134,21 @@ def get_code_key_s3_uri(self, code_key): Template code key """ return FILE_TO_S3_URI_MAP[CODE_KEY_TO_FILE_MAP[code_key]] - - def _update_template(self, file_name): - """ - Updates a template before converting it to a cloud formation template - and saves it to sub_input_file_path + + def get_deployment_ids(self): + ids = [] + if not self.stack_resources: + return ids + + for res in self.stack_resources["StackResourceSummaries"]: + if res["ResourceType"] == "AWS::ApiGateway::Deployment": + ids.append(res["LogicalResourceId"]) + + return ids + + def _fill_template(self, file_name): + """ + Replaces the template variables with their value Parameters ---------- @@ -155,7 +168,16 @@ def _update_template(self, file_name): self.sub_input_file_path = updated_template_path - def _deploy_stack(self): + def set_template_resource(self, resource_name, property_name, value): + with open(self.sub_input_file_path, "r+") as f: + data = f.read() + yaml_doc = yaml.load(data, Loader=yaml.FullLoader) + yaml_doc['Resources'][resource_name]["Properties"][property_name] = value + + with open(self.sub_input_file_path, "w") as f: + yaml.dump(yaml_doc, f) + + def deploy_stack(self): """ Deploys the current cloud formation stack """ @@ -173,18 +195,14 @@ def _deploy_stack(self): self.deployer.execute_changeset(result["Id"], self.stack_name) self.deployer.wait_for_execute(self.stack_name, changeset_type) - def _verify_stack(self, expected_resource_path): + self.stack_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) + self.stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) + + def verify_stack(self): """ Gets and compares the Cloud Formation stack against the expect result file - - Parameters: - ---------- - expected_resource_path: string - Absolute path to the expected result file """ - stacks_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) - stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) # verify if the stack was successfully created - self.assertEqual(stacks_description["Stacks"][0]["StackStatus"], "CREATE_COMPLETE") + self.assertEqual(self.stack_description["Stacks"][0]["StackStatus"], "CREATE_COMPLETE") # verify if the stack contains the expected resources - self.assertTrue(verify_stack_resources(expected_resource_path, stack_resources)) + self.assertTrue(verify_stack_resources(self.expected_resource_path, self.stack_resources)) diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py index e98278a55..8cebc8fd2 100644 --- a/tests_integ/single/test_basic_api.py +++ b/tests_integ/single/test_basic_api.py @@ -3,13 +3,25 @@ class TestBasicApi(BaseTest): + def test_basic_api(self): + self.create_and_verify_stack("basic_api") + + first_dep_ids = self.get_deployment_ids() + + self.set_template_resource("MyApi", "DefinitionUri", self.get_s3_uri("swagger2.json")) + self.transform_template() + self.deploy_stack() + + second_dep_ids = self.get_deployment_ids() + + self.assertEqual(len(set(first_dep_ids).intersection(second_dep_ids)), 0) + @parameterized.expand( [ "basic_api_inline_openapi", "basic_api_inline_swagger", - "basic_api_with_tags", - "basic_api", + "basic_api_with_tags" ] ) - def test_basic_api(self, file_name): + def test_basic_api_others(self, file_name): self.create_and_verify_stack(file_name) From e41d19d4af3d39bba268e71bd22b75f8d29f0ec9 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Wed, 18 Nov 2020 15:49:14 -0800 Subject: [PATCH 051/105] refactor: Renamed S3 dict from URI to URL --- tests_integ/helpers/base_test.py | 10 +++++----- tests_integ/helpers/file_resources.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 8d28ca7ef..908c0fb36 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -9,7 +9,7 @@ from botocore.exceptions import ClientError from samcli.lib.deploy.deployer import Deployer from tests_integ.helpers.helpers import transform_template, verify_stack_resources, generate_suffix, create_bucket -from tests_integ.helpers.file_resources import FILE_TO_S3_URI_MAP, CODE_KEY_TO_FILE_MAP +from tests_integ.helpers.file_resources import FILE_TO_S3_URL_MAP, CODE_KEY_TO_FILE_MAP LOG = logging.getLogger(__name__) STACK_NAME_PREFIX = "sam-integ-stack-" @@ -68,13 +68,13 @@ def _upload_resources(cls): current_file_name = "" try: - for file_name, _ in FILE_TO_S3_URI_MAP.items(): + for file_name, _ in FILE_TO_S3_URL_MAP.items(): current_file_name = file_name code_path = str(Path(cls.code_dir, file_name)) LOG.debug("Uploading file %s to bucket %s", file_name, cls.s3_bucket_name) cls.s3_client.upload_file(code_path, cls.s3_bucket_name, file_name) LOG.debug("File %s uploaded successfully to bucket %s", file_name, cls.s3_bucket_name) - FILE_TO_S3_URI_MAP[file_name] = f"s3://{cls.s3_bucket_name}/{file_name}" + FILE_TO_S3_URL_MAP[file_name] = f"s3://{cls.s3_bucket_name}/{file_name}" except ClientError as error: LOG.error("Upload of file %s to bucket %s failed", current_file_name, cls.s3_bucket_name, exc_info=error) cls._clean_bucket() @@ -122,7 +122,7 @@ def get_s3_uri(self, file_name): file_name : string Resource file name """ - return FILE_TO_S3_URI_MAP[file_name] + return FILE_TO_S3_URL_MAP[file_name] def get_code_key_s3_uri(self, code_key): """ @@ -133,7 +133,7 @@ def get_code_key_s3_uri(self, code_key): code_key : string Template code key """ - return FILE_TO_S3_URI_MAP[CODE_KEY_TO_FILE_MAP[code_key]] + return FILE_TO_S3_URL_MAP[CODE_KEY_TO_FILE_MAP[code_key]] def get_deployment_ids(self): ids = [] diff --git a/tests_integ/helpers/file_resources.py b/tests_integ/helpers/file_resources.py index c48963176..400b94e03 100644 --- a/tests_integ/helpers/file_resources.py +++ b/tests_integ/helpers/file_resources.py @@ -1,4 +1,4 @@ -FILE_TO_S3_URI_MAP = { +FILE_TO_S3_URL_MAP = { "code.zip": "", "layer1.zip": "", "swagger1.json": "", From d37fda534edb23fdc3ad269683081a1b2c2bcbec Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Wed, 18 Nov 2020 18:06:19 -0800 Subject: [PATCH 052/105] feat: Implemented all the test_basic_api tests, added get_stack_stages method to BaseTest, refactored a few things here and there --- tests_integ/helpers/base_test.py | 51 +++++++++++++++++++---- tests_integ/helpers/helpers.py | 4 +- tests_integ/single/test_basic_api.py | 62 ++++++++++++++++++++++------ 3 files changed, 95 insertions(+), 22 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 908c0fb36..56aa988e0 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -29,6 +29,7 @@ def setUpClass(cls): cls.session = boto3.session.Session() cls.my_region = cls.session.region_name cls.s3_client = boto3.client("s3") + cls.api_client = boto3.client('apigateway', cls.my_region) if not os.path.exists(cls.output_dir): os.mkdir(cls.output_dir) @@ -52,7 +53,12 @@ def _clean_bucket(cls): try: cls.s3_client.delete_object(Key=object_summary.key, Bucket=cls.s3_bucket_name) except ClientError as e: - LOG.error("Unable to delete object %s from bucket %s", object_summary.key, cls.s3_bucket_name, exc_info=e) + LOG.error( + "Unable to delete object %s from bucket %s", + object_summary.key, + cls.s3_bucket_name, + exc_info=e + ) try: cls.s3_client.delete_bucket(Bucket=cls.s3_bucket_name) except ClientError as e: @@ -134,18 +140,28 @@ def get_code_key_s3_uri(self, code_key): Template code key """ return FILE_TO_S3_URL_MAP[CODE_KEY_TO_FILE_MAP[code_key]] - - def get_deployment_ids(self): + + def get_stack_deployment_ids(self): ids = [] if not self.stack_resources: return ids - + for res in self.stack_resources["StackResourceSummaries"]: if res["ResourceType"] == "AWS::ApiGateway::Deployment": ids.append(res["LogicalResourceId"]) - + return ids + def get_stack_stages(self): + if not self.stack_resources: + return [] + + for res in self.stack_resources["StackResourceSummaries"]: + if res["ResourceType"] == "AWS::ApiGateway::RestApi": + return self.api_client.get_stages(restApiId=res["PhysicalResourceId"])["item"] + + return [] + def _fill_template(self, file_name): """ Replaces the template variables with their value @@ -168,15 +184,34 @@ def _fill_template(self, file_name): self.sub_input_file_path = updated_template_path - def set_template_resource(self, resource_name, property_name, value): + def set_template_resource_property(self, resource_name, property_name, value): + """ + Updates a resource property of the current SAM template + + Parameters + ---------- + resource_name: string + resource name + property_name: string + property name + value + value + """ with open(self.sub_input_file_path, "r+") as f: - data = f.read() + data = f.read() yaml_doc = yaml.load(data, Loader=yaml.FullLoader) yaml_doc['Resources'][resource_name]["Properties"][property_name] = value with open(self.sub_input_file_path, "w") as f: yaml.dump(yaml_doc, f) + def get_template_resource_property(self, resource_name, property_name): + with open(self.sub_input_file_path, "r+") as f: + data = f.read() + yaml_doc = yaml.load(data, Loader=yaml.FullLoader) + + return yaml_doc['Resources'][resource_name]["Properties"][property_name] + def deploy_stack(self): """ Deploys the current cloud formation stack @@ -196,7 +231,7 @@ def deploy_stack(self): self.deployer.wait_for_execute(self.stack_name, changeset_type) self.stack_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) - self.stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) + self.stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) def verify_stack(self): """ diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index cd268d84c..4ab8f3a19 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -32,8 +32,8 @@ def transform_template(input_file_path, output_file_path): print("Wrote transformed CloudFormation template to: " + output_file_path) except InvalidDocumentException as e: - errorMessage = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) - LOG.error(errorMessage) + error_message = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) + LOG.error(error_message) errors = map(lambda cause: cause.message, e.causes) LOG.error(errors) diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py index 8cebc8fd2..98c5eff62 100644 --- a/tests_integ/single/test_basic_api.py +++ b/tests_integ/single/test_basic_api.py @@ -6,22 +6,60 @@ class TestBasicApi(BaseTest): def test_basic_api(self): self.create_and_verify_stack("basic_api") - first_dep_ids = self.get_deployment_ids() + first_dep_ids = self.get_stack_deployment_ids() + self.assertEqual(len(first_dep_ids), 1) - self.set_template_resource("MyApi", "DefinitionUri", self.get_s3_uri("swagger2.json")) + self.set_template_resource_property("MyApi", "DefinitionUri", self.get_s3_uri("swagger2.json")) self.transform_template() self.deploy_stack() - second_dep_ids = self.get_deployment_ids() + second_dep_ids = self.get_stack_deployment_ids() + self.assertEqual(len(second_dep_ids), 1) self.assertEqual(len(set(first_dep_ids).intersection(second_dep_ids)), 0) - @parameterized.expand( - [ - "basic_api_inline_openapi", - "basic_api_inline_swagger", - "basic_api_with_tags" - ] - ) - def test_basic_api_others(self, file_name): - self.create_and_verify_stack(file_name) + def test_basic_api_inline_openapi(self): + self.create_and_verify_stack("basic_api_inline_openapi") + + first_dep_ids = self.get_stack_deployment_ids() + self.assertEqual(len(first_dep_ids), 1) + + body = self.get_template_resource_property("MyApi", "DefinitionBody") + body["basePath"] = "/newDemo" + self.set_template_resource_property("MyApi", "DefinitionBody", body) + self.transform_template() + self.deploy_stack() + + second_dep_ids = self.get_stack_deployment_ids() + self.assertEqual(len(second_dep_ids), 1) + + self.assertEqual(len(set(first_dep_ids).intersection(second_dep_ids)), 0) + + def test_basic_api_inline_swagger(self): + self.create_and_verify_stack("basic_api_inline_swagger") + + first_dep_ids = self.get_stack_deployment_ids() + self.assertEqual(len(first_dep_ids), 1) + + body = self.get_template_resource_property("MyApi", "DefinitionBody") + body["basePath"] = "/newDemo" + self.set_template_resource_property("MyApi", "DefinitionBody", body) + self.transform_template() + self.deploy_stack() + + second_dep_ids = self.get_stack_deployment_ids() + self.assertEqual(len(second_dep_ids), 1) + + self.assertEqual(len(set(first_dep_ids).intersection(second_dep_ids)), 0) + + def test_basic_api_with_tags(self): + self.create_and_verify_stack("basic_api_with_tags") + + stages = self.get_stack_stages() + self.assertEqual(len(stages), 2) + + stage = next((s for s in stages if s["stageName"] == "my-new-stage-name"), None) + self.assertIsNotNone(stage) + self.assertEqual(stage["tags"]["TagKey1"], "TagValue1") + self.assertEqual(stage["tags"]["TagKey2"], "") + From befee0e3ba19625a7f6a9c781996439764aa7628 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Thu, 19 Nov 2020 13:54:05 -0800 Subject: [PATCH 053/105] feat: test_basic_application tests migrated --- tests_integ/helpers/base_test.py | 55 +++++++++++++++---- tests_integ/helpers/file_resources.py | 6 +- tests_integ/resources/code/template.yaml | 8 +++ .../single/basic_application_s3_location.json | 3 + .../single/basic_api_with_cache.yaml | 12 ---- tests_integ/single/test_basic_application.py | 35 +++++++++--- 6 files changed, 85 insertions(+), 34 deletions(-) create mode 100644 tests_integ/resources/code/template.yaml create mode 100644 tests_integ/resources/expected/single/basic_application_s3_location.json delete mode 100644 tests_integ/resources/templates/single/basic_api_with_cache.yaml diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 56aa988e0..3f240ece7 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -80,12 +80,26 @@ def _upload_resources(cls): LOG.debug("Uploading file %s to bucket %s", file_name, cls.s3_bucket_name) cls.s3_client.upload_file(code_path, cls.s3_bucket_name, file_name) LOG.debug("File %s uploaded successfully to bucket %s", file_name, cls.s3_bucket_name) - FILE_TO_S3_URL_MAP[file_name] = f"s3://{cls.s3_bucket_name}/{file_name}" + FILE_TO_S3_URL_MAP[file_name] = cls._get_s3_url(file_name) except ClientError as error: LOG.error("Upload of file %s to bucket %s failed", current_file_name, cls.s3_bucket_name, exc_info=error) cls._clean_bucket() raise error + @classmethod + def _get_s3_url(cls, file_name): + if file_name != "template.yaml": + return f"s3://{cls.s3_bucket_name}/{file_name}" + + if cls.my_region == "us-east-1": + return f"https://s3.amazonaws.com/{cls.s3_bucket_name}/{file_name}" + if cls.my_region == "us-iso-east-1": + return f"https://s3.us-iso-east-1.c2s.ic.gov/{cls.s3_bucket_name}/{file_name}" + if cls.my_region == "us-isob-east-1": + return f"https://s3.us-isob-east-1.sc2s.sgov.gov/{cls.s3_bucket_name}/{file_name}" + + return f"https://s3-{cls.my_region}.amazonaws.com/{cls.s3_bucket_name}/{file_name}" + def setUp(self): self.cloudformation_client = boto3.client("cloudformation") self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") @@ -118,6 +132,9 @@ def create_and_verify_stack(self, file_name): def transform_template(self): transform_template(self.sub_input_file_path, self.output_file_path) + + def get_region(self): + return self.my_region def get_s3_uri(self, file_name): """ @@ -141,26 +158,40 @@ def get_code_key_s3_uri(self, code_key): """ return FILE_TO_S3_URL_MAP[CODE_KEY_TO_FILE_MAP[code_key]] + def get_stack_resources(self, resource_type, stack_resources=None): + if not stack_resources: + stack_resources = self.stack_resources + + resources = [] + for res in stack_resources["StackResourceSummaries"]: + if res["ResourceType"] == resource_type: + resources.append(res) + + return resources + def get_stack_deployment_ids(self): + resources = self.get_stack_resources("AWS::ApiGateway::Deployment") ids = [] - if not self.stack_resources: - return ids - - for res in self.stack_resources["StackResourceSummaries"]: - if res["ResourceType"] == "AWS::ApiGateway::Deployment": - ids.append(res["LogicalResourceId"]) + for res in resources: + ids.append(res["LogicalResourceId"]) return ids def get_stack_stages(self): - if not self.stack_resources: + resources = self.get_stack_resources("AWS::ApiGateway::RestApi") + + if not resources: return [] + + return self.api_client.get_stages(restApiId=resources[0]["PhysicalResourceId"])["item"] + + def get_stack_nested_stack_resources(self): + resources = self.get_stack_resources("AWS::CloudFormation::Stack") - for res in self.stack_resources["StackResourceSummaries"]: - if res["ResourceType"] == "AWS::ApiGateway::RestApi": - return self.api_client.get_stages(restApiId=res["PhysicalResourceId"])["item"] + if not resources: + return None - return [] + return self.cloudformation_client.list_stack_resources(StackName=resources[0]["PhysicalResourceId"]) def _fill_template(self, file_name): """ diff --git a/tests_integ/helpers/file_resources.py b/tests_integ/helpers/file_resources.py index 400b94e03..840011968 100644 --- a/tests_integ/helpers/file_resources.py +++ b/tests_integ/helpers/file_resources.py @@ -2,11 +2,13 @@ "code.zip": "", "layer1.zip": "", "swagger1.json": "", - "swagger2.json": "" + "swagger2.json": "", + "template.yaml": "" } CODE_KEY_TO_FILE_MAP = { "codeuri": "code.zip", "contenturi": "layer1.zip", - "definitionuri": "swagger1.json" + "definitionuri": "swagger1.json", + "templateurl": "template.yaml" } diff --git a/tests_integ/resources/code/template.yaml b/tests_integ/resources/code/template.yaml new file mode 100644 index 000000000..1949e8ba6 --- /dev/null +++ b/tests_integ/resources/code/template.yaml @@ -0,0 +1,8 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Resources: + MyTable: + Type: 'AWS::Serverless::SimpleTable' +Outputs: + TableName: + Value: !Ref MyTable \ No newline at end of file diff --git a/tests_integ/resources/expected/single/basic_application_s3_location.json b/tests_integ/resources/expected/single/basic_application_s3_location.json new file mode 100644 index 000000000..a033c90b7 --- /dev/null +++ b/tests_integ/resources/expected/single/basic_application_s3_location.json @@ -0,0 +1,3 @@ +[ + { "LogicalResourceId":"MyNestedApp", "ResourceType":"AWS::CloudFormation::Stack" } +] diff --git a/tests_integ/resources/templates/single/basic_api_with_cache.yaml b/tests_integ/resources/templates/single/basic_api_with_cache.yaml deleted file mode 100644 index be6779f9e..000000000 --- a/tests_integ/resources/templates/single/basic_api_with_cache.yaml +++ /dev/null @@ -1,12 +0,0 @@ -Resources: - MyApi: - Type: AWS::Serverless::Api - Properties: - StageName: Prod - DefinitionUri: ${definitionuri} - CacheClusterEnabled: true - CacheClusterSize: '1.6' - Variables: - a: foo - b: bar - diff --git a/tests_integ/single/test_basic_application.py b/tests_integ/single/test_basic_application.py index 44213f8e6..0feb7b93e 100644 --- a/tests_integ/single/test_basic_application.py +++ b/tests_integ/single/test_basic_application.py @@ -3,11 +3,30 @@ class TestBasicApplication(BaseTest): - @parameterized.expand( - [ - "basic_application_sar_location", - "basic_application_sar_location_with_intrinsics", - ] - ) - def test_basic_application(self, file_name): - self.create_and_verify_stack(file_name) + def test_basic_application_s3_location(self): + self.create_and_verify_stack("basic_application_s3_location") + + nested_stack_resource = self.get_stack_nested_stack_resources() + tables = self.get_stack_resources("AWS::DynamoDB::Table", nested_stack_resource) + + self.assertEqual(len(tables), 1) + self.assertEqual(tables[0]["LogicalResourceId"], "MyTable") + + def test_basic_application_sar_location(self): + self.create_and_verify_stack("basic_application_sar_location") + + nested_stack_resource = self.get_stack_nested_stack_resources() + functions = self.get_stack_resources("AWS::Lambda::Function", nested_stack_resource) + + self.assertEqual(len(functions), 1) + self.assertEqual(functions[0]["LogicalResourceId"], "helloworldpython") + + def test_basic_application_sar_location_with_intrinsics(self): + expected_function_name = "helloworldpython" if self.get_region() == "us-east-1" else "helloworldpython3" + self.create_and_verify_stack("basic_application_sar_location_with_intrinsics") + + nested_stack_resource = self.get_stack_nested_stack_resources() + functions = self.get_stack_resources("AWS::Lambda::Function", nested_stack_resource) + + self.assertEqual(len(functions), 1) + self.assertEqual(functions[0]["LogicalResourceId"], expected_function_name) From 0fac1bc173056eea6f820f9105cf5cdf6e0f0172 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Thu, 19 Nov 2020 14:33:01 -0800 Subject: [PATCH 054/105] feat: Migrated test_basic_http_api --- tests_integ/helpers/base_test.py | 11 ++++++++++- tests_integ/single/test_basic_http_api.py | 14 +++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 3f240ece7..c24bd9a62 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -30,6 +30,7 @@ def setUpClass(cls): cls.my_region = cls.session.region_name cls.s3_client = boto3.client("s3") cls.api_client = boto3.client('apigateway', cls.my_region) + cls.api_v2_client = boto3.client('apigatewayv2') if not os.path.exists(cls.output_dir): os.mkdir(cls.output_dir) @@ -182,9 +183,17 @@ def get_stack_stages(self): if not resources: return [] - + return self.api_client.get_stages(restApiId=resources[0]["PhysicalResourceId"])["item"] + def get_stack_stages_v2(self): + resources = self.get_stack_resources("AWS::ApiGatewayV2::Api") + + if not resources: + return None + + return self.api_v2_client.get_stages(ApiId=resources[0]["PhysicalResourceId"])["Items"] + def get_stack_nested_stack_resources(self): resources = self.get_stack_resources("AWS::CloudFormation::Stack") diff --git a/tests_integ/single/test_basic_http_api.py b/tests_integ/single/test_basic_http_api.py index 31a31fcb9..77bd6c85e 100644 --- a/tests_integ/single/test_basic_http_api.py +++ b/tests_integ/single/test_basic_http_api.py @@ -3,10 +3,10 @@ class TestBasicHttpApi(BaseTest): - @parameterized.expand( - [ - "basic_http_api", - ] - ) - def test_basic_http_api(self, file_name): - self.create_and_verify_stack(file_name) + def test_basic_http_api(self): + self.create_and_verify_stack("basic_http_api") + + stages = self.get_stack_stages_v2() + + self.assertEqual(len(stages), 1) + self.assertEqual(stages[0]["StageName"], "$default") From 1bdb60b831a6937f4fb292939896dfa1052fb294 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Thu, 19 Nov 2020 17:49:29 -0800 Subject: [PATCH 055/105] feat: Migrated basic_state_machine tests --- tests_integ/helpers/base_test.py | 11 ++++++++ .../single/basic_state_machine_with_tags.json | 4 +++ .../single/test_basic_state_machine.py | 25 +++++++++++++------ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index c24bd9a62..d4fe54405 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -31,6 +31,7 @@ def setUpClass(cls): cls.s3_client = boto3.client("s3") cls.api_client = boto3.client('apigateway', cls.my_region) cls.api_v2_client = boto3.client('apigatewayv2') + cls.sfn_client = boto3.client('stepfunctions') if not os.path.exists(cls.output_dir): os.mkdir(cls.output_dir) @@ -169,6 +170,16 @@ def get_stack_resources(self, resource_type, stack_resources=None): resources.append(res) return resources + + def get_stack_output(self, output_key): + for output in self.stack_description["Stacks"][0]["Outputs"]: + if output["OutputKey"] == output_key: + return output + return None + + def get_stack_tags(self, output_name): + resource_arn = self.get_stack_output(output_name)["OutputValue"] + return self.sfn_client.list_tags_for_resource(resourceArn=resource_arn)["tags"] def get_stack_deployment_ids(self): resources = self.get_stack_resources("AWS::ApiGateway::Deployment") diff --git a/tests_integ/resources/expected/single/basic_state_machine_with_tags.json b/tests_integ/resources/expected/single/basic_state_machine_with_tags.json index e69de29bb..976394f6b 100644 --- a/tests_integ/resources/expected/single/basic_state_machine_with_tags.json +++ b/tests_integ/resources/expected/single/basic_state_machine_with_tags.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyStateMachine", "ResourceType":"AWS::StepFunctions::StateMachine"}, + { "LogicalResourceId":"MyStateMachineRole", "ResourceType":"AWS::IAM::Role" } +] diff --git a/tests_integ/single/test_basic_state_machine.py b/tests_integ/single/test_basic_state_machine.py index 276445f1b..d6848d1d5 100644 --- a/tests_integ/single/test_basic_state_machine.py +++ b/tests_integ/single/test_basic_state_machine.py @@ -3,11 +3,20 @@ class TestBasicLayerVersion(BaseTest): - @parameterized.expand( - [ - "basic_state_machine_inline_definition", - # ("basic_state_machine_with_tags"), # cannot be translated by sam-tran - ] - ) - def test_basic_state_machine(self, file_name): - self.create_and_verify_stack(file_name) + def test_basic_state_machine_inline_definition(self): + self.create_and_verify_stack("basic_state_machine_inline_definition") + + def test_basic_state_machine_with_tags(self): + self.create_and_verify_stack("basic_state_machine_with_tags") + + tags = self.get_stack_tags("MyStateMachineArn") + + self.assertIsNotNone(tags) + self._verify_tag_presence(tags, "stateMachine:createdBy", "SAM") + self._verify_tag_presence(tags, "TagOne", "ValueOne") + self._verify_tag_presence(tags, "TagTwo", "ValueTwo") + + def _verify_tag_presence(self, tags, key, value): + tag = next(tag for tag in tags if tag["key"] == key) + self.assertIsNotNone(tag) + self.assertEqual(tag["value"], value) From e1bf1f84d5390e1f52c84490c733475d1d961be5 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Thu, 19 Nov 2020 18:25:10 -0800 Subject: [PATCH 056/105] migrate all basic function test --- appveyor.yml | 3 +- tests_integ/helpers/base_test.py | 62 +++++++++- tests_integ/single/test_basic_function.py | 113 +++++++++++++++++- .../single/test_basic_layer_version.py | 26 +++- 4 files changed, 192 insertions(+), 12 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 05d321142..d3e76ee37 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,8 +10,9 @@ environment: build: off + install: - make init - +- make test-integ test_script: - tox diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 56aa988e0..f51cdd77d 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -30,6 +30,8 @@ def setUpClass(cls): cls.my_region = cls.session.region_name cls.s3_client = boto3.client("s3") cls.api_client = boto3.client('apigateway', cls.my_region) + cls.lambda_client = boto3.client('lambda') + cls.iam_client = boto3.client('iam') if not os.path.exists(cls.output_dir): os.mkdir(cls.output_dir) @@ -97,7 +99,7 @@ def tearDown(self): if os.path.exists(self.sub_input_file_path): os.remove(self.sub_input_file_path) - def create_and_verify_stack(self, file_name): + def create_and_verify_stack(self, file_name, parameters=[]): """ Creates the Cloud Formation stack and verifies it against the expected result @@ -106,6 +108,8 @@ def create_and_verify_stack(self, file_name): ---------- file_name : string Template file name + parameters : list + List of parameters """ self.output_file_path = str(Path(self.output_dir, "cfn_" + file_name + ".yaml")) self.expected_resource_path = str(Path(self.expected_dir, file_name + ".json")) @@ -113,7 +117,7 @@ def create_and_verify_stack(self, file_name): self._fill_template(file_name) self.transform_template() - self.deploy_stack() + self.deploy_stack(parameters) self.verify_stack() def transform_template(self): @@ -162,6 +166,56 @@ def get_stack_stages(self): return [] + def get_stack_outputs(self): + if not self.stack_description: + return {} + else: + output_key_to_value = {} + output_list = self.stack_description["Outputs"] + for output in output_list: + output_key_to_value[output["OutputKey"]] = output["OutputValue"] + return output_key_to_value + + def get_resource_status_by_logical_id(self, logical_id): + if not self.stack_resources: + return "" + + for res in self.stack_resources["StackResourceSummaries"]: + if res["LogicalResourceId"] == logical_id: + return res["ResourceStatus"] + + return "" + + def get_physical_id_by_type(self, resource_type): + if not self.stack_resources: + return "" + + for res in self.stack_resources["StackResourceSummaries"]: + if res["ResourceType"] == resource_type: + return res["PhysicalResourceId"] + + return "" + + def get_logical_id_by_type(self, resource_type): + if not self.stack_resources: + return "" + + for res in self.stack_resources["StackResourceSummaries"]: + if res["ResourceType"] == resource_type: + return res["LogicalResourceId"] + + return "" + + def get_physical_id_by_logical_id(self, logical_id): + if not self.stack_resources: + return "" + + for res in self.stack_resources["StackResourceSummaries"]: + if res["LogicalResourceId"] == logical_id: + return res["PhysicalResourceId"] + + return "" + def _fill_template(self, file_name): """ Replaces the template variables with their value @@ -212,7 +266,7 @@ def get_template_resource_property(self, resource_name, property_name): return yaml_doc['Resources'][resource_name]["Properties"][property_name] - def deploy_stack(self): + def deploy_stack(self, parameters=[]): """ Deploys the current cloud formation stack """ @@ -220,7 +274,7 @@ def deploy_stack(self): result, changeset_type = self.deployer.create_and_wait_for_changeset( stack_name=self.stack_name, cfn_template=cfn_file.read(), - parameter_values=[], + parameter_values=parameters, capabilities=["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"], role_arn=None, notification_arns=[], diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index 6c45ae9a7..efbf4d4e7 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -1,20 +1,121 @@ +import json + from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest class TestBasicFunction(BaseTest): + @parameterized.expand( [ "basic_function", - "basic_function_event_destinations", "basic_function_no_envvar", "basic_function_openapi", - "basic_function_with_kmskeyarn", - "basic_function_with_sns_dlq", - "basic_function_with_sqs_dlq", - "basic_function_with_tags", - # ("basic_function_with_tracing"), # need different set up to create changeset ] ) def test_basic_function(self, file_name): self.create_and_verify_stack(file_name) + + self.set_template_resource_property("MyLambdaFunction", "Timeout", 10) + self.transform_template() + self.deploy_stack() + + self.assertEqual(self.get_resource_status_by_logical_id("MyLambdaFunction"), "UPDATE_COMPLETE") + + @parameterized.expand( + [ + ("basic_function_with_sns_dlq", "sns:Publish"), + ("basic_function_with_sqs_dlq", "sqs:SendMessage"), + ] + ) + def test_basic_function_with_dlq(self, file_name, action): + dlq_policy_name = "DeadLetterQueuePolicy" + self.create_and_verify_stack(file_name) + + lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") + function_configuration = self.lambda_client.get_function_configuration(FunctionName=lambda_function_name) + dlq_arn = function_configuration['DeadLetterConfig']['TargetArn'] + self.assertIsNotNone(dlq_arn, "DLQ Arn should be set") + + role_name = self.get_physical_id_by_type("AWS::IAM::Role") + role_policy_result = self.iam_client.get_role_policy(RoleName=role_name, PolicyName=dlq_policy_name) + statements = role_policy_result["PolicyDocument"]["Statement"] + + self.assertEqual(len(statements), 1, "Only one statement must be in policy") + self.assertEqual(statements[0]['Action'], action) + self.assertEqual(statements[0]['Resource'], dlq_arn) + self.assertEqual(statements[0]['Effect'], "Allow") + + def test_basic_function_with_kms_key_arn(self): + self.create_and_verify_stack("basic_function_with_kmskeyarn") + + lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") + function_configuration = self.lambda_client.get_function_configuration(FunctionName=lambda_function_name) + kms_key_arn = function_configuration["KMSKeyArn"] + + self.assertIsNotNone(kms_key_arn, "Expecting KmsKeyArn to be set.") + + def test_basic_function_with_tags(self): + self.create_and_verify_stack("basic_function_with_tags") + lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") + get_function_result = self.lambda_client.get_function(FunctionName=lambda_function_name) + tags = get_function_result['Tags'] + + self.assertIsNotNone(tags, "Expecting tags on function.") + self.assertTrue("lambda:createdBy" in tags, "Expected 'lambda:CreatedBy' tag key, but not found.") + self.assertEqual("SAM", tags["lambda:createdBy"], "Expected 'SAM' tag value, but not found.") + self.assertTrue("TagKey1" in tags) + self.assertEqual(tags["TagKey1"], "TagValue1") + self.assertTrue("TagKey2" in tags) + self.assertEqual(tags["TagKey2"], "") + + def test_basic_function_event_destinations(self): + self.create_and_verify_stack("basic_function_event_destinations") + + test_function_1 = self.get_physical_id_by_logical_id("MyTestFunction") + test_function_2 = self.get_physical_id_by_logical_id("MyTestFunction2") + + function_invoke_config_result = self.lambda_client.get_function_event_invoke_config(FunctionName=test_function_1, Qualifier="$LATEST") + self.assertIsNotNone(function_invoke_config_result["DestinationConfig"], "Expecting destination config to be set.") + self.assertEqual(int(function_invoke_config_result["MaximumEventAgeInSeconds"]), 70, "MaximumEventAgeInSeconds value is not set or incorrect.") + self.assertEqual(int(function_invoke_config_result["MaximumRetryAttempts"]), 1, "MaximumRetryAttempts value is not set or incorrect.") + + function_invoke_config_result = self.lambda_client.get_function_event_invoke_config(FunctionName=test_function_2, Qualifier="live") + self.assertIsNotNone(function_invoke_config_result["DestinationConfig"], "Expecting destination config to be set.") + self.assertEqual(int(function_invoke_config_result["MaximumEventAgeInSeconds"]), 80, "MaximumEventAgeInSeconds value is not set or incorrect.") + self.assertEqual(int(function_invoke_config_result["MaximumRetryAttempts"]), 2, "MaximumRetryAttempts value is not set or incorrect.") + + def test_basic_function_with_tracing(self): + parameters = [ + { + 'ParameterKey': 'Bucket', + 'ParameterValue': self.s3_bucket_name, + 'UsePreviousValue': False, + 'ResolvedValue': 'string' + }, + { + 'ParameterKey': 'CodeKey', + 'ParameterValue': "code.zip", + 'UsePreviousValue': False, + 'ResolvedValue': 'string' + }, + { + 'ParameterKey': 'SwaggerKey', + 'ParameterValue': "swagger1.json", + 'UsePreviousValue': False, + 'ResolvedValue': 'string' + }, + ] + self.create_and_verify_stack("basic_function_with_tracing", parameters) + + active_tracing_function_id = self.get_physical_id_by_logical_id("ActiveTracingFunction") + pass_through_tracing_function_id = self.get_physical_id_by_logical_id("PassThroughTracingFunction") + + function_configuration_result = self.lambda_client.get_function_configuration(FunctionName=active_tracing_function_id) + self.assertIsNotNone(function_configuration_result["TracingConfig"], "Expecting tracing config to be set.") + self.assertEqual(function_configuration_result["TracingConfig"]["Mode"], "Active", "Expecting tracing config mode to be set to Active.") + + function_configuration_result = self.lambda_client.get_function_configuration(FunctionName=pass_through_tracing_function_id) + self.assertIsNotNone(function_configuration_result["TracingConfig"], "Expecting tracing config to be set.") + self.assertEqual(function_configuration_result["TracingConfig"]["Mode"], "PassThrough", "Expecting tracing config mode to be set to PassThrough.") + diff --git a/tests_integ/single/test_basic_layer_version.py b/tests_integ/single/test_basic_layer_version.py index 0dd958757..c7090f1f0 100644 --- a/tests_integ/single/test_basic_layer_version.py +++ b/tests_integ/single/test_basic_layer_version.py @@ -6,8 +6,32 @@ class TestBasicLayerVersion(BaseTest): @parameterized.expand( [ "basic_layer", - "basic_layer_with_parameters", + # "basic_layer_with_parameters", ] ) def test_basic_layer_version(self, file_name): self.create_and_verify_stack(file_name) + + layer_logical_id_1 = self.get_logical_id_by_type("AWS::Lambda::LayerVersion") + + self.set_template_resource_property("MyLayerVersion", "Description", "A basic layer") + self.transform_template() + self.deploy_stack() + + layer_logical_id_2 = self.get_logical_id_by_type("AWS::Lambda::LayerVersion") + + self.assertFalse(layer_logical_id_1 == layer_logical_id_2) + + # def test_basic_layer_with_parameters(self, file_name): + # self.create_and_verify_stack(file_name) + # + # outputs = self.get_stack_outputs() + # layer_arn = outputs["MyLayerArn"] + # license = outputs["License"] + # layer_name = outputs["LayerName"] + # description = outputs["Description"] + # + # layer_version = self.lambda_client.get_layer_version_by_arn(Arn=layer_arn) + # + # self.lambda_client + From 09e97890f82d74769cfa00ec68d41d6314736583 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Thu, 19 Nov 2020 19:16:29 -0800 Subject: [PATCH 057/105] migrate all basic layer version test --- tests_integ/helpers/base_test.py | 2 +- .../single/test_basic_layer_version.py | 36 +++++++++---------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index f51cdd77d..a4b42b540 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -171,7 +171,7 @@ def get_stack_outputs(self): return {} else: output_key_to_value = {} - output_list = self.stack_description["Outputs"] + output_list = self.stack_description["Stacks"][0]["Outputs"] for output in output_list: output_key_to_value[output["OutputKey"]] = output["OutputValue"] return output_key_to_value diff --git a/tests_integ/single/test_basic_layer_version.py b/tests_integ/single/test_basic_layer_version.py index c7090f1f0..575ca3588 100644 --- a/tests_integ/single/test_basic_layer_version.py +++ b/tests_integ/single/test_basic_layer_version.py @@ -3,14 +3,9 @@ class TestBasicLayerVersion(BaseTest): - @parameterized.expand( - [ - "basic_layer", - # "basic_layer_with_parameters", - ] - ) - def test_basic_layer_version(self, file_name): - self.create_and_verify_stack(file_name) + + def test_basic_layer_version(self): + self.create_and_verify_stack("basic_layer") layer_logical_id_1 = self.get_logical_id_by_type("AWS::Lambda::LayerVersion") @@ -22,16 +17,17 @@ def test_basic_layer_version(self, file_name): self.assertFalse(layer_logical_id_1 == layer_logical_id_2) - # def test_basic_layer_with_parameters(self, file_name): - # self.create_and_verify_stack(file_name) - # - # outputs = self.get_stack_outputs() - # layer_arn = outputs["MyLayerArn"] - # license = outputs["License"] - # layer_name = outputs["LayerName"] - # description = outputs["Description"] - # - # layer_version = self.lambda_client.get_layer_version_by_arn(Arn=layer_arn) - # - # self.lambda_client + def test_basic_layer_with_parameters(self): + self.create_and_verify_stack("basic_layer_with_parameters") + + outputs = self.get_stack_outputs() + layer_arn = outputs["MyLayerArn"] + license = outputs["License"] + layer_name = outputs["LayerName"] + description = outputs["Description"] + + layer_version_result = self.lambda_client.get_layer_version_by_arn(Arn=layer_arn) + + self.assertEqual(layer_version_result["LicenseInfo"], license) + self.assertEqual(layer_version_result["Description"], description) From ba3d54d59d7f1be6e9aca4b04980f23a0358e4be Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 20 Nov 2020 09:59:19 -0800 Subject: [PATCH 058/105] remove useless import and add NoRegionError in create bucket when region is None --- tests_integ/helpers/helpers.py | 6 ++++-- tests_integ/single/test_basic_api.py | 1 - tests_integ/single/test_basic_function.py | 2 -- tests_integ/single/test_basic_layer_version.py | 2 -- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 4ab8f3a19..e4098d233 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -6,7 +6,7 @@ from functools import reduce import boto3 -from botocore.exceptions import ClientError +from botocore.exceptions import ClientError, NoRegionError from samtranslator.model.exceptions import InvalidDocumentException from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader @@ -80,7 +80,9 @@ def create_bucket(bucket_name, region=None): """ # Create bucket - if region is None or region == 'us-east-1': + if region is None: + raise NoRegionError() + elif region == 'us-east-1': s3_client = boto3.client("s3") s3_client.create_bucket(Bucket=bucket_name) else: diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py index 98c5eff62..306dc6f3d 100644 --- a/tests_integ/single/test_basic_api.py +++ b/tests_integ/single/test_basic_api.py @@ -1,4 +1,3 @@ -from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index efbf4d4e7..9740787b0 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -1,5 +1,3 @@ -import json - from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest diff --git a/tests_integ/single/test_basic_layer_version.py b/tests_integ/single/test_basic_layer_version.py index 575ca3588..0f82f3807 100644 --- a/tests_integ/single/test_basic_layer_version.py +++ b/tests_integ/single/test_basic_layer_version.py @@ -1,4 +1,3 @@ -from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest @@ -23,7 +22,6 @@ def test_basic_layer_with_parameters(self): outputs = self.get_stack_outputs() layer_arn = outputs["MyLayerArn"] license = outputs["License"] - layer_name = outputs["LayerName"] description = outputs["Description"] layer_version_result = self.lambda_client.get_layer_version_by_arn(Arn=layer_arn) From 9a3b154006cff2fe4a8d93b7e643e6015c15fb05 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Fri, 20 Nov 2020 10:24:57 -0800 Subject: [PATCH 059/105] refactor: Added url type to FILE_TO_S3_URL_MAP so we don't rely on the file name to decide the type of S3 URL to generate. Changed "r+" file openings to "r" --- tests_integ/helpers/base_test.py | 16 ++++++++-------- tests_integ/helpers/file_resources.py | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 16f3c911d..e74f9df81 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -78,21 +78,21 @@ def _upload_resources(cls): current_file_name = "" try: - for file_name, _ in FILE_TO_S3_URL_MAP.items(): + for file_name, file_info in FILE_TO_S3_URL_MAP.items(): current_file_name = file_name code_path = str(Path(cls.code_dir, file_name)) LOG.debug("Uploading file %s to bucket %s", file_name, cls.s3_bucket_name) cls.s3_client.upload_file(code_path, cls.s3_bucket_name, file_name) LOG.debug("File %s uploaded successfully to bucket %s", file_name, cls.s3_bucket_name) - FILE_TO_S3_URL_MAP[file_name] = cls._get_s3_url(file_name) + file_info["url"] = cls._get_s3_url(file_name, file_info["type"]) except ClientError as error: LOG.error("Upload of file %s to bucket %s failed", current_file_name, cls.s3_bucket_name, exc_info=error) cls._clean_bucket() raise error @classmethod - def _get_s3_url(cls, file_name): - if file_name != "template.yaml": + def _get_s3_url(cls, file_name, url_type): + if url_type == "s3": return f"s3://{cls.s3_bucket_name}/{file_name}" if cls.my_region == "us-east-1": @@ -151,7 +151,7 @@ def get_s3_uri(self, file_name): file_name : string Resource file name """ - return FILE_TO_S3_URL_MAP[file_name] + return FILE_TO_S3_URL_MAP[file_name]["url"] def get_code_key_s3_uri(self, code_key): """ @@ -162,7 +162,7 @@ def get_code_key_s3_uri(self, code_key): code_key : string Template code key """ - return FILE_TO_S3_URL_MAP[CODE_KEY_TO_FILE_MAP[code_key]] + return FILE_TO_S3_URL_MAP[CODE_KEY_TO_FILE_MAP[code_key]]["url"] def get_stack_resources(self, resource_type, stack_resources=None): if not stack_resources: @@ -302,7 +302,7 @@ def set_template_resource_property(self, resource_name, property_name, value): value value """ - with open(self.sub_input_file_path, "r+") as f: + with open(self.sub_input_file_path, "r") as f: data = f.read() yaml_doc = yaml.load(data, Loader=yaml.FullLoader) yaml_doc['Resources'][resource_name]["Properties"][property_name] = value @@ -311,7 +311,7 @@ def set_template_resource_property(self, resource_name, property_name, value): yaml.dump(yaml_doc, f) def get_template_resource_property(self, resource_name, property_name): - with open(self.sub_input_file_path, "r+") as f: + with open(self.sub_input_file_path, "r") as f: data = f.read() yaml_doc = yaml.load(data, Loader=yaml.FullLoader) diff --git a/tests_integ/helpers/file_resources.py b/tests_integ/helpers/file_resources.py index 840011968..d4e72a27c 100644 --- a/tests_integ/helpers/file_resources.py +++ b/tests_integ/helpers/file_resources.py @@ -1,9 +1,9 @@ FILE_TO_S3_URL_MAP = { - "code.zip": "", - "layer1.zip": "", - "swagger1.json": "", - "swagger2.json": "", - "template.yaml": "" + "code.zip": { "type": "s3", "url": "" }, + "layer1.zip": { "type": "s3", "url": "" }, + "swagger1.json": { "type": "s3", "url": "" }, + "swagger2.json": { "type": "s3", "url": "" }, + "template.yaml": { "type": "http", "url": "" } } CODE_KEY_TO_FILE_MAP = { From 30e841e7eaa4c4fdb66c0047906e843b88c5dde2 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Fri, 20 Nov 2020 13:39:49 -0800 Subject: [PATCH 060/105] refactor: Removed unnecessary parameter --- tests_integ/single/test_basic_api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py index 306dc6f3d..998dfc04a 100644 --- a/tests_integ/single/test_basic_api.py +++ b/tests_integ/single/test_basic_api.py @@ -57,8 +57,7 @@ def test_basic_api_with_tags(self): stages = self.get_stack_stages() self.assertEqual(len(stages), 2) - stage = next((s for s in stages if s["stageName"] == "my-new-stage-name"), None) + stage = next((s for s in stages if s["stageName"] == "my-new-stage-name")) self.assertIsNotNone(stage) self.assertEqual(stage["tags"]["TagKey1"], "TagValue1") self.assertEqual(stage["tags"]["TagKey2"], "") - From 4eaaed00a4915f57c777a3728a1168230693097f Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Fri, 20 Nov 2020 14:27:14 -0800 Subject: [PATCH 061/105] docs: Added docstrings for basic tests --- tests_integ/single/test_basic_api.py | 15 +++++++++++++ tests_integ/single/test_basic_application.py | 13 ++++++++++++ tests_integ/single/test_basic_http_api.py | 6 ++++++ .../single/test_basic_state_machine.py | 21 +++++++++++++++++++ 4 files changed, 55 insertions(+) diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py index 998dfc04a..0c724c43d 100644 --- a/tests_integ/single/test_basic_api.py +++ b/tests_integ/single/test_basic_api.py @@ -2,7 +2,13 @@ class TestBasicApi(BaseTest): + """ + Basic AWS::Serverless::Api tests + """ def test_basic_api(self): + """ + Creates an API and updates its DefinitionUri + """ self.create_and_verify_stack("basic_api") first_dep_ids = self.get_stack_deployment_ids() @@ -18,6 +24,9 @@ def test_basic_api(self): self.assertEqual(len(set(first_dep_ids).intersection(second_dep_ids)), 0) def test_basic_api_inline_openapi(self): + """ + Creates an API with and inline OpenAPI and updates its DefinitionBody basePath + """ self.create_and_verify_stack("basic_api_inline_openapi") first_dep_ids = self.get_stack_deployment_ids() @@ -35,6 +44,9 @@ def test_basic_api_inline_openapi(self): self.assertEqual(len(set(first_dep_ids).intersection(second_dep_ids)), 0) def test_basic_api_inline_swagger(self): + """ + Creates an API with an inline Swagger and updates its DefinitionBody basePath + """ self.create_and_verify_stack("basic_api_inline_swagger") first_dep_ids = self.get_stack_deployment_ids() @@ -52,6 +64,9 @@ def test_basic_api_inline_swagger(self): self.assertEqual(len(set(first_dep_ids).intersection(second_dep_ids)), 0) def test_basic_api_with_tags(self): + """ + Creates an API with tags + """ self.create_and_verify_stack("basic_api_with_tags") stages = self.get_stack_stages() diff --git a/tests_integ/single/test_basic_application.py b/tests_integ/single/test_basic_application.py index 0feb7b93e..8097722b6 100644 --- a/tests_integ/single/test_basic_application.py +++ b/tests_integ/single/test_basic_application.py @@ -3,7 +3,14 @@ class TestBasicApplication(BaseTest): + """ + Basic AWS::Serverless::Application tests + """ def test_basic_application_s3_location(self): + """ + Creates an application with its properties defined as a template + file in a S3 bucket + """ self.create_and_verify_stack("basic_application_s3_location") nested_stack_resource = self.get_stack_nested_stack_resources() @@ -13,6 +20,9 @@ def test_basic_application_s3_location(self): self.assertEqual(tables[0]["LogicalResourceId"], "MyTable") def test_basic_application_sar_location(self): + """ + Creates an application with a lamda function + """ self.create_and_verify_stack("basic_application_sar_location") nested_stack_resource = self.get_stack_nested_stack_resources() @@ -22,6 +32,9 @@ def test_basic_application_sar_location(self): self.assertEqual(functions[0]["LogicalResourceId"], "helloworldpython") def test_basic_application_sar_location_with_intrinsics(self): + """ + Creates an application with a lambda function with intrinsics + """ expected_function_name = "helloworldpython" if self.get_region() == "us-east-1" else "helloworldpython3" self.create_and_verify_stack("basic_application_sar_location_with_intrinsics") diff --git a/tests_integ/single/test_basic_http_api.py b/tests_integ/single/test_basic_http_api.py index 77bd6c85e..0da29becb 100644 --- a/tests_integ/single/test_basic_http_api.py +++ b/tests_integ/single/test_basic_http_api.py @@ -3,7 +3,13 @@ class TestBasicHttpApi(BaseTest): + """ + Basic AWS::Serverless::HttpApi tests + """ def test_basic_http_api(self): + """ + Creates a HTTP API + """ self.create_and_verify_stack("basic_http_api") stages = self.get_stack_stages_v2() diff --git a/tests_integ/single/test_basic_state_machine.py b/tests_integ/single/test_basic_state_machine.py index d6848d1d5..4cc386e82 100644 --- a/tests_integ/single/test_basic_state_machine.py +++ b/tests_integ/single/test_basic_state_machine.py @@ -3,10 +3,19 @@ class TestBasicLayerVersion(BaseTest): + """ + Basic AWS::Serverless::StateMachine tests + """ def test_basic_state_machine_inline_definition(self): + """ + Creates a State Machine from inline definition + """ self.create_and_verify_stack("basic_state_machine_inline_definition") def test_basic_state_machine_with_tags(self): + """ + Creates a State Machine with tags + """ self.create_and_verify_stack("basic_state_machine_with_tags") tags = self.get_stack_tags("MyStateMachineArn") @@ -17,6 +26,18 @@ def test_basic_state_machine_with_tags(self): self._verify_tag_presence(tags, "TagTwo", "ValueTwo") def _verify_tag_presence(self, tags, key, value): + """ + Verifies the presence of a tag and its value + + Parameters + ---------- + tags : List of dict + List of tag objects + key : string + Tag key + value : string + Tag value + """ tag = next(tag for tag in tags if tag["key"] == key) self.assertIsNotNone(tag) self.assertEqual(tag["value"], value) From 2e14e38aafedbc3920ab3fd448aa235c3f8e6db8 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 20 Nov 2020 14:43:03 -0800 Subject: [PATCH 062/105] remove unused import and update layer version test with parameters --- tests_integ/single/test_basic_application.py | 1 - tests_integ/single/test_basic_function.py | 22 ++++++++++++++++++- tests_integ/single/test_basic_http_api.py | 1 - .../single/test_basic_layer_version.py | 12 +++++++++- .../single/test_basic_state_machine.py | 1 - 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/tests_integ/single/test_basic_application.py b/tests_integ/single/test_basic_application.py index 8097722b6..2a42e0799 100644 --- a/tests_integ/single/test_basic_application.py +++ b/tests_integ/single/test_basic_application.py @@ -1,4 +1,3 @@ -from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index 9740787b0..4529655e5 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -3,7 +3,9 @@ class TestBasicFunction(BaseTest): - + """ + Basic AWS::Lambda::Function tests + """ @parameterized.expand( [ "basic_function", @@ -12,6 +14,9 @@ class TestBasicFunction(BaseTest): ] ) def test_basic_function(self, file_name): + """ + Creates a basic lambda function + """ self.create_and_verify_stack(file_name) self.set_template_resource_property("MyLambdaFunction", "Timeout", 10) @@ -27,6 +32,9 @@ def test_basic_function(self, file_name): ] ) def test_basic_function_with_dlq(self, file_name, action): + """ + Creates a basic lambda function with dead letter queue policy + """ dlq_policy_name = "DeadLetterQueuePolicy" self.create_and_verify_stack(file_name) @@ -45,6 +53,9 @@ def test_basic_function_with_dlq(self, file_name, action): self.assertEqual(statements[0]['Effect'], "Allow") def test_basic_function_with_kms_key_arn(self): + """ + Creates a basic lambda function with KMS key arn + """ self.create_and_verify_stack("basic_function_with_kmskeyarn") lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") @@ -54,6 +65,9 @@ def test_basic_function_with_kms_key_arn(self): self.assertIsNotNone(kms_key_arn, "Expecting KmsKeyArn to be set.") def test_basic_function_with_tags(self): + """ + Creates a basic lambda function with tags + """ self.create_and_verify_stack("basic_function_with_tags") lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") get_function_result = self.lambda_client.get_function(FunctionName=lambda_function_name) @@ -68,6 +82,9 @@ def test_basic_function_with_tags(self): self.assertEqual(tags["TagKey2"], "") def test_basic_function_event_destinations(self): + """ + Creates a basic lambda function with event destinations + """ self.create_and_verify_stack("basic_function_event_destinations") test_function_1 = self.get_physical_id_by_logical_id("MyTestFunction") @@ -84,6 +101,9 @@ def test_basic_function_event_destinations(self): self.assertEqual(int(function_invoke_config_result["MaximumRetryAttempts"]), 2, "MaximumRetryAttempts value is not set or incorrect.") def test_basic_function_with_tracing(self): + """ + Creates a basic lambda function with tracing + """ parameters = [ { 'ParameterKey': 'Bucket', diff --git a/tests_integ/single/test_basic_http_api.py b/tests_integ/single/test_basic_http_api.py index 0da29becb..5790def89 100644 --- a/tests_integ/single/test_basic_http_api.py +++ b/tests_integ/single/test_basic_http_api.py @@ -1,4 +1,3 @@ -from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest diff --git a/tests_integ/single/test_basic_layer_version.py b/tests_integ/single/test_basic_layer_version.py index 0f82f3807..9ba2d26d6 100644 --- a/tests_integ/single/test_basic_layer_version.py +++ b/tests_integ/single/test_basic_layer_version.py @@ -2,8 +2,13 @@ class TestBasicLayerVersion(BaseTest): - + """ + Basic AWS::Lambda::LayerVersion tests + """ def test_basic_layer_version(self): + """ + Creates a basic lambda layer version + """ self.create_and_verify_stack("basic_layer") layer_logical_id_1 = self.get_logical_id_by_type("AWS::Lambda::LayerVersion") @@ -17,14 +22,19 @@ def test_basic_layer_version(self): self.assertFalse(layer_logical_id_1 == layer_logical_id_2) def test_basic_layer_with_parameters(self): + """ + Creates a basic lambda layer version with parameters + """ self.create_and_verify_stack("basic_layer_with_parameters") outputs = self.get_stack_outputs() layer_arn = outputs["MyLayerArn"] license = outputs["License"] + layer_name = outputs["LayerName"] description = outputs["Description"] layer_version_result = self.lambda_client.get_layer_version_by_arn(Arn=layer_arn) + self.lambda_client.delete_layer_version(LayerName=layer_name, VersionNumber=layer_version_result["Version"]) self.assertEqual(layer_version_result["LicenseInfo"], license) self.assertEqual(layer_version_result["Description"], description) diff --git a/tests_integ/single/test_basic_state_machine.py b/tests_integ/single/test_basic_state_machine.py index 4cc386e82..66ff8a160 100644 --- a/tests_integ/single/test_basic_state_machine.py +++ b/tests_integ/single/test_basic_state_machine.py @@ -1,4 +1,3 @@ -from parameterized import parameterized from tests_integ.helpers.base_test import BaseTest From 7c91bb0c934551108ace0f364594364675093f65 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 20 Nov 2020 15:01:00 -0800 Subject: [PATCH 063/105] black reformating --- tests_integ/helpers/base_test.py | 25 ++-- tests_integ/helpers/file_resources.py | 12 +- tests_integ/helpers/helpers.py | 2 +- tests_integ/single/test_basic_api.py | 1 + tests_integ/single/test_basic_application.py | 5 +- tests_integ/single/test_basic_function.py | 114 ++++++++++++------ tests_integ/single/test_basic_http_api.py | 1 + .../single/test_basic_layer_version.py | 2 +- .../single/test_basic_state_machine.py | 1 + 9 files changed, 100 insertions(+), 63 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index e74f9df81..0e0b5f164 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -29,11 +29,11 @@ def setUpClass(cls): cls.session = boto3.session.Session() cls.my_region = cls.session.region_name cls.s3_client = boto3.client("s3") - cls.api_client = boto3.client('apigateway', cls.my_region) - cls.lambda_client = boto3.client('lambda') - cls.iam_client = boto3.client('iam') - cls.api_v2_client = boto3.client('apigatewayv2') - cls.sfn_client = boto3.client('stepfunctions') + cls.api_client = boto3.client("apigateway", cls.my_region) + cls.lambda_client = boto3.client("lambda") + cls.iam_client = boto3.client("iam") + cls.api_v2_client = boto3.client("apigatewayv2") + cls.sfn_client = boto3.client("stepfunctions") if not os.path.exists(cls.output_dir): os.mkdir(cls.output_dir) @@ -49,7 +49,7 @@ def _clean_bucket(cls): """ Empties and deletes the bucket used for the tests """ - s3 = boto3.resource('s3') + s3 = boto3.resource("s3") bucket = s3.Bucket(cls.s3_bucket_name) object_summary_iterator = bucket.objects.all() @@ -58,10 +58,7 @@ def _clean_bucket(cls): cls.s3_client.delete_object(Key=object_summary.key, Bucket=cls.s3_bucket_name) except ClientError as e: LOG.error( - "Unable to delete object %s from bucket %s", - object_summary.key, - cls.s3_bucket_name, - exc_info=e + "Unable to delete object %s from bucket %s", object_summary.key, cls.s3_bucket_name, exc_info=e ) try: cls.s3_client.delete_bucket(Bucket=cls.s3_bucket_name) @@ -138,7 +135,7 @@ def create_and_verify_stack(self, file_name, parameters=[]): def transform_template(self): transform_template(self.sub_input_file_path, self.output_file_path) - + def get_region(self): return self.my_region @@ -174,7 +171,7 @@ def get_stack_resources(self, resource_type, stack_resources=None): resources.append(res) return resources - + def get_stack_output(self, output_key): for output in self.stack_description["Stacks"][0]["Outputs"]: if output["OutputKey"] == output_key: @@ -305,7 +302,7 @@ def set_template_resource_property(self, resource_name, property_name, value): with open(self.sub_input_file_path, "r") as f: data = f.read() yaml_doc = yaml.load(data, Loader=yaml.FullLoader) - yaml_doc['Resources'][resource_name]["Properties"][property_name] = value + yaml_doc["Resources"][resource_name]["Properties"][property_name] = value with open(self.sub_input_file_path, "w") as f: yaml.dump(yaml_doc, f) @@ -315,7 +312,7 @@ def get_template_resource_property(self, resource_name, property_name): data = f.read() yaml_doc = yaml.load(data, Loader=yaml.FullLoader) - return yaml_doc['Resources'][resource_name]["Properties"][property_name] + return yaml_doc["Resources"][resource_name]["Properties"][property_name] def deploy_stack(self, parameters=[]): """ diff --git a/tests_integ/helpers/file_resources.py b/tests_integ/helpers/file_resources.py index d4e72a27c..ed0bd16f4 100644 --- a/tests_integ/helpers/file_resources.py +++ b/tests_integ/helpers/file_resources.py @@ -1,14 +1,14 @@ FILE_TO_S3_URL_MAP = { - "code.zip": { "type": "s3", "url": "" }, - "layer1.zip": { "type": "s3", "url": "" }, - "swagger1.json": { "type": "s3", "url": "" }, - "swagger2.json": { "type": "s3", "url": "" }, - "template.yaml": { "type": "http", "url": "" } + "code.zip": {"type": "s3", "url": ""}, + "layer1.zip": {"type": "s3", "url": ""}, + "swagger1.json": {"type": "s3", "url": ""}, + "swagger2.json": {"type": "s3", "url": ""}, + "template.yaml": {"type": "http", "url": ""}, } CODE_KEY_TO_FILE_MAP = { "codeuri": "code.zip", "contenturi": "layer1.zip", "definitionuri": "swagger1.json", - "templateurl": "template.yaml" + "templateurl": "template.yaml", } diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index e4098d233..7420bd431 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -82,7 +82,7 @@ def create_bucket(bucket_name, region=None): # Create bucket if region is None: raise NoRegionError() - elif region == 'us-east-1': + elif region == "us-east-1": s3_client = boto3.client("s3") s3_client.create_bucket(Bucket=bucket_name) else: diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py index 0c724c43d..6ad928b26 100644 --- a/tests_integ/single/test_basic_api.py +++ b/tests_integ/single/test_basic_api.py @@ -5,6 +5,7 @@ class TestBasicApi(BaseTest): """ Basic AWS::Serverless::Api tests """ + def test_basic_api(self): """ Creates an API and updates its DefinitionUri diff --git a/tests_integ/single/test_basic_application.py b/tests_integ/single/test_basic_application.py index 2a42e0799..09e4799fa 100644 --- a/tests_integ/single/test_basic_application.py +++ b/tests_integ/single/test_basic_application.py @@ -5,9 +5,10 @@ class TestBasicApplication(BaseTest): """ Basic AWS::Serverless::Application tests """ + def test_basic_application_s3_location(self): """ - Creates an application with its properties defined as a template + Creates an application with its properties defined as a template file in a S3 bucket """ self.create_and_verify_stack("basic_application_s3_location") @@ -32,7 +33,7 @@ def test_basic_application_sar_location(self): def test_basic_application_sar_location_with_intrinsics(self): """ - Creates an application with a lambda function with intrinsics + Creates an application with a lambda function with intrinsics """ expected_function_name = "helloworldpython" if self.get_region() == "us-east-1" else "helloworldpython3" self.create_and_verify_stack("basic_application_sar_location_with_intrinsics") diff --git a/tests_integ/single/test_basic_function.py b/tests_integ/single/test_basic_function.py index 4529655e5..b4463a3b0 100644 --- a/tests_integ/single/test_basic_function.py +++ b/tests_integ/single/test_basic_function.py @@ -6,6 +6,7 @@ class TestBasicFunction(BaseTest): """ Basic AWS::Lambda::Function tests """ + @parameterized.expand( [ "basic_function", @@ -40,7 +41,7 @@ def test_basic_function_with_dlq(self, file_name, action): lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") function_configuration = self.lambda_client.get_function_configuration(FunctionName=lambda_function_name) - dlq_arn = function_configuration['DeadLetterConfig']['TargetArn'] + dlq_arn = function_configuration["DeadLetterConfig"]["TargetArn"] self.assertIsNotNone(dlq_arn, "DLQ Arn should be set") role_name = self.get_physical_id_by_type("AWS::IAM::Role") @@ -48,9 +49,9 @@ def test_basic_function_with_dlq(self, file_name, action): statements = role_policy_result["PolicyDocument"]["Statement"] self.assertEqual(len(statements), 1, "Only one statement must be in policy") - self.assertEqual(statements[0]['Action'], action) - self.assertEqual(statements[0]['Resource'], dlq_arn) - self.assertEqual(statements[0]['Effect'], "Allow") + self.assertEqual(statements[0]["Action"], action) + self.assertEqual(statements[0]["Resource"], dlq_arn) + self.assertEqual(statements[0]["Effect"], "Allow") def test_basic_function_with_kms_key_arn(self): """ @@ -71,7 +72,7 @@ def test_basic_function_with_tags(self): self.create_and_verify_stack("basic_function_with_tags") lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") get_function_result = self.lambda_client.get_function(FunctionName=lambda_function_name) - tags = get_function_result['Tags'] + tags = get_function_result["Tags"] self.assertIsNotNone(tags, "Expecting tags on function.") self.assertTrue("lambda:createdBy" in tags, "Expected 'lambda:CreatedBy' tag key, but not found.") @@ -90,50 +91,85 @@ def test_basic_function_event_destinations(self): test_function_1 = self.get_physical_id_by_logical_id("MyTestFunction") test_function_2 = self.get_physical_id_by_logical_id("MyTestFunction2") - function_invoke_config_result = self.lambda_client.get_function_event_invoke_config(FunctionName=test_function_1, Qualifier="$LATEST") - self.assertIsNotNone(function_invoke_config_result["DestinationConfig"], "Expecting destination config to be set.") - self.assertEqual(int(function_invoke_config_result["MaximumEventAgeInSeconds"]), 70, "MaximumEventAgeInSeconds value is not set or incorrect.") - self.assertEqual(int(function_invoke_config_result["MaximumRetryAttempts"]), 1, "MaximumRetryAttempts value is not set or incorrect.") - - function_invoke_config_result = self.lambda_client.get_function_event_invoke_config(FunctionName=test_function_2, Qualifier="live") - self.assertIsNotNone(function_invoke_config_result["DestinationConfig"], "Expecting destination config to be set.") - self.assertEqual(int(function_invoke_config_result["MaximumEventAgeInSeconds"]), 80, "MaximumEventAgeInSeconds value is not set or incorrect.") - self.assertEqual(int(function_invoke_config_result["MaximumRetryAttempts"]), 2, "MaximumRetryAttempts value is not set or incorrect.") + function_invoke_config_result = self.lambda_client.get_function_event_invoke_config( + FunctionName=test_function_1, Qualifier="$LATEST" + ) + self.assertIsNotNone( + function_invoke_config_result["DestinationConfig"], "Expecting destination config to be set." + ) + self.assertEqual( + int(function_invoke_config_result["MaximumEventAgeInSeconds"]), + 70, + "MaximumEventAgeInSeconds value is not set or incorrect.", + ) + self.assertEqual( + int(function_invoke_config_result["MaximumRetryAttempts"]), + 1, + "MaximumRetryAttempts value is not set or incorrect.", + ) + + function_invoke_config_result = self.lambda_client.get_function_event_invoke_config( + FunctionName=test_function_2, Qualifier="live" + ) + self.assertIsNotNone( + function_invoke_config_result["DestinationConfig"], "Expecting destination config to be set." + ) + self.assertEqual( + int(function_invoke_config_result["MaximumEventAgeInSeconds"]), + 80, + "MaximumEventAgeInSeconds value is not set or incorrect.", + ) + self.assertEqual( + int(function_invoke_config_result["MaximumRetryAttempts"]), + 2, + "MaximumRetryAttempts value is not set or incorrect.", + ) def test_basic_function_with_tracing(self): """ Creates a basic lambda function with tracing """ parameters = [ - { - 'ParameterKey': 'Bucket', - 'ParameterValue': self.s3_bucket_name, - 'UsePreviousValue': False, - 'ResolvedValue': 'string' - }, - { - 'ParameterKey': 'CodeKey', - 'ParameterValue': "code.zip", - 'UsePreviousValue': False, - 'ResolvedValue': 'string' - }, - { - 'ParameterKey': 'SwaggerKey', - 'ParameterValue': "swagger1.json", - 'UsePreviousValue': False, - 'ResolvedValue': 'string' - }, - ] + { + "ParameterKey": "Bucket", + "ParameterValue": self.s3_bucket_name, + "UsePreviousValue": False, + "ResolvedValue": "string", + }, + { + "ParameterKey": "CodeKey", + "ParameterValue": "code.zip", + "UsePreviousValue": False, + "ResolvedValue": "string", + }, + { + "ParameterKey": "SwaggerKey", + "ParameterValue": "swagger1.json", + "UsePreviousValue": False, + "ResolvedValue": "string", + }, + ] self.create_and_verify_stack("basic_function_with_tracing", parameters) active_tracing_function_id = self.get_physical_id_by_logical_id("ActiveTracingFunction") pass_through_tracing_function_id = self.get_physical_id_by_logical_id("PassThroughTracingFunction") - function_configuration_result = self.lambda_client.get_function_configuration(FunctionName=active_tracing_function_id) + function_configuration_result = self.lambda_client.get_function_configuration( + FunctionName=active_tracing_function_id + ) self.assertIsNotNone(function_configuration_result["TracingConfig"], "Expecting tracing config to be set.") - self.assertEqual(function_configuration_result["TracingConfig"]["Mode"], "Active", "Expecting tracing config mode to be set to Active.") - - function_configuration_result = self.lambda_client.get_function_configuration(FunctionName=pass_through_tracing_function_id) + self.assertEqual( + function_configuration_result["TracingConfig"]["Mode"], + "Active", + "Expecting tracing config mode to be set to Active.", + ) + + function_configuration_result = self.lambda_client.get_function_configuration( + FunctionName=pass_through_tracing_function_id + ) self.assertIsNotNone(function_configuration_result["TracingConfig"], "Expecting tracing config to be set.") - self.assertEqual(function_configuration_result["TracingConfig"]["Mode"], "PassThrough", "Expecting tracing config mode to be set to PassThrough.") - + self.assertEqual( + function_configuration_result["TracingConfig"]["Mode"], + "PassThrough", + "Expecting tracing config mode to be set to PassThrough.", + ) diff --git a/tests_integ/single/test_basic_http_api.py b/tests_integ/single/test_basic_http_api.py index 5790def89..3530e9a69 100644 --- a/tests_integ/single/test_basic_http_api.py +++ b/tests_integ/single/test_basic_http_api.py @@ -5,6 +5,7 @@ class TestBasicHttpApi(BaseTest): """ Basic AWS::Serverless::HttpApi tests """ + def test_basic_http_api(self): """ Creates a HTTP API diff --git a/tests_integ/single/test_basic_layer_version.py b/tests_integ/single/test_basic_layer_version.py index 9ba2d26d6..0912bec77 100644 --- a/tests_integ/single/test_basic_layer_version.py +++ b/tests_integ/single/test_basic_layer_version.py @@ -5,6 +5,7 @@ class TestBasicLayerVersion(BaseTest): """ Basic AWS::Lambda::LayerVersion tests """ + def test_basic_layer_version(self): """ Creates a basic lambda layer version @@ -38,4 +39,3 @@ def test_basic_layer_with_parameters(self): self.assertEqual(layer_version_result["LicenseInfo"], license) self.assertEqual(layer_version_result["Description"], description) - diff --git a/tests_integ/single/test_basic_state_machine.py b/tests_integ/single/test_basic_state_machine.py index 66ff8a160..b78ac80a6 100644 --- a/tests_integ/single/test_basic_state_machine.py +++ b/tests_integ/single/test_basic_state_machine.py @@ -5,6 +5,7 @@ class TestBasicLayerVersion(BaseTest): """ Basic AWS::Serverless::StateMachine tests """ + def test_basic_state_machine_inline_definition(self): """ Creates a State Machine from inline definition From 20e3cfd9757d1752e46ea8270a9e46e5c012ff28 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 20 Nov 2020 15:20:28 -0800 Subject: [PATCH 064/105] update appveyor.yml file --- appveyor.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index d3e76ee37..9881fe407 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,15 +4,21 @@ image: Ubuntu environment: matrix: - TOXENV: py27 + PYTHON_VERSION: '2.7' - TOXENV: py36 + PYTHON_VERSION: '3.6' - TOXENV: py37 + PYTHON_VERSION: '3.7' - TOXENV: py38 + PYTHON_VERSION: '3.8' build: off - install: +- sh: "source ${HOME}/venv${PYTHON_VERSION}/bin/activate" +- sh: "python --version" - make init -- make test-integ + test_script: - tox + From 4091d3fdec38322dee0ff35ccec43040ad483de4 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Fri, 20 Nov 2020 15:24:14 -0800 Subject: [PATCH 065/105] fix: Changed string formatting incompatible with Python 2 in base_test, added aws-sam-cli as a dev dependency --- requirements/dev.txt | 1 + tests_integ/helpers/base_test.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index a08e446e0..0ad2b8e0d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -10,6 +10,7 @@ pytest~=6.1.1; python_version >= '3.6' pytest~=4.6.11; python_version < '3.6' # pytest dropped python 2 support after 4.6.x mock>=3.0.5,<4.0.0 # 4.0.0 drops Python 2 support parameterized~=0.7.4 +aws-sam-cli>=1.11.0 # Requirements for examples requests~=2.24.0 diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 0e0b5f164..be88c50e3 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -90,16 +90,16 @@ def _upload_resources(cls): @classmethod def _get_s3_url(cls, file_name, url_type): if url_type == "s3": - return f"s3://{cls.s3_bucket_name}/{file_name}" + return "s3://{}/{}".format(cls.s3_bucket_name, file_name) if cls.my_region == "us-east-1": - return f"https://s3.amazonaws.com/{cls.s3_bucket_name}/{file_name}" + return "https://s3.amazonaws.com/{}/{}".format(cls.s3_bucket_name, file_name) if cls.my_region == "us-iso-east-1": - return f"https://s3.us-iso-east-1.c2s.ic.gov/{cls.s3_bucket_name}/{file_name}" + return "https://s3.us-iso-east-1.c2s.ic.gov/{}/{}".format(cls.s3_bucket_name, file_name) if cls.my_region == "us-isob-east-1": - return f"https://s3.us-isob-east-1.sc2s.sgov.gov/{cls.s3_bucket_name}/{file_name}" + return "https://s3.us-isob-east-1.sc2s.sgov.gov/{}/{}".format(cls.s3_bucket_name, file_name) - return f"https://s3-{cls.my_region}.amazonaws.com/{cls.s3_bucket_name}/{file_name}" + return "https://s3-{}.amazonaws.com/{}/{}".format(cls.my_region, cls.s3_bucket_name, file_name) def setUp(self): self.cloudformation_client = boto3.client("cloudformation") From c79c53bbb4a73771b0f74c771d739fb7965144ca Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 20 Nov 2020 15:35:13 -0800 Subject: [PATCH 066/105] add make test-integ in appveyor.yml file --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 9881fe407..420f3190d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,6 +18,7 @@ install: - sh: "source ${HOME}/venv${PYTHON_VERSION}/bin/activate" - sh: "python --version" - make init +- make test-integ test_script: - tox From a2918f6feecb2e1bd07f6c26ad1b49968d810749 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 20 Nov 2020 15:48:06 -0800 Subject: [PATCH 067/105] add default region in appveyor environment --- appveyor.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 420f3190d..667c80a1e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,6 +2,9 @@ version: 1.0.{build} image: Ubuntu environment: + + AWS_DEFAULT_REGION: us-east-1 + matrix: - TOXENV: py27 PYTHON_VERSION: '2.7' From ef7acfeaed5bd617faed22d3df19ac6a52d9c599 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Fri, 20 Nov 2020 16:07:38 -0800 Subject: [PATCH 068/105] fix: Reverting adding aws-sam-cli to dev dependencies, it's for integration tests only --- requirements/dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 0ad2b8e0d..a08e446e0 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -10,7 +10,6 @@ pytest~=6.1.1; python_version >= '3.6' pytest~=4.6.11; python_version < '3.6' # pytest dropped python 2 support after 4.6.x mock>=3.0.5,<4.0.0 # 4.0.0 drops Python 2 support parameterized~=0.7.4 -aws-sam-cli>=1.11.0 # Requirements for examples requests~=2.24.0 From 1c4c1c8451b4cb88278f31b5c6838330b58acfbc Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Fri, 20 Nov 2020 16:45:34 -0800 Subject: [PATCH 069/105] refactor: Changed a remaining python3 string format syntax in _fill_template to python2 --- tests_integ/helpers/base_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index be88c50e3..7bc005f98 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -278,7 +278,7 @@ def _fill_template(self, file_name): with open(input_file_path, "r") as f: data = f.read() for key, _ in CODE_KEY_TO_FILE_MAP.items(): - data = data.replace(f"${{{key}}}", self.get_code_key_s3_uri(key)) + data = data.replace("${{{}}}".format(key), self.get_code_key_s3_uri(key)) yaml_doc = yaml.load(data, Loader=yaml.FullLoader) with open(updated_template_path, "w") as f: From 2857627be912bf519220b67180d36e037294ed4a Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Mon, 23 Nov 2020 09:51:58 -0800 Subject: [PATCH 070/105] refactor: Using URI throughout, yaml load/dump function, [] not used as a default value anymore (dangerous), remove "r" read mode as it is default --- tests_integ/helpers/base_test.py | 84 +++++++++++++++++---------- tests_integ/helpers/file_resources.py | 12 ++-- tests_integ/helpers/helpers.py | 4 +- 3 files changed, 62 insertions(+), 38 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 7bc005f98..0f46cbd5f 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -9,7 +9,7 @@ from botocore.exceptions import ClientError from samcli.lib.deploy.deployer import Deployer from tests_integ.helpers.helpers import transform_template, verify_stack_resources, generate_suffix, create_bucket -from tests_integ.helpers.file_resources import FILE_TO_S3_URL_MAP, CODE_KEY_TO_FILE_MAP +from tests_integ.helpers.file_resources import FILE_TO_S3_URI_MAP, CODE_KEY_TO_FILE_MAP LOG = logging.getLogger(__name__) STACK_NAME_PREFIX = "sam-integ-stack-" @@ -75,21 +75,21 @@ def _upload_resources(cls): current_file_name = "" try: - for file_name, file_info in FILE_TO_S3_URL_MAP.items(): + for file_name, file_info in FILE_TO_S3_URI_MAP.items(): current_file_name = file_name code_path = str(Path(cls.code_dir, file_name)) LOG.debug("Uploading file %s to bucket %s", file_name, cls.s3_bucket_name) cls.s3_client.upload_file(code_path, cls.s3_bucket_name, file_name) LOG.debug("File %s uploaded successfully to bucket %s", file_name, cls.s3_bucket_name) - file_info["url"] = cls._get_s3_url(file_name, file_info["type"]) + file_info["uri"] = cls._get_s3_uri(file_name, file_info["type"]) except ClientError as error: LOG.error("Upload of file %s to bucket %s failed", current_file_name, cls.s3_bucket_name, exc_info=error) cls._clean_bucket() raise error @classmethod - def _get_s3_url(cls, file_name, url_type): - if url_type == "s3": + def _get_s3_uri(cls, file_name, uri_type): + if uri_type == "s3": return "s3://{}/{}".format(cls.s3_bucket_name, file_name) if cls.my_region == "us-east-1": @@ -112,7 +112,7 @@ def tearDown(self): if os.path.exists(self.sub_input_file_path): os.remove(self.sub_input_file_path) - def create_and_verify_stack(self, file_name, parameters=[]): + def create_and_verify_stack(self, file_name, parameters=None): """ Creates the Cloud Formation stack and verifies it against the expected result @@ -148,7 +148,7 @@ def get_s3_uri(self, file_name): file_name : string Resource file name """ - return FILE_TO_S3_URL_MAP[file_name]["url"] + return FILE_TO_S3_URI_MAP[file_name]["uri"] def get_code_key_s3_uri(self, code_key): """ @@ -159,7 +159,7 @@ def get_code_key_s3_uri(self, code_key): code_key : string Template code key """ - return FILE_TO_S3_URL_MAP[CODE_KEY_TO_FILE_MAP[code_key]]["url"] + return FILE_TO_S3_URI_MAP[CODE_KEY_TO_FILE_MAP[code_key]]["uri"] def get_stack_resources(self, resource_type, stack_resources=None): if not stack_resources: @@ -217,12 +217,11 @@ def get_stack_nested_stack_resources(self): def get_stack_outputs(self): if not self.stack_description: return {} - else: - output_key_to_value = {} - output_list = self.stack_description["Stacks"][0]["Outputs"] - for output in output_list: - output_key_to_value[output["OutputKey"]] = output["OutputValue"] - return output_key_to_value + output_list = self.stack_description["Stacks"][0]["Outputs"] + return { + output["OutputKey"]: output["OutputValue"] + for output in output_list + } def get_resource_status_by_logical_id(self, logical_id): if not self.stack_resources: @@ -275,14 +274,14 @@ def _fill_template(self, file_name): """ input_file_path = str(Path(self.template_dir, file_name + ".yaml")) updated_template_path = str(Path(self.output_dir, "sub_" + file_name + ".yaml")) - with open(input_file_path, "r") as f: + with open(input_file_path) as f: data = f.read() for key, _ in CODE_KEY_TO_FILE_MAP.items(): + # We must double the {} to escape them so they will survive a round of unescape data = data.replace("${{{}}}".format(key), self.get_code_key_s3_uri(key)) yaml_doc = yaml.load(data, Loader=yaml.FullLoader) - with open(updated_template_path, "w") as f: - yaml.dump(yaml_doc, f) + self._dump_yaml(updated_template_path, yaml_doc) self.sub_input_file_path = updated_template_path @@ -299,30 +298,23 @@ def set_template_resource_property(self, resource_name, property_name, value): value value """ - with open(self.sub_input_file_path, "r") as f: - data = f.read() - yaml_doc = yaml.load(data, Loader=yaml.FullLoader) + yaml_doc = self._load_yaml(self.sub_input_file_path) yaml_doc["Resources"][resource_name]["Properties"][property_name] = value - - with open(self.sub_input_file_path, "w") as f: - yaml.dump(yaml_doc, f) + self._dump_yaml(self.sub_input_file_path, yaml_doc) def get_template_resource_property(self, resource_name, property_name): - with open(self.sub_input_file_path, "r") as f: - data = f.read() - yaml_doc = yaml.load(data, Loader=yaml.FullLoader) - + yaml_doc = yaml_doc = self._load_yaml(self.sub_input_file_path) return yaml_doc["Resources"][resource_name]["Properties"][property_name] - def deploy_stack(self, parameters=[]): + def deploy_stack(self, parameters=None): """ Deploys the current cloud formation stack """ - with open(self.output_file_path, "r") as cfn_file: + with open(self.output_file_path) as cfn_file: result, changeset_type = self.deployer.create_and_wait_for_changeset( stack_name=self.stack_name, cfn_template=cfn_file.read(), - parameter_values=parameters, + parameter_values=[] if parameters is None else parameters, capabilities=["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"], role_arn=None, notification_arns=[], @@ -343,3 +335,35 @@ def verify_stack(self): self.assertEqual(self.stack_description["Stacks"][0]["StackStatus"], "CREATE_COMPLETE") # verify if the stack contains the expected resources self.assertTrue(verify_stack_resources(self.expected_resource_path, self.stack_resources)) + + def _load_yaml(self, file_path): + """ + Loads a yaml file + + Parameters + ---------- + file_path : Path + File path + + Returns + ------- + Object + Yaml object + """ + with open(file_path) as f: + data = f.read() + return yaml.load(data, Loader=yaml.FullLoader) + + def _dump_yaml(self, file_path, yaml_doc): + """ + Writes a yaml object to a file + + Parameters + ---------- + file_path : Path + File path + yaml_doc : Object + Yaml object + """ + with open(file_path, "w") as f: + yaml.dump(yaml_doc, f) diff --git a/tests_integ/helpers/file_resources.py b/tests_integ/helpers/file_resources.py index ed0bd16f4..eadd53336 100644 --- a/tests_integ/helpers/file_resources.py +++ b/tests_integ/helpers/file_resources.py @@ -1,9 +1,9 @@ -FILE_TO_S3_URL_MAP = { - "code.zip": {"type": "s3", "url": ""}, - "layer1.zip": {"type": "s3", "url": ""}, - "swagger1.json": {"type": "s3", "url": ""}, - "swagger2.json": {"type": "s3", "url": ""}, - "template.yaml": {"type": "http", "url": ""}, +FILE_TO_S3_URI_MAP = { + "code.zip": {"type": "s3", "uri": ""}, + "layer1.zip": {"type": "s3", "uri": ""}, + "swagger1.json": {"type": "s3", "uri": ""}, + "swagger2.json": {"type": "s3", "uri": ""}, + "template.yaml": {"type": "http", "uri": ""}, } CODE_KEY_TO_FILE_MAP = { diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 7420bd431..c170d8943 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -20,7 +20,7 @@ def transform_template(input_file_path, output_file_path): LOG = logging.getLogger(__name__) iam_client = boto3.client("iam") - with open(input_file_path, "r") as f: + with open(input_file_path) as f: sam_template = yaml_parse(f) try: @@ -39,7 +39,7 @@ def transform_template(input_file_path, output_file_path): def verify_stack_resources(expected_file_path, stack_resources): - with open(expected_file_path, "r") as expected_data: + with open(expected_file_path) as expected_data: expected_resources = _sort_resources(json.load(expected_data)) parsed_resources = _sort_resources(stack_resources["StackResourceSummaries"]) From e099b0b7c495fa95efa87de3ed1e5997b20afa39 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Mon, 23 Nov 2020 18:16:29 -0800 Subject: [PATCH 071/105] feat: Added local Deployer class so that we don't depend on sam-cli anymore, all tests now run on python2 and python3 --- requirements/dev.txt | 5 + tests_integ/helpers/base_test.py | 11 +- tests_integ/helpers/deployer/__init__.py | 0 tests_integ/helpers/deployer/deployer.py | 486 ++++++++++++++++++ .../helpers/deployer/exceptions/__init__.py | 0 .../helpers/deployer/exceptions/exceptions.py | 89 ++++ .../helpers/deployer/utils/__init__.py | 0 .../deployer/utils/artifact_exporter.py | 61 +++ tests_integ/helpers/deployer/utils/colors.py | 92 ++++ .../helpers/deployer/utils/table_print.py | 136 +++++ tests_integ/helpers/deployer/utils/time.py | 127 +++++ tests_integ/helpers/helpers.py | 2 +- 12 files changed, 1004 insertions(+), 5 deletions(-) create mode 100644 tests_integ/helpers/deployer/__init__.py create mode 100644 tests_integ/helpers/deployer/deployer.py create mode 100644 tests_integ/helpers/deployer/exceptions/__init__.py create mode 100644 tests_integ/helpers/deployer/exceptions/exceptions.py create mode 100644 tests_integ/helpers/deployer/utils/__init__.py create mode 100644 tests_integ/helpers/deployer/utils/artifact_exporter.py create mode 100644 tests_integ/helpers/deployer/utils/colors.py create mode 100644 tests_integ/helpers/deployer/utils/table_print.py create mode 100644 tests_integ/helpers/deployer/utils/time.py diff --git a/requirements/dev.txt b/requirements/dev.txt index a08e446e0..be3851301 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -11,6 +11,11 @@ pytest~=4.6.11; python_version < '3.6' # pytest dropped python 2 support after 4 mock>=3.0.5,<4.0.0 # 4.0.0 drops Python 2 support parameterized~=0.7.4 +# Integration tests +pathlib2>=2.3.5; python_version < '3' +click~=7.1 +dateparser~=0.7 + # Requirements for examples requests~=2.24.0 diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 0f46cbd5f..204d92f9e 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -1,13 +1,16 @@ import logging import os -from pathlib import Path +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path from unittest.case import TestCase import boto3 import pytest import yaml from botocore.exceptions import ClientError -from samcli.lib.deploy.deployer import Deployer +from tests_integ.helpers.deployer.deployer import Deployer from tests_integ.helpers.helpers import transform_template, verify_stack_resources, generate_suffix, create_bucket from tests_integ.helpers.file_resources import FILE_TO_S3_URI_MAP, CODE_KEY_TO_FILE_MAP @@ -35,7 +38,7 @@ def setUpClass(cls): cls.api_v2_client = boto3.client("apigatewayv2") cls.sfn_client = boto3.client("stepfunctions") - if not os.path.exists(cls.output_dir): + if not cls.output_dir.exists(): os.mkdir(cls.output_dir) cls._upload_resources() @@ -103,7 +106,7 @@ def _get_s3_uri(cls, file_name, uri_type): def setUp(self): self.cloudformation_client = boto3.client("cloudformation") - self.deployer = Deployer(self.cloudformation_client, changeset_prefix="sam-integ-") + self.deployer = Deployer(self.cloudformation_client) def tearDown(self): self.cloudformation_client.delete_stack(StackName=self.stack_name) diff --git a/tests_integ/helpers/deployer/__init__.py b/tests_integ/helpers/deployer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integ/helpers/deployer/deployer.py b/tests_integ/helpers/deployer/deployer.py new file mode 100644 index 000000000..a36d7da36 --- /dev/null +++ b/tests_integ/helpers/deployer/deployer.py @@ -0,0 +1,486 @@ +""" +Cloudformation deploy class which also streams events and changeset information +""" + +# Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +# This is a modified version of the Deployer class from aws-sam-cli +# (and its dependencies) to work with python 2 +# Modifications: +# - Imports now reference local classes +# - Alternative imports for python2 +# - py3 -> py2 migrations (ex: "".format() instead of f"", no "from" for raise) +# - Moved UserException to exceptions.py +# - Moved DeployColor to colors.py +# - Removed unnecessary functions from artifact_exporter +import sys +import math +from collections import OrderedDict +import logging +import time +from datetime import datetime + +import botocore + +from tests_integ.helpers.deployer.utils.colors import DeployColor +from tests_integ.helpers.deployer.exceptions import exceptions as deploy_exceptions +from tests_integ.helpers.deployer.utils.table_print import pprint_column_names, pprint_columns, newline_per_item, MIN_OFFSET +from tests_integ.helpers.deployer.utils.artifact_exporter import mktempfile, parse_s3_url +from tests_integ.helpers.deployer.utils.time import utc_to_timestamp + +LOG = logging.getLogger(__name__) + +DESCRIBE_STACK_EVENTS_FORMAT_STRING = ( + "{ResourceStatus:<{0}} {ResourceType:<{1}} {LogicalResourceId:<{2}} {ResourceStatusReason:<{3}}" +) +DESCRIBE_STACK_EVENTS_DEFAULT_ARGS = OrderedDict( + { + "ResourceStatus": "ResourceStatus", + "ResourceType": "ResourceType", + "LogicalResourceId": "LogicalResourceId", + "ResourceStatusReason": "ResourceStatusReason", + } +) + +DESCRIBE_STACK_EVENTS_TABLE_HEADER_NAME = "CloudFormation events from changeset" + +DESCRIBE_CHANGESET_FORMAT_STRING = "{Operation:<{0}} {LogicalResourceId:<{1}} {ResourceType:<{2}} {Replacement:<{3}}" +DESCRIBE_CHANGESET_DEFAULT_ARGS = OrderedDict( + { + "Operation": "Operation", + "LogicalResourceId": "LogicalResourceId", + "ResourceType": "ResourceType", + "Replacement": "Replacement", + } +) + +DESCRIBE_CHANGESET_TABLE_HEADER_NAME = "CloudFormation stack changeset" + +OUTPUTS_FORMAT_STRING = "{Outputs:<{0}}" +OUTPUTS_DEFAULTS_ARGS = OrderedDict({"Outputs": "Outputs"}) + +OUTPUTS_TABLE_HEADER_NAME = "CloudFormation outputs from deployed stack" + + +class Deployer: + def __init__(self, cloudformation_client, changeset_prefix="sam-integ-"): + self._client = cloudformation_client + self.changeset_prefix = changeset_prefix + # 500ms of sleep time between stack checks and describe stack events. + self.client_sleep = 0.5 + # 2000ms of backoff time which is exponentially used, when there are exceptions during describe stack events + self.backoff = 2 + # Maximum number of attempts before raising exception back up the chain. + self.max_attempts = 3 + self.deploy_color = DeployColor() + + def has_stack(self, stack_name): + """ + Checks if a CloudFormation stack with given name exists + + :param stack_name: Name or ID of the stack + :return: True if stack exists. False otherwise + """ + try: + resp = self._client.describe_stacks(StackName=stack_name) + if not resp["Stacks"]: + return False + + # When you run CreateChangeSet on a a stack that does not exist, + # CloudFormation will create a stack and set it's status + # REVIEW_IN_PROGRESS. However this stack is cannot be manipulated + # by "update" commands. Under this circumstances, we treat like + # this stack does not exist and call CreateChangeSet will + # ChangeSetType set to CREATE and not UPDATE. + stack = resp["Stacks"][0] + return stack["StackStatus"] != "REVIEW_IN_PROGRESS" + + except botocore.exceptions.ClientError as e: + # If a stack does not exist, describe_stacks will throw an + # exception. Unfortunately we don't have a better way than parsing + # the exception msg to understand the nature of this exception. + + if "Stack with id {0} does not exist".format(stack_name) in str(e): + LOG.debug("Stack with id %s does not exist", stack_name) + return False + except botocore.exceptions.BotoCoreError as e: + # If there are credentials, environment errors, + # catch that and throw a deploy failed error. + + LOG.debug("Botocore Exception : %s", str(e)) + raise deploy_exceptions.DeployFailedError(stack_name=stack_name, msg=str(e)) + + except Exception as e: + # We don't know anything about this exception. Don't handle + LOG.debug("Unable to get stack details.", exc_info=e) + raise e + + def create_changeset( + self, stack_name, cfn_template, parameter_values, capabilities, role_arn, notification_arns, s3_uploader, tags + ): + """ + Call Cloudformation to create a changeset and wait for it to complete + + :param stack_name: Name or ID of stack + :param cfn_template: CloudFormation template string + :param parameter_values: Template parameters object + :param capabilities: Array of capabilities passed to CloudFormation + :param tags: Array of tags passed to CloudFormation + :return: + """ + if not self.has_stack(stack_name): + changeset_type = "CREATE" + # When creating a new stack, UsePreviousValue=True is invalid. + # For such parameters, users should either override with new value, + # or set a Default value in template to successfully create a stack. + parameter_values = [x for x in parameter_values if not x.get("UsePreviousValue", False)] + else: + changeset_type = "UPDATE" + # UsePreviousValue not valid if parameter is new + summary = self._client.get_template_summary(StackName=stack_name) + existing_parameters = [parameter["ParameterKey"] for parameter in summary["Parameters"]] + parameter_values = [ + x + for x in parameter_values + if not (x.get("UsePreviousValue", False) and x["ParameterKey"] not in existing_parameters) + ] + + # Each changeset will get a unique name based on time. + # Description is also setup based on current date and that SAM CLI is used. + kwargs = { + "ChangeSetName": self.changeset_prefix + str(int(time.time())), + "StackName": stack_name, + "TemplateBody": cfn_template, + "ChangeSetType": changeset_type, + "Parameters": parameter_values, + "Capabilities": capabilities, + "Description": "Created by SAM CLI at {0} UTC".format(datetime.utcnow().isoformat()), + "Tags": tags, + } + + # If an S3 uploader is available, use TemplateURL to deploy rather than + # TemplateBody. This is required for large templates. + if s3_uploader: + with mktempfile() as temporary_file: + temporary_file.write(kwargs.pop("TemplateBody")) + temporary_file.flush() + + # TemplateUrl property requires S3 URL to be in path-style format + parts = parse_s3_url( + s3_uploader.upload_with_dedup(temporary_file.name, "template"), version_property="Version" + ) + kwargs["TemplateURL"] = s3_uploader.to_path_style_s3_url(parts["Key"], parts.get("Version", None)) + + # don't set these arguments if not specified to use existing values + if role_arn is not None: + kwargs["RoleARN"] = role_arn + if notification_arns is not None: + kwargs["NotificationARNs"] = notification_arns + return self._create_change_set(stack_name=stack_name, changeset_type=changeset_type, **kwargs) + + def _create_change_set(self, stack_name, changeset_type, **kwargs): + try: + resp = self._client.create_change_set(**kwargs) + return resp, changeset_type + except botocore.exceptions.ClientError as ex: + if "The bucket you are attempting to access must be addressed using the specified endpoint" in str(ex): + raise deploy_exceptions.DeployBucketInDifferentRegionError("Failed to create/update stack {}".format(stack_name)) + raise deploy_exceptions.ChangeSetError(stack_name=stack_name, msg=str(ex)) + + except Exception as ex: + LOG.debug("Unable to create changeset", exc_info=ex) + raise deploy_exceptions.ChangeSetError(stack_name=stack_name, msg=str(ex)) + + @pprint_column_names( + format_string=DESCRIBE_CHANGESET_FORMAT_STRING, + format_kwargs=DESCRIBE_CHANGESET_DEFAULT_ARGS, + table_header=DESCRIBE_CHANGESET_TABLE_HEADER_NAME, + ) + def describe_changeset(self, change_set_id, stack_name, **kwargs): + """ + Call Cloudformation to describe a changeset + + :param change_set_id: ID of the changeset + :param stack_name: Name of the CloudFormation stack + :return: dictionary of changes described in the changeset. + """ + paginator = self._client.get_paginator("describe_change_set") + response_iterator = paginator.paginate(ChangeSetName=change_set_id, StackName=stack_name) + changes = {"Add": [], "Modify": [], "Remove": []} + changes_showcase = {"Add": "+ Add", "Modify": "* Modify", "Remove": "- Delete"} + changeset = False + for item in response_iterator: + cf_changes = item.get("Changes") + for change in cf_changes: + changeset = True + resource_props = change.get("ResourceChange") + action = resource_props.get("Action") + changes[action].append( + { + "LogicalResourceId": resource_props.get("LogicalResourceId"), + "ResourceType": resource_props.get("ResourceType"), + "Replacement": "N/A" + if resource_props.get("Replacement") is None + else resource_props.get("Replacement"), + } + ) + + for k, v in changes.items(): + for value in v: + row_color = self.deploy_color.get_changeset_action_color(action=k) + pprint_columns( + columns=[ + changes_showcase.get(k, k), + value["LogicalResourceId"], + value["ResourceType"], + value["Replacement"], + ], + width=kwargs["width"], + margin=kwargs["margin"], + format_string=DESCRIBE_CHANGESET_FORMAT_STRING, + format_args=kwargs["format_args"], + columns_dict=DESCRIBE_CHANGESET_DEFAULT_ARGS.copy(), + color=row_color, + ) + + if not changeset: + # There can be cases where there are no changes, + # but could be an an addition of a SNS notification topic. + pprint_columns( + columns=["-", "-", "-", "-"], + width=kwargs["width"], + margin=kwargs["margin"], + format_string=DESCRIBE_CHANGESET_FORMAT_STRING, + format_args=kwargs["format_args"], + columns_dict=DESCRIBE_CHANGESET_DEFAULT_ARGS.copy(), + ) + + return changes + + def wait_for_changeset(self, changeset_id, stack_name): + """ + Waits until the changeset creation completes + + :param changeset_id: ID or name of the changeset + :param stack_name: Stack name + :return: Latest status of the create-change-set operation + """ + sys.stdout.write("\nWaiting for changeset to be created..\n") + sys.stdout.flush() + + # Wait for changeset to be created + waiter = self._client.get_waiter("change_set_create_complete") + # Poll every 5 seconds. Changeset creation should be fast + waiter_config = {"Delay": 5} + try: + waiter.wait(ChangeSetName=changeset_id, StackName=stack_name, WaiterConfig=waiter_config) + except botocore.exceptions.WaiterError as ex: + + resp = ex.last_response + status = resp["Status"] + reason = resp["StatusReason"] + + if ( + status == "FAILED" + and "The submitted information didn't contain changes." in reason + or "No updates are to be performed" in reason + ): + raise deploy_exceptions.ChangeEmptyError(stack_name=stack_name) + + raise deploy_exceptions.ChangeSetError( + stack_name=stack_name, msg="ex: {0} Status: {1}. Reason: {2}".format(ex, status, reason) + ) + + def execute_changeset(self, changeset_id, stack_name): + """ + Calls CloudFormation to execute changeset + + :param changeset_id: ID of the changeset + :param stack_name: Name or ID of the stack + :return: Response from execute-change-set call + """ + try: + return self._client.execute_change_set(ChangeSetName=changeset_id, StackName=stack_name) + except botocore.exceptions.ClientError as ex: + raise deploy_exceptions.DeployFailedError(stack_name=stack_name, msg=str(ex)) + + def get_last_event_time(self, stack_name): + """ + Finds the last event time stamp thats present for the stack, if not get the current time + :param stack_name: Name or ID of the stack + :return: unix epoch + """ + try: + return utc_to_timestamp( + self._client.describe_stack_events(StackName=stack_name)["StackEvents"][0]["Timestamp"] + ) + except KeyError: + return time.time() + + @pprint_column_names( + format_string=DESCRIBE_STACK_EVENTS_FORMAT_STRING, + format_kwargs=DESCRIBE_STACK_EVENTS_DEFAULT_ARGS, + table_header=DESCRIBE_STACK_EVENTS_TABLE_HEADER_NAME, + ) + def describe_stack_events(self, stack_name, time_stamp_marker, **kwargs): + """ + Calls CloudFormation to get current stack events + :param stack_name: Name or ID of the stack + :param time_stamp_marker: last event time on the stack to start streaming events from. + :return: + """ + + stack_change_in_progress = True + events = set() + retry_attempts = 0 + + while stack_change_in_progress and retry_attempts <= self.max_attempts: + try: + + # Only sleep if there have been no retry_attempts + time.sleep(self.client_sleep if retry_attempts == 0 else 0) + describe_stacks_resp = self._client.describe_stacks(StackName=stack_name) + paginator = self._client.get_paginator("describe_stack_events") + response_iterator = paginator.paginate(StackName=stack_name) + stack_status = describe_stacks_resp["Stacks"][0]["StackStatus"] + latest_time_stamp_marker = time_stamp_marker + for event_items in response_iterator: + for event in event_items["StackEvents"]: + if event["EventId"] not in events and utc_to_timestamp(event["Timestamp"]) > time_stamp_marker: + events.add(event["EventId"]) + latest_time_stamp_marker = max( + latest_time_stamp_marker, utc_to_timestamp(event["Timestamp"]) + ) + row_color = self.deploy_color.get_stack_events_status_color(status=event["ResourceStatus"]) + pprint_columns( + columns=[ + event["ResourceStatus"], + event["ResourceType"], + event["LogicalResourceId"], + event.get("ResourceStatusReason", "-"), + ], + width=kwargs["width"], + margin=kwargs["margin"], + format_string=DESCRIBE_STACK_EVENTS_FORMAT_STRING, + format_args=kwargs["format_args"], + columns_dict=DESCRIBE_STACK_EVENTS_DEFAULT_ARGS.copy(), + color=row_color, + ) + # Skip already shown old event entries + elif utc_to_timestamp(event["Timestamp"]) <= time_stamp_marker: + time_stamp_marker = latest_time_stamp_marker + break + else: # go to next loop if not break from inside loop + time_stamp_marker = latest_time_stamp_marker # update marker if all events are new + continue + break # reached here only if break from inner loop! + + if self._check_stack_complete(stack_status): + stack_change_in_progress = False + break + except botocore.exceptions.ClientError as ex: + retry_attempts = retry_attempts + 1 + if retry_attempts > self.max_attempts: + LOG.error("Describing stack events for %s failed: %s", stack_name, str(ex)) + return + # Sleep in exponential backoff mode + time.sleep(math.pow(self.backoff, retry_attempts)) + + def _check_stack_complete(self, status): + return "COMPLETE" in status and "CLEANUP" not in status + + def wait_for_execute(self, stack_name, changeset_type): + sys.stdout.write( + "\n{} - Waiting for stack create/update " + "to complete\n".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + ) + sys.stdout.flush() + + self.describe_stack_events(stack_name, self.get_last_event_time(stack_name)) + + # Pick the right waiter + if changeset_type == "CREATE": + waiter = self._client.get_waiter("stack_create_complete") + elif changeset_type == "UPDATE": + waiter = self._client.get_waiter("stack_update_complete") + else: + raise RuntimeError("Invalid changeset type {0}".format(changeset_type)) + + # Poll every 30 seconds. Polling too frequently risks hitting rate limits + # on CloudFormation's DescribeStacks API + waiter_config = {"Delay": 30, "MaxAttempts": 120} + + try: + waiter.wait(StackName=stack_name, WaiterConfig=waiter_config) + except botocore.exceptions.WaiterError as ex: + LOG.debug("Execute changeset waiter exception", exc_info=ex) + + raise deploy_exceptions.DeployFailedError(stack_name=stack_name, msg=str(ex)) + + outputs = self.get_stack_outputs(stack_name=stack_name, echo=False) + if outputs: + self._display_stack_outputs(outputs) + + def create_and_wait_for_changeset( + self, stack_name, cfn_template, parameter_values, capabilities, role_arn, notification_arns, s3_uploader, tags + ): + try: + result, changeset_type = self.create_changeset( + stack_name, cfn_template, parameter_values, capabilities, role_arn, notification_arns, s3_uploader, tags + ) + self.wait_for_changeset(result["Id"], stack_name) + self.describe_changeset(result["Id"], stack_name) + return result, changeset_type + except botocore.exceptions.ClientError as ex: + raise deploy_exceptions.DeployFailedError(stack_name=stack_name, msg=str(ex)) + + @pprint_column_names( + format_string=OUTPUTS_FORMAT_STRING, format_kwargs=OUTPUTS_DEFAULTS_ARGS, table_header=OUTPUTS_TABLE_HEADER_NAME + ) + def _display_stack_outputs(self, stack_outputs, **kwargs): + for counter, output in enumerate(stack_outputs): + for k, v in [ + ("Key", output.get("OutputKey")), + ("Description", output.get("Description", "-")), + ("Value", output.get("OutputValue")), + ]: + pprint_columns( + columns=["{k:<{0}}{v:<{0}}".format(MIN_OFFSET, k=k, v=v)], + width=kwargs["width"], + margin=kwargs["margin"], + format_string=OUTPUTS_FORMAT_STRING, + format_args=kwargs["format_args"], + columns_dict=OUTPUTS_DEFAULTS_ARGS.copy(), + color="green", + replace_whitespace=False, + break_long_words=False, + drop_whitespace=False, + ) + newline_per_item(stack_outputs, counter) + + def get_stack_outputs(self, stack_name, echo=True): + try: + stacks_description = self._client.describe_stacks(StackName=stack_name) + try: + outputs = stacks_description["Stacks"][0]["Outputs"] + if echo: + sys.stdout.write("\nStack {stack_name} outputs:\n".format(stack_name=stack_name)) + sys.stdout.flush() + self._display_stack_outputs(stack_outputs=outputs) + return outputs + except KeyError: + return None + + except botocore.exceptions.ClientError as ex: + raise deploy_exceptions.DeployStackOutPutFailedError(stack_name=stack_name, msg=str(ex)) diff --git a/tests_integ/helpers/deployer/exceptions/__init__.py b/tests_integ/helpers/deployer/exceptions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integ/helpers/deployer/exceptions/exceptions.py b/tests_integ/helpers/deployer/exceptions/exceptions.py new file mode 100644 index 000000000..0a85783d8 --- /dev/null +++ b/tests_integ/helpers/deployer/exceptions/exceptions.py @@ -0,0 +1,89 @@ +""" +Exceptions that are raised by sam deploy +""" +import click + +class UserException(click.ClickException): + """ + Base class for all exceptions that need to be surfaced to the user. Typically, we will display the exception + message to user and return the error code from CLI process + """ + + exit_code = 1 + + def __init__(self, message, wrapped_from=None): + self.wrapped_from = wrapped_from + + click.ClickException.__init__(self, message) + +class ChangeEmptyError(UserException): + def __init__(self, stack_name): + self.stack_name = stack_name + message_fmt = "No changes to deploy. Stack {stack_name} is up to date" + super(ChangeEmptyError, self).__init__(message=message_fmt.format(stack_name=self.stack_name)) + + +class ChangeSetError(UserException): + def __init__(self, stack_name, msg): + self.stack_name = stack_name + self.msg = msg + message_fmt = "Failed to create changeset for the stack: {stack_name}, {msg}" + super(ChangeSetError, self).__init__(message=message_fmt.format(stack_name=self.stack_name, msg=self.msg)) + + +class DeployFailedError(UserException): + def __init__(self, stack_name, msg): + self.stack_name = stack_name + self.msg = msg + + message_fmt = "Failed to create/update the stack: {stack_name}, {msg}" + + super(DeployFailedError, self).__init__(message=message_fmt.format(stack_name=self.stack_name, msg=msg)) + + +class GuidedDeployFailedError(UserException): + def __init__(self, msg): + self.msg = msg + super(GuidedDeployFailedError, self).__init__(message=msg) + + +class DeployStackOutPutFailedError(UserException): + def __init__(self, stack_name, msg): + self.stack_name = stack_name + self.msg = msg + + message_fmt = "Failed to get outputs from stack: {stack_name}, {msg}" + + super(DeployStackOutPutFailedError, self).__init__(message=message_fmt.format(stack_name=self.stack_name, msg=msg)) + + +class DeployBucketInDifferentRegionError(UserException): + def __init__(self, msg): + self.msg = msg + + message_fmt = "{msg} : deployment s3 bucket is in a different region, try sam deploy --guided" + + super(DeployBucketInDifferentRegionError, self).__init__(message=message_fmt.format(msg=self.msg)) + + +class DeployBucketRequiredError(UserException): + def __init__(self): + + message_fmt = ( + "Templates with a size greater than 51,200 bytes must be deployed " + "via an S3 Bucket. Please add the --s3-bucket parameter to your " + "command. The local template will be copied to that S3 bucket and " + "then deployed." + ) + + super(DeployBucketRequiredError, self).__init__(message=message_fmt) + + +class DeployResolveS3AndS3SetError(UserException): + def __init__(self): + message_fmt = ( + "Cannot use both --resolve-s3 and --s3-bucket parameters in non-guided deployments." + " Please use only one or use the --guided option for a guided deployment." + ) + + super(DeployResolveS3AndS3SetError, self).__init__(message=message_fmt) diff --git a/tests_integ/helpers/deployer/utils/__init__.py b/tests_integ/helpers/deployer/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests_integ/helpers/deployer/utils/artifact_exporter.py b/tests_integ/helpers/deployer/utils/artifact_exporter.py new file mode 100644 index 000000000..d0a96bfe6 --- /dev/null +++ b/tests_integ/helpers/deployer/utils/artifact_exporter.py @@ -0,0 +1,61 @@ +""" +Logic for uploading to S3 per Cloudformation Specific Resource +""" +# pylint: disable=no-member + +# Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import os +import tempfile +import contextlib +from contextlib import contextmanager +try: + from urllib.parse import urlparse, parse_qs +except ImportError: # py2 + from urlparse import urlparse, parse_qs +import uuid + + +def parse_s3_url(url, bucket_name_property="Bucket", object_key_property="Key", version_property=None): + + if isinstance(url, str) and url.startswith("s3://"): + + parsed = urlparse(url) + query = parse_qs(parsed.query) + + if parsed.netloc and parsed.path: + result = dict() + result[bucket_name_property] = parsed.netloc + result[object_key_property] = parsed.path.lstrip("/") + + # If there is a query string that has a single versionId field, + # set the object version and return + if version_property is not None and "versionId" in query and len(query["versionId"]) == 1: + result[version_property] = query["versionId"][0] + + return result + + raise ValueError("URL given to the parse method is not a valid S3 url " "{0}".format(url)) + +@contextmanager +def mktempfile(): + directory = tempfile.gettempdir() + filename = os.path.join(directory, uuid.uuid4().hex) + + try: + with open(filename, "w+") as handle: + yield handle + finally: + if os.path.exists(filename): + os.remove(filename) diff --git a/tests_integ/helpers/deployer/utils/colors.py b/tests_integ/helpers/deployer/utils/colors.py new file mode 100644 index 000000000..d852f0288 --- /dev/null +++ b/tests_integ/helpers/deployer/utils/colors.py @@ -0,0 +1,92 @@ +""" +Wrapper to generated colored messages for printing in Terminal +""" + +import click + + +class Colored: + """ + Helper class to add ANSI colors and decorations to text. Given a string, ANSI colors are added with special prefix + and suffix characters that are specially interpreted by Terminals to display colors. + + Ex: "message" -> add red color -> \x1b[31mmessage\x1b[0m + + This class serves two purposes: + - Hide the underlying library used to provide colors: In this case, we use ``click`` library which is usually + used to build a CLI interface. We use ``click`` just to minimize the number of dependencies we add to this + project. This class allows us to replace click with any other color library like ``pygments`` without + changing callers. + + - Transparently turn off colors: In cases when the string is not written to Terminal (ex: log file) the ANSI + color codes should not be written. This class supports the scenario by allowing you to turn off colors. + Calls to methods like `red()` will simply return the input string. + """ + + def __init__(self, colorize=True): + """ + Initialize the object + + Parameters + ---------- + colorize : bool + Optional. Set this to True to turn on coloring. False will turn off coloring + """ + self.colorize = colorize + + def red(self, msg): + """Color the input red""" + return self._color(msg, "red") + + def green(self, msg): + """Color the input green""" + return self._color(msg, "green") + + def cyan(self, msg): + """Color the input cyan""" + return self._color(msg, "cyan") + + def white(self, msg): + """Color the input white""" + return self._color(msg, "white") + + def yellow(self, msg): + """Color the input yellow""" + return self._color(msg, "yellow") + + def underline(self, msg): + """Underline the input""" + return click.style(msg, underline=True) if self.colorize else msg + + def _color(self, msg, color): + """Internal helper method to add colors to input""" + kwargs = {"fg": color} + return click.style(msg, **kwargs) if self.colorize else msg + +class DeployColor: + def __init__(self): + self._color = Colored() + self.changeset_color_map = {"Add": "green", "Modify": "yellow", "Remove": "red"} + self.status_color_map = { + "CREATE_COMPLETE": "green", + "CREATE_FAILED": "red", + "CREATE_IN_PROGRESS": "yellow", + "DELETE_COMPLETE": "green", + "DELETE_FAILED": "red", + "DELETE_IN_PROGRESS": "red", + "REVIEW_IN_PROGRESS": "yellow", + "ROLLBACK_COMPLETE": "red", + "ROLLBACK_IN_PROGRESS": "red", + "UPDATE_COMPLETE": "green", + "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS": "yellow", + "UPDATE_IN_PROGRESS": "yellow", + "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS": "red", + "UPDATE_ROLLBACK_FAILED": "red", + "UPDATE_ROLLBACK_IN_PROGRESS": "red", + } + + def get_stack_events_status_color(self, status): + return self.status_color_map.get(status, "yellow") + + def get_changeset_action_color(self, action): + return self.changeset_color_map.get(action, "yellow") \ No newline at end of file diff --git a/tests_integ/helpers/deployer/utils/table_print.py b/tests_integ/helpers/deployer/utils/table_print.py new file mode 100644 index 000000000..e1cefd35d --- /dev/null +++ b/tests_integ/helpers/deployer/utils/table_print.py @@ -0,0 +1,136 @@ +""" +Utilities for table pretty printing using click +""" +from itertools import count +try: + from itertools import zip_longest +except ImportError: # py2 + from itertools import izip_longest as zip_longest +import textwrap +from functools import wraps + +import click + +MIN_OFFSET = 20 + + +def pprint_column_names(format_string, format_kwargs, margin=None, table_header=None, color="yellow"): + """ + + :param format_string: format string to be used that has the strings, minimum width to be replaced + :param format_kwargs: dictionary that is supplied to the format_string to format the string + :param margin: margin that is to be reduced from column width for columnar text. + :param table_header: Supplied table header + :param color: color supplied for table headers and column names. + :return: boilerplate table string + """ + + min_width = 100 + min_margin = 2 + + def pprint_wrap(func): + # Calculate terminal width, number of columns in the table + width, _ = click.get_terminal_size() + # For UX purposes, set a minimum width for the table to be usable + # and usable_width keeps margins in mind. + width = max(width, min_width) + + total_args = len(format_kwargs) + if not total_args: + raise ValueError("Number of arguments supplied should be > 0 , format_kwargs: {}".format(format_kwargs)) + + # Get width to be a usable number so that we can equally divide the space for all the columns. + # Can be refactored, to allow for modularity in the shaping of the columns. + width = width - (width % total_args) + usable_width_no_margin = int(width) - 1 + usable_width = int((usable_width_no_margin - (margin if margin else min_margin))) + if total_args > int(usable_width / 2): + raise ValueError("Total number of columns exceed available width") + width_per_column = int(usable_width / total_args) + + # The final column should not roll over into the next line + final_arg_width = width_per_column - 1 + + # the format string contains minimumwidth that need to be set. + # eg: "{a:{0}}} {b:<{1}}} {c:{2}}}" + format_args = [width_per_column for _ in range(total_args - 1)] + format_args.extend([final_arg_width]) + + # format arguments are now ready for setting minimumwidth + + @wraps(func) + def wrap(*args, **kwargs): + # The table is setup with the column names, format_string contains the column names. + if table_header: + click.secho("\n" + table_header) + click.secho("-" * usable_width, fg=color) + click.secho(format_string.format(*format_args, **format_kwargs), fg=color) + click.secho("-" * usable_width, fg=color) + # format_args which have the minimumwidth set per {} in the format_string is passed to the function + # which this decorator wraps, so that the function has access to the correct format_args + kwargs["format_args"] = format_args + kwargs["width"] = width_per_column + kwargs["margin"] = margin if margin else min_margin + result = func(*args, **kwargs) + # Complete the table + click.secho("-" * usable_width, fg=color) + return result + + return wrap + + return pprint_wrap + + +def wrapped_text_generator(texts, width, margin, **textwrap_kwargs): + """ + + Return a generator where the contents are wrapped text to a specified width. + + :param texts: list of text that needs to be wrapped at specified width + :param width: width of the text to be wrapped + :param margin: margin to be reduced from width for cleaner UX + :param textwrap_kwargs: keyword arguments that are passed to textwrap.wrap + :return: generator of wrapped text + """ + for text in texts: + yield textwrap.wrap(text, width=width - margin, **textwrap_kwargs) + + +def pprint_columns(columns, width, margin, format_string, format_args, columns_dict, color="yellow", **textwrap_kwargs): + """ + + Print columns based on list of columnar text, associated formatting string and associated format arguments. + + :param columns: list of columnnar text that go into columns as specified by the format_string + :param width: width of the text to be wrapped + :param margin: margin to be reduced from width for cleaner UX + :param format_string: A format string that has both width and text specifiers set. + :param format_args: list of offset specifiers + :param columns_dict: arguments dictionary that have dummy values per column + :param color: color supplied for rows within the table. + :param textwrap_kwargs: keyword arguments that are passed to textwrap.wrap + :return: + """ + for columns_text in zip_longest(*wrapped_text_generator(columns, width, margin, **textwrap_kwargs), fillvalue=""): + counter = count() + # Generate columnar data that correspond to the column names and update them. + for k, _ in columns_dict.items(): + columns_dict[k] = columns_text[next(counter)] + + click.secho(format_string.format(*format_args, **columns_dict), fg=color) + + +def newline_per_item(iterable, counter): + """ + Adds a new line based on the index of a given iterable + Parameters + ---------- + iterable: Any iterable that implements __len__ + counter: Current index within the iterable + + Returns + ------- + + """ + if counter < len(iterable) - 1: + click.echo(message="", nl=True) diff --git a/tests_integ/helpers/deployer/utils/time.py b/tests_integ/helpers/deployer/utils/time.py new file mode 100644 index 000000000..02b078337 --- /dev/null +++ b/tests_integ/helpers/deployer/utils/time.py @@ -0,0 +1,127 @@ +""" +Date & Time related utilities +""" + +import datetime +import dateparser + +from dateutil.tz import tzutc + + +def timestamp_to_iso(timestamp): + """ + Convert Unix Epoch Timestamp to ISO formatted time string: + Ex: 1234567890 -> 2018-07-05T03:09:43.842000 + + Parameters + ---------- + timestamp : int + Unix epoch timestamp + + Returns + ------- + str + ISO formatted time string + """ + + return to_datetime(timestamp).isoformat() + + +def to_datetime(timestamp): + """ + Convert Unix Epoch Timestamp to Python's ``datetime.datetime`` object + + Parameters + ---------- + timestamp : int + Unix epoch timestamp + + Returns + ------- + datetime.datetime + Datetime representation of timestamp + """ + + timestamp_secs = int(timestamp) / 1000.0 + return datetime.datetime.utcfromtimestamp(timestamp_secs) + + +def to_timestamp(some_time): + """ + Converts the given datetime value to Unix timestamp + + Parameters + ---------- + some_time : datetime.datetime + Value to be converted to unix epoch. This must be without any timezone identifier + + Returns + ------- + int + Unix timestamp of the given time + """ + + # `total_seconds()` returns elaped microseconds as a float. Get just milliseconds and discard the rest. + return int((some_time - datetime.datetime(1970, 1, 1)).total_seconds() * 1000.0) + + +def utc_to_timestamp(utc): + """ + Converts utc timestamp with tz_info set to utc to Unix timestamp + :param utc: datetime.datetime + :return: UNIX timestamp + """ + + return to_timestamp(utc.replace(tzinfo=None)) + + +def to_utc(some_time): + """ + Convert the given date to UTC, if the date contains a timezone. + + Parameters + ---------- + some_time : datetime.datetime + datetime object to convert to UTC + + Returns + ------- + datetime.datetime + Converted datetime object + """ + + # Convert timezone aware objects to UTC + if some_time.tzinfo and some_time.utcoffset(): + some_time = some_time.astimezone(tzutc()) + + # Now that time is UTC, simply remove the timezone component. + return some_time.replace(tzinfo=None) + + +def parse_date(date_string): + """ + Parse the given string as datetime object. This parser supports in almost any string formats. + + For relative times, like `10min ago`, this parser computes the actual time relative to current UTC time. This + allows time to always be in UTC if an explicit time zone is not provided. + + Parameters + ---------- + date_string : str + String representing the date + + Returns + ------- + datetime.datetime + Parsed datetime object. None, if the string cannot be parsed. + """ + + parser_settings = { + # Relative times like '10m ago' must subtract from the current UTC time. Without this setting, dateparser + # will use current local time as the base for subtraction, but falsely assume it is a UTC time. Therefore + # the time that dateparser returns will be a `datetime` object that did not have any timezone information. + # So be explicit to set the time to UTC. + "RELATIVE_BASE": datetime.datetime.utcnow() + } + + return dateparser.parse(date_string, settings=parser_settings) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index c170d8943..04dd83e28 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -49,7 +49,7 @@ def verify_stack_resources(expected_file_path, stack_resources): for i in range(len(expected_resources)): exp = expected_resources[i] parsed = parsed_resources[i] - if not re.fullmatch(exp["LogicalResourceId"] + "([0-9a-f]{10})?", parsed["LogicalResourceId"]): + if not re.match("^" + exp["LogicalResourceId"] + "([0-9a-f]{10})?$", parsed["LogicalResourceId"]): return False if exp["ResourceType"] != parsed["ResourceType"]: return False From 6a975094881c30678b22f2e305ccda56141c1302 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Mon, 23 Nov 2020 20:51:27 -0800 Subject: [PATCH 072/105] fix appveyor os.mkdir failure --- tests_integ/helpers/base_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 204d92f9e..8fdc32c7b 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -39,7 +39,7 @@ def setUpClass(cls): cls.sfn_client = boto3.client("stepfunctions") if not cls.output_dir.exists(): - os.mkdir(cls.output_dir) + os.mkdir(str(cls.output_dir)) cls._upload_resources() @@ -306,7 +306,7 @@ def set_template_resource_property(self, resource_name, property_name, value): self._dump_yaml(self.sub_input_file_path, yaml_doc) def get_template_resource_property(self, resource_name, property_name): - yaml_doc = yaml_doc = self._load_yaml(self.sub_input_file_path) + yaml_doc = self._load_yaml(self.sub_input_file_path) return yaml_doc["Resources"][resource_name]["Properties"][property_name] def deploy_stack(self, parameters=None): From 453a1cab17217f2416b78d0a3e3bb1488f9ffb53 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Tue, 24 Nov 2020 10:43:30 -0800 Subject: [PATCH 073/105] revert changes in appveyor.yml, run make test-integ in the tox.ini --- appveyor.yml | 7 +------ tox.ini | 2 ++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 667c80a1e..a967bcea1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,9 +2,6 @@ version: 1.0.{build} image: Ubuntu environment: - - AWS_DEFAULT_REGION: us-east-1 - matrix: - TOXENV: py27 PYTHON_VERSION: '2.7' @@ -21,8 +18,6 @@ install: - sh: "source ${HOME}/venv${PYTHON_VERSION}/bin/activate" - sh: "python --version" - make init -- make test-integ test_script: -- tox - +- tox \ No newline at end of file diff --git a/tox.ini b/tox.ini index 2df512827..50c551111 100644 --- a/tox.ini +++ b/tox.ini @@ -21,10 +21,12 @@ passenv = AWS* setenv = PYTHONHASHSEED = 0 commands = make pr2.7 codecov + make test-integ [testenv] commands = make pr codecov + make test-integ deps = codecov>=1.4.0 passenv = AWS* TONXENV CI TRAVIS TRAVIS_* whitelist_externals = make, black From 2cb57a9ee657c1a6809a24cd1869ffb941917506 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Tue, 24 Nov 2020 10:49:12 -0800 Subject: [PATCH 074/105] black reformat --- tests_integ/helpers/base_test.py | 6 ++---- tests_integ/helpers/deployer/deployer.py | 13 ++++++++++--- .../helpers/deployer/exceptions/exceptions.py | 6 +++++- .../helpers/deployer/utils/artifact_exporter.py | 4 +++- tests_integ/helpers/deployer/utils/colors.py | 3 ++- tests_integ/helpers/deployer/utils/table_print.py | 5 +++-- 6 files changed, 25 insertions(+), 12 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 8fdc32c7b..1e5ace77c 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -1,5 +1,6 @@ import logging import os + try: from pathlib import Path except ImportError: @@ -221,10 +222,7 @@ def get_stack_outputs(self): if not self.stack_description: return {} output_list = self.stack_description["Stacks"][0]["Outputs"] - return { - output["OutputKey"]: output["OutputValue"] - for output in output_list - } + return {output["OutputKey"]: output["OutputValue"] for output in output_list} def get_resource_status_by_logical_id(self, logical_id): if not self.stack_resources: diff --git a/tests_integ/helpers/deployer/deployer.py b/tests_integ/helpers/deployer/deployer.py index a36d7da36..d1928fe5a 100644 --- a/tests_integ/helpers/deployer/deployer.py +++ b/tests_integ/helpers/deployer/deployer.py @@ -15,7 +15,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -# This is a modified version of the Deployer class from aws-sam-cli +# This is a modified version of the Deployer class from aws-sam-cli # (and its dependencies) to work with python 2 # Modifications: # - Imports now reference local classes @@ -35,7 +35,12 @@ from tests_integ.helpers.deployer.utils.colors import DeployColor from tests_integ.helpers.deployer.exceptions import exceptions as deploy_exceptions -from tests_integ.helpers.deployer.utils.table_print import pprint_column_names, pprint_columns, newline_per_item, MIN_OFFSET +from tests_integ.helpers.deployer.utils.table_print import ( + pprint_column_names, + pprint_columns, + newline_per_item, + MIN_OFFSET, +) from tests_integ.helpers.deployer.utils.artifact_exporter import mktempfile, parse_s3_url from tests_integ.helpers.deployer.utils.time import utc_to_timestamp @@ -195,7 +200,9 @@ def _create_change_set(self, stack_name, changeset_type, **kwargs): return resp, changeset_type except botocore.exceptions.ClientError as ex: if "The bucket you are attempting to access must be addressed using the specified endpoint" in str(ex): - raise deploy_exceptions.DeployBucketInDifferentRegionError("Failed to create/update stack {}".format(stack_name)) + raise deploy_exceptions.DeployBucketInDifferentRegionError( + "Failed to create/update stack {}".format(stack_name) + ) raise deploy_exceptions.ChangeSetError(stack_name=stack_name, msg=str(ex)) except Exception as ex: diff --git a/tests_integ/helpers/deployer/exceptions/exceptions.py b/tests_integ/helpers/deployer/exceptions/exceptions.py index 0a85783d8..ad1b7a445 100644 --- a/tests_integ/helpers/deployer/exceptions/exceptions.py +++ b/tests_integ/helpers/deployer/exceptions/exceptions.py @@ -3,6 +3,7 @@ """ import click + class UserException(click.ClickException): """ Base class for all exceptions that need to be surfaced to the user. Typically, we will display the exception @@ -16,6 +17,7 @@ def __init__(self, message, wrapped_from=None): click.ClickException.__init__(self, message) + class ChangeEmptyError(UserException): def __init__(self, stack_name): self.stack_name = stack_name @@ -54,7 +56,9 @@ def __init__(self, stack_name, msg): message_fmt = "Failed to get outputs from stack: {stack_name}, {msg}" - super(DeployStackOutPutFailedError, self).__init__(message=message_fmt.format(stack_name=self.stack_name, msg=msg)) + super(DeployStackOutPutFailedError, self).__init__( + message=message_fmt.format(stack_name=self.stack_name, msg=msg) + ) class DeployBucketInDifferentRegionError(UserException): diff --git a/tests_integ/helpers/deployer/utils/artifact_exporter.py b/tests_integ/helpers/deployer/utils/artifact_exporter.py index d0a96bfe6..c38ecbce8 100644 --- a/tests_integ/helpers/deployer/utils/artifact_exporter.py +++ b/tests_integ/helpers/deployer/utils/artifact_exporter.py @@ -20,9 +20,10 @@ import tempfile import contextlib from contextlib import contextmanager + try: from urllib.parse import urlparse, parse_qs -except ImportError: # py2 +except ImportError: # py2 from urlparse import urlparse, parse_qs import uuid @@ -48,6 +49,7 @@ def parse_s3_url(url, bucket_name_property="Bucket", object_key_property="Key", raise ValueError("URL given to the parse method is not a valid S3 url " "{0}".format(url)) + @contextmanager def mktempfile(): directory = tempfile.gettempdir() diff --git a/tests_integ/helpers/deployer/utils/colors.py b/tests_integ/helpers/deployer/utils/colors.py index d852f0288..a5a7b480b 100644 --- a/tests_integ/helpers/deployer/utils/colors.py +++ b/tests_integ/helpers/deployer/utils/colors.py @@ -63,6 +63,7 @@ def _color(self, msg, color): kwargs = {"fg": color} return click.style(msg, **kwargs) if self.colorize else msg + class DeployColor: def __init__(self): self._color = Colored() @@ -89,4 +90,4 @@ def get_stack_events_status_color(self, status): return self.status_color_map.get(status, "yellow") def get_changeset_action_color(self, action): - return self.changeset_color_map.get(action, "yellow") \ No newline at end of file + return self.changeset_color_map.get(action, "yellow") diff --git a/tests_integ/helpers/deployer/utils/table_print.py b/tests_integ/helpers/deployer/utils/table_print.py index e1cefd35d..d0ebe39cc 100644 --- a/tests_integ/helpers/deployer/utils/table_print.py +++ b/tests_integ/helpers/deployer/utils/table_print.py @@ -2,10 +2,11 @@ Utilities for table pretty printing using click """ from itertools import count + try: from itertools import zip_longest -except ImportError: # py2 - from itertools import izip_longest as zip_longest +except ImportError: # py2 + from itertools import izip_longest as zip_longest import textwrap from functools import wraps From abcc4aac7af53b7371e29bdd79275037414d256f Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Wed, 25 Nov 2020 14:34:34 -0800 Subject: [PATCH 075/105] refactor: Get resource default returned values are most consistent, added doc to helpers --- tests_integ/helpers/base_test.py | 18 +++--- tests_integ/helpers/helpers.py | 99 +++++++++++++++++++++++++------- 2 files changed, 87 insertions(+), 30 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 1e5ace77c..cfc5c6220 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -206,7 +206,7 @@ def get_stack_stages_v2(self): resources = self.get_stack_resources("AWS::ApiGatewayV2::Api") if not resources: - return None + return [] return self.api_v2_client.get_stages(ApiId=resources[0]["PhysicalResourceId"])["Items"] @@ -226,43 +226,43 @@ def get_stack_outputs(self): def get_resource_status_by_logical_id(self, logical_id): if not self.stack_resources: - return "" + return None for res in self.stack_resources["StackResourceSummaries"]: if res["LogicalResourceId"] == logical_id: return res["ResourceStatus"] - return "" + return None def get_physical_id_by_type(self, resource_type): if not self.stack_resources: - return "" + return None for res in self.stack_resources["StackResourceSummaries"]: if res["ResourceType"] == resource_type: return res["PhysicalResourceId"] - return "" + return None def get_logical_id_by_type(self, resource_type): if not self.stack_resources: - return "" + return None for res in self.stack_resources["StackResourceSummaries"]: if res["ResourceType"] == resource_type: return res["LogicalResourceId"] - return "" + return None def get_physical_id_by_logical_id(self, logical_id): if not self.stack_resources: - return "" + return None for res in self.stack_resources["StackResourceSummaries"]: if res["LogicalResourceId"] == logical_id: return res["PhysicalResourceId"] - return "" + return None def _fill_template(self, file_name): """ diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 04dd83e28..94d9c15bc 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -2,7 +2,7 @@ import logging import re import random -import string # not deprecated, a bug from pylint https://www.logilab.org/ticket/2481 +import string # pylint: disable=deprecated-module from functools import reduce import boto3 @@ -13,24 +13,38 @@ from samtranslator.translator.transform import transform from samtranslator.yaml_helper import yaml_parse +# Length of the suffix sometimes added by CFN to logical IDs +LOGICAL_RES_SUFFIX_LENGTH = 10 +# Length of the random suffix added at the end of the resources we create +# to avoid collisions between tests RANDOM_SUFFIX_LENGTH = 12 -def transform_template(input_file_path, output_file_path): +def transform_template(sam_template_path, cfn_output_path): + """ + Locally transforms a SAM template to a Cloud Formation template + + Parameters + ---------- + sam_template_path : Path + SAM template input path + cfn_output_path : Path + Cloud formation template output path + """ LOG = logging.getLogger(__name__) iam_client = boto3.client("iam") - with open(input_file_path) as f: + with open(sam_template_path) as f: sam_template = yaml_parse(f) try: cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) - with open(output_file_path, "w") as f: + with open(cfn_output_path, "w") as f: f.write(cloud_formation_template_prettified) - print("Wrote transformed CloudFormation template to: " + output_file_path) + print("Wrote transformed CloudFormation template to: " + cfn_output_path) except InvalidDocumentException as e: error_message = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) LOG.error(error_message) @@ -39,6 +53,21 @@ def transform_template(input_file_path, output_file_path): def verify_stack_resources(expected_file_path, stack_resources): + """ + Verifies that the stack resources match the expected ones + + Parameters + ---------- + expected_file_path : Path + Path to the file containing the expected resources + stack_resources : List + Stack resources + + Returns + ------- + bool + True if the stack resources exactly match the expected ones, False otherwise + """ with open(expected_file_path) as expected_data: expected_resources = _sort_resources(json.load(expected_data)) parsed_resources = _sort_resources(stack_resources["StackResourceSummaries"]) @@ -49,7 +78,10 @@ def verify_stack_resources(expected_file_path, stack_resources): for i in range(len(expected_resources)): exp = expected_resources[i] parsed = parsed_resources[i] - if not re.match("^" + exp["LogicalResourceId"] + "([0-9a-f]{10})?$", parsed["LogicalResourceId"]): + if not re.match( + "^" + exp["LogicalResourceId"] + "([0-9a-f]{" + str(LOGICAL_RES_SUFFIX_LENGTH) + "})?$", + parsed["LogicalResourceId"], + ): return False if exp["ResourceType"] != parsed["ResourceType"]: return False @@ -57,32 +89,57 @@ def verify_stack_resources(expected_file_path, stack_resources): def generate_suffix(): - # Very basic random letters generator + """ + Generates a basic random string of length RANDOM_SUFFIX_LENGTH + to append to objects names used in the tests to avoid collisions + between tests runs + + Returns + ------- + string + Random lowercase alphanumeric string of length RANDOM_SUFFIX_LENGTH + """ return "".join(random.choice(string.ascii_lowercase) for i in range(RANDOM_SUFFIX_LENGTH)) def _sort_resources(resources): - return sorted(resources, key=lambda d: d["LogicalResourceId"]) - + """ + Sorts a stack's resources by LogicalResourceId -def create_bucket(bucket_name, region=None): - """Create an S3 bucket in a specified region + Parameters + ---------- + resources : list + Resources to sort - copy code from boto3 doc example - MG: removed the try so that the exception bubbles up and interrupts the test + Returns + ------- + list + List of resources, sorted + """ + if resources is None: + return [] + return sorted(resources, key=lambda d: d["LogicalResourceId"]) - If a region is not specified, the bucket is created in the S3 default - region (us-east-1). - :param bucket_name: Bucket to create - :param region: String region to create bucket in, e.g., 'us-west-2' - :return: True if bucket created, else False +def create_bucket(bucket_name, region): + """ + Creates a S3 bucket in a specific region + + Parameters + ---------- + bucket_name : string + Bucket name + region : string + Region name + + Raises + ------ + NoRegionError + If region is not specified """ - - # Create bucket if region is None: raise NoRegionError() - elif region == "us-east-1": + if region == "us-east-1": s3_client = boto3.client("s3") s3_client.create_bucket(Bucket=bucket_name) else: From f97d759d91a0cea86b8b3f237220b104d066da3c Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Wed, 25 Nov 2020 17:01:15 -0800 Subject: [PATCH 076/105] refactor: Renamed get_stack_stages functions to include api, removed unused deployer exceptions and unnecessary region parameter pass to the apigateway client --- tests_integ/helpers/base_test.py | 6 ++-- .../helpers/deployer/exceptions/exceptions.py | 29 ------------------- tests_integ/single/test_basic_api.py | 2 +- tests_integ/single/test_basic_http_api.py | 2 +- 4 files changed, 5 insertions(+), 34 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index cfc5c6220..4d2635abe 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -33,7 +33,7 @@ def setUpClass(cls): cls.session = boto3.session.Session() cls.my_region = cls.session.region_name cls.s3_client = boto3.client("s3") - cls.api_client = boto3.client("apigateway", cls.my_region) + cls.api_client = boto3.client("apigateway") cls.lambda_client = boto3.client("lambda") cls.iam_client = boto3.client("iam") cls.api_v2_client = boto3.client("apigatewayv2") @@ -194,7 +194,7 @@ def get_stack_deployment_ids(self): return ids - def get_stack_stages(self): + def get_api_stack_stages(self): resources = self.get_stack_resources("AWS::ApiGateway::RestApi") if not resources: @@ -202,7 +202,7 @@ def get_stack_stages(self): return self.api_client.get_stages(restApiId=resources[0]["PhysicalResourceId"])["item"] - def get_stack_stages_v2(self): + def get_api_v2_stack_stages(self): resources = self.get_stack_resources("AWS::ApiGatewayV2::Api") if not resources: diff --git a/tests_integ/helpers/deployer/exceptions/exceptions.py b/tests_integ/helpers/deployer/exceptions/exceptions.py index ad1b7a445..bbb17ea5e 100644 --- a/tests_integ/helpers/deployer/exceptions/exceptions.py +++ b/tests_integ/helpers/deployer/exceptions/exceptions.py @@ -43,12 +43,6 @@ def __init__(self, stack_name, msg): super(DeployFailedError, self).__init__(message=message_fmt.format(stack_name=self.stack_name, msg=msg)) -class GuidedDeployFailedError(UserException): - def __init__(self, msg): - self.msg = msg - super(GuidedDeployFailedError, self).__init__(message=msg) - - class DeployStackOutPutFailedError(UserException): def __init__(self, stack_name, msg): self.stack_name = stack_name @@ -68,26 +62,3 @@ def __init__(self, msg): message_fmt = "{msg} : deployment s3 bucket is in a different region, try sam deploy --guided" super(DeployBucketInDifferentRegionError, self).__init__(message=message_fmt.format(msg=self.msg)) - - -class DeployBucketRequiredError(UserException): - def __init__(self): - - message_fmt = ( - "Templates with a size greater than 51,200 bytes must be deployed " - "via an S3 Bucket. Please add the --s3-bucket parameter to your " - "command. The local template will be copied to that S3 bucket and " - "then deployed." - ) - - super(DeployBucketRequiredError, self).__init__(message=message_fmt) - - -class DeployResolveS3AndS3SetError(UserException): - def __init__(self): - message_fmt = ( - "Cannot use both --resolve-s3 and --s3-bucket parameters in non-guided deployments." - " Please use only one or use the --guided option for a guided deployment." - ) - - super(DeployResolveS3AndS3SetError, self).__init__(message=message_fmt) diff --git a/tests_integ/single/test_basic_api.py b/tests_integ/single/test_basic_api.py index 6ad928b26..4a16d2df9 100644 --- a/tests_integ/single/test_basic_api.py +++ b/tests_integ/single/test_basic_api.py @@ -70,7 +70,7 @@ def test_basic_api_with_tags(self): """ self.create_and_verify_stack("basic_api_with_tags") - stages = self.get_stack_stages() + stages = self.get_api_stack_stages() self.assertEqual(len(stages), 2) stage = next((s for s in stages if s["stageName"] == "my-new-stage-name")) diff --git a/tests_integ/single/test_basic_http_api.py b/tests_integ/single/test_basic_http_api.py index 3530e9a69..33ae31c86 100644 --- a/tests_integ/single/test_basic_http_api.py +++ b/tests_integ/single/test_basic_http_api.py @@ -12,7 +12,7 @@ def test_basic_http_api(self): """ self.create_and_verify_stack("basic_http_api") - stages = self.get_stack_stages_v2() + stages = self.get_api_v2_stack_stages() self.assertEqual(len(stages), 1) self.assertEqual(stages[0]["StageName"], "$default") From 656b457875a275d5d04c199a79f5438efb00193b Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Thu, 26 Nov 2020 16:20:20 -0800 Subject: [PATCH 077/105] docs: Added first version of README.md file to the tests_integ directory --- tests_integ/README.md | 97 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tests_integ/README.md diff --git a/tests_integ/README.md b/tests_integ/README.md new file mode 100644 index 000000000..766263d5e --- /dev/null +++ b/tests_integ/README.md @@ -0,0 +1,97 @@ +# AWS SAM integration tests + +These integration tests test SAM against AWS services by translating SAM templates, deploying them to Cloud Formation and verifying the resulting objects. + +They must run under Python 2 and 3. + +## Run the tests + +### Prerequisites + +#### User and rights + +An Internet connection and an active AWS account are required to run the tests as they will interact with AWS services (most notably Cloud Formation) to create and update objects (Stacks, APIs, ...). + +AWS credentials must be configured either through a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) or [environment variables](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html) + +The user running the tests must have the following roles: +``` +AmazonSQSFullAccess +AmazonSNSFullAccess +AmazonAPIGatewayAdministrator +AWSKeyManagementServicePowerUser +AWSStepFunctionsFullAccess +``` + +#### Initialize the development environment + +If you haven't done so already, run the following command in a terminal at the root of the repository to initialize the development environment: + +``` +make init +``` + +### Running all the tests + +From the root of the repository, run: + +``` +make test-integ +``` + +This can take multiple minutes to execute. + +### Running a specific test file + +From the command line, run: + +``` +pytest --no-cov path/to/the/test_file.py +``` + +Example: + +```sh +pytest --no-cov tests_integ/single/test_basic_api.py +``` + +*We don't measure coverage for integration tests.* + +## Architecture + +### Helpers + +Common classes and tools used by tests. + +``` ++-- helpers/ +| +-- deployer Tools to deploy to Cloud Formation +| +-- base_test.py Common class from which all test classes inherit +| +-- file_resources.py Files to upload to S3 +| +-- helpers.py Miscellaneous helpers +``` + +`base_test.py` runs setup methods before all tests in a class to upload the `file_resources.py` resources to an S3 bucket and after all tests to empty and delete this bucket. + +### Resources + +File resources used by tests + +``` ++-- resources +| +-- code Files to upload to S3 +| +-- expected Files describing the expected created resources +| +-- templates Source SAM templates to translate and deploy +``` + +The matching *expected* and *template* files should have the same name. + +Example: `single/test_basic_api.py` takes the `templates/single/basic_api.yaml` file as input SAM template and tests it against the `expected/single/basic_api.json` file. + +### Single + +Simple tests + +### Tmp + +This directory will be created on the first run and will contain temporary and intermediary files used by the tests: sam templates with substituted variable values, translated temporary cloud formation templates, ... From 77d5b913928fcd5482aff6e1dae59c367511db8f Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 27 Nov 2020 14:44:58 -0800 Subject: [PATCH 078/105] chagne hard code FILE_TO_S3_URI_MAP, CODE_KEY_TO_FILE_MAP to be parameters to pass in. Updated README as well. --- tests_integ/README.md | 27 +++++++++++++++++++++-- tests_integ/helpers/base_test.py | 38 +++++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/tests_integ/README.md b/tests_integ/README.md index 766263d5e..7c90ce3e0 100644 --- a/tests_integ/README.md +++ b/tests_integ/README.md @@ -19,7 +19,7 @@ The user running the tests must have the following roles: AmazonSQSFullAccess AmazonSNSFullAccess AmazonAPIGatewayAdministrator -AWSKeyManagementServicePowerUser +AWSKeyManagementServiceFullAccess AWSStepFunctionsFullAccess ``` @@ -55,8 +55,31 @@ Example: pytest --no-cov tests_integ/single/test_basic_api.py ``` +### Running a specific test + +From the command line, run: + +``` +pytest --no-cov path/to/the/testfile.py::test_class::test_method +``` + +Example: + +```sh +pytets --no-cov tests_integ/single/test_basic_api.py::TestBasicApi::test_basic_api +``` + *We don't measure coverage for integration tests.* +## Write a test + +1. Add your test templates to the tests_integ/resources/templates ‘s single or combination folder +2. Write a expected json file for all the expected resources and add it to the tests_integ/resources/expected +3. (option) Upload the resource files(.zip and etc.) to tests_integ/resources/code and update the relation in tests_integ/helpers/file_resources +4. Write and add your python test code to the tests_integ ‘s single or combination +5. Run it! + + ## Architecture ### Helpers @@ -90,7 +113,7 @@ Example: `single/test_basic_api.py` takes the `templates/single/basic_api.yaml` ### Single -Simple tests +All basic tests file are here ### Tmp diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 4d2635abe..9321d9bdf 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -42,7 +42,7 @@ def setUpClass(cls): if not cls.output_dir.exists(): os.mkdir(str(cls.output_dir)) - cls._upload_resources() + cls._upload_resources(FILE_TO_S3_URI_MAP) @classmethod def tearDownClass(cls): @@ -70,16 +70,20 @@ def _clean_bucket(cls): LOG.error("Unable to delete bucket %s", cls.s3_bucket_name, exc_info=e) @classmethod - def _upload_resources(cls): + def _upload_resources(cls, file_to_s3_uri_map): """ Creates the bucket and uploads the files used by the tests to it """ + if not file_to_s3_uri_map or not file_to_s3_uri_map.items(): + LOG.debug("No resources to upload") + return + create_bucket(cls.s3_bucket_name, region=cls.my_region) current_file_name = "" try: - for file_name, file_info in FILE_TO_S3_URI_MAP.items(): + for file_name, file_info in file_to_s3_uri_map.items(): current_file_name = file_name code_path = str(Path(cls.code_dir, file_name)) LOG.debug("Uploading file %s to bucket %s", file_name, cls.s3_bucket_name) @@ -132,7 +136,7 @@ def create_and_verify_stack(self, file_name, parameters=None): self.expected_resource_path = str(Path(self.expected_dir, file_name + ".json")) self.stack_name = STACK_NAME_PREFIX + file_name.replace("_", "-") + "-" + generate_suffix() - self._fill_template(file_name) + self._fill_template(file_name, CODE_KEY_TO_FILE_MAP, FILE_TO_S3_URI_MAP) self.transform_template() self.deploy_stack(parameters) self.verify_stack() @@ -143,7 +147,7 @@ def transform_template(self): def get_region(self): return self.my_region - def get_s3_uri(self, file_name): + def get_s3_uri(self, file_name, file_to_s3_uri_map): """ Returns the S3 URI of a resource file @@ -151,10 +155,12 @@ def get_s3_uri(self, file_name): ---------- file_name : string Resource file name + file_to_s3_uri_map : dict + code file name to it's s3 uri map """ - return FILE_TO_S3_URI_MAP[file_name]["uri"] + return file_to_s3_uri_map[file_name]["uri"] - def get_code_key_s3_uri(self, code_key): + def get_code_key_s3_uri(self, code_key, code_key_to_file, file_to_s3_uri_map): """ Returns the S3 URI of a code key for template replacement @@ -162,8 +168,12 @@ def get_code_key_s3_uri(self, code_key): ---------- code_key : string Template code key + code_key_to_file: dict + Template code key to code file name map + file_to_s3_uri_map : dict + code file name to it's s3 uri map """ - return FILE_TO_S3_URI_MAP[CODE_KEY_TO_FILE_MAP[code_key]]["uri"] + return file_to_s3_uri_map[code_key_to_file[code_key]]["uri"] def get_stack_resources(self, resource_type, stack_resources=None): if not stack_resources: @@ -264,7 +274,7 @@ def get_physical_id_by_logical_id(self, logical_id): return None - def _fill_template(self, file_name): + def _fill_template(self, file_name, code_key_to_file, file_to_s3_uri_map): """ Replaces the template variables with their value @@ -272,14 +282,20 @@ def _fill_template(self, file_name): ---------- file_name : string Template file name + code_key_to_file: dict + Template code key to code file name map + file_to_s3_uri_map : dict + code file name to it's s3 uri map """ input_file_path = str(Path(self.template_dir, file_name + ".yaml")) updated_template_path = str(Path(self.output_dir, "sub_" + file_name + ".yaml")) with open(input_file_path) as f: data = f.read() - for key, _ in CODE_KEY_TO_FILE_MAP.items(): + for key, _ in code_key_to_file.items(): # We must double the {} to escape them so they will survive a round of unescape - data = data.replace("${{{}}}".format(key), self.get_code_key_s3_uri(key)) + data = data.replace( + "${{{}}}".format(key), self.get_code_key_s3_uri(key, code_key_to_file, file_to_s3_uri_map) + ) yaml_doc = yaml.load(data, Loader=yaml.FullLoader) self._dump_yaml(updated_template_path, yaml_doc) From 1871bc828cc3662df90da56f783d71e4c6933e48 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Fri, 27 Nov 2020 15:04:01 -0800 Subject: [PATCH 079/105] fix: Need to tell pytest in the MakeFile to only search for *.py files so that it doesn't attempt to run the README.md file --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 78a98c6be..d6bf2c90d 100755 --- a/Makefile +++ b/Makefile @@ -8,8 +8,9 @@ init: test: pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests/* +# Need to specify *.py or pytest attempts to execute the README.md file test-integ: - pytest --no-cov tests_integ/* + pytest --no-cov tests_integ/**/*.py black: black setup.py samtranslator/* tests/* tests_integ/* bin/*.py From 6b3ab9f70475fe4e0df64a0e47190dc8b55bc4aa Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Fri, 27 Nov 2020 15:08:44 -0800 Subject: [PATCH 080/105] chore: Minor README.md wording/format changes --- tests_integ/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests_integ/README.md b/tests_integ/README.md index 7c90ce3e0..2400bb772 100644 --- a/tests_integ/README.md +++ b/tests_integ/README.md @@ -15,6 +15,7 @@ An Internet connection and an active AWS account are required to run the tests a AWS credentials must be configured either through a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) or [environment variables](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html) The user running the tests must have the following roles: + ``` AmazonSQSFullAccess AmazonSNSFullAccess @@ -66,20 +67,19 @@ pytest --no-cov path/to/the/testfile.py::test_class::test_method Example: ```sh -pytets --no-cov tests_integ/single/test_basic_api.py::TestBasicApi::test_basic_api +pytest --no-cov tests_integ/single/test_basic_api.py::TestBasicApi::test_basic_api ``` *We don't measure coverage for integration tests.* ## Write a test -1. Add your test templates to the tests_integ/resources/templates ‘s single or combination folder -2. Write a expected json file for all the expected resources and add it to the tests_integ/resources/expected -3. (option) Upload the resource files(.zip and etc.) to tests_integ/resources/code and update the relation in tests_integ/helpers/file_resources -4. Write and add your python test code to the tests_integ ‘s single or combination +1. Add your test templates to the `tests_integ/resources/templates` single or combination folder +2. Write an expected json file for all the expected resources and add it to the `tests_integ/resources/expected` +3. (optional) Add the resource files (zip, json, etc.) to `tests_integ/resources/code` and update the relation in `tests_integ/helpers/file_resources.py` +4. Write and add your python test code to the `tests_integ` single or combination folder 5. Run it! - ## Architecture ### Helpers From f1e0569e81a3cf6d45bf895ed2fe777e9c1f7290 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 27 Nov 2020 15:48:26 -0800 Subject: [PATCH 081/105] change file_to_s3_uri_map and code_key_to_file to be class variables --- tests_integ/helpers/base_test.py | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 9321d9bdf..c81321a51 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -39,6 +39,9 @@ def setUpClass(cls): cls.api_v2_client = boto3.client("apigatewayv2") cls.sfn_client = boto3.client("stepfunctions") + cls.file_to_s3_uri_map = FILE_TO_S3_URI_MAP + cls.code_key_to_file = CODE_KEY_TO_FILE_MAP + if not cls.output_dir.exists(): os.mkdir(str(cls.output_dir)) @@ -136,7 +139,7 @@ def create_and_verify_stack(self, file_name, parameters=None): self.expected_resource_path = str(Path(self.expected_dir, file_name + ".json")) self.stack_name = STACK_NAME_PREFIX + file_name.replace("_", "-") + "-" + generate_suffix() - self._fill_template(file_name, CODE_KEY_TO_FILE_MAP, FILE_TO_S3_URI_MAP) + self._fill_template(file_name) self.transform_template() self.deploy_stack(parameters) self.verify_stack() @@ -147,7 +150,7 @@ def transform_template(self): def get_region(self): return self.my_region - def get_s3_uri(self, file_name, file_to_s3_uri_map): + def get_s3_uri(self, file_name): """ Returns the S3 URI of a resource file @@ -155,12 +158,10 @@ def get_s3_uri(self, file_name, file_to_s3_uri_map): ---------- file_name : string Resource file name - file_to_s3_uri_map : dict - code file name to it's s3 uri map """ - return file_to_s3_uri_map[file_name]["uri"] + return self.file_to_s3_uri_map[file_name]["uri"] - def get_code_key_s3_uri(self, code_key, code_key_to_file, file_to_s3_uri_map): + def get_code_key_s3_uri(self, code_key): """ Returns the S3 URI of a code key for template replacement @@ -168,12 +169,8 @@ def get_code_key_s3_uri(self, code_key, code_key_to_file, file_to_s3_uri_map): ---------- code_key : string Template code key - code_key_to_file: dict - Template code key to code file name map - file_to_s3_uri_map : dict - code file name to it's s3 uri map """ - return file_to_s3_uri_map[code_key_to_file[code_key]]["uri"] + return self.file_to_s3_uri_map[self.code_key_to_file[code_key]]["uri"] def get_stack_resources(self, resource_type, stack_resources=None): if not stack_resources: @@ -274,7 +271,7 @@ def get_physical_id_by_logical_id(self, logical_id): return None - def _fill_template(self, file_name, code_key_to_file, file_to_s3_uri_map): + def _fill_template(self, file_name): """ Replaces the template variables with their value @@ -282,20 +279,14 @@ def _fill_template(self, file_name, code_key_to_file, file_to_s3_uri_map): ---------- file_name : string Template file name - code_key_to_file: dict - Template code key to code file name map - file_to_s3_uri_map : dict - code file name to it's s3 uri map """ input_file_path = str(Path(self.template_dir, file_name + ".yaml")) updated_template_path = str(Path(self.output_dir, "sub_" + file_name + ".yaml")) with open(input_file_path) as f: data = f.read() - for key, _ in code_key_to_file.items(): + for key, _ in self.code_key_to_file.items(): # We must double the {} to escape them so they will survive a round of unescape - data = data.replace( - "${{{}}}".format(key), self.get_code_key_s3_uri(key, code_key_to_file, file_to_s3_uri_map) - ) + data = data.replace("${{{}}}".format(key), self.get_code_key_s3_uri(key)) yaml_doc = yaml.load(data, Loader=yaml.FullLoader) self._dump_yaml(updated_template_path, yaml_doc) From 58e14c70821a4f2a4534f33ecdd6bb98a2c8413f Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Fri, 27 Nov 2020 16:09:58 -0800 Subject: [PATCH 082/105] fix: black and black-check Make commands now only check *.py files in the tests_integ directory --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d6bf2c90d..b9bc252bd 100755 --- a/Makefile +++ b/Makefile @@ -13,10 +13,10 @@ test-integ: pytest --no-cov tests_integ/**/*.py black: - black setup.py samtranslator/* tests/* tests_integ/* bin/*.py + black setup.py samtranslator/* tests/* tests_integ/**/*.py bin/*.py black-check: - black --check setup.py samtranslator/* tests/* tests_integ/* bin/*.py + black --check setup.py samtranslator/* tests/* tests_integ/**/*.py bin/*.py # Command to run everytime you make changes to verify everything works dev: test From 724dfdfdaf9c0f77dbe8f04c5a15506be4d3fce2 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Tue, 1 Dec 2020 16:12:59 -0800 Subject: [PATCH 083/105] docs: Moved the test_integ readme file to the root, renamed it, referenced it in the dev guide and made a few adjustments in it --- DEVELOPMENT_GUIDE.md | 4 ++ tests_integ/README.md => INTEGRATION_TESTS.md | 38 ++++++++++--------- Makefile | 7 ++-- 3 files changed, 28 insertions(+), 21 deletions(-) rename tests_integ/README.md => INTEGRATION_TESTS.md (58%) diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md index 6ccbb9ad6..de44b115c 100644 --- a/DEVELOPMENT_GUIDE.md +++ b/DEVELOPMENT_GUIDE.md @@ -128,6 +128,10 @@ will not work in Python3.6). If you want to test in many versions, you can creat each version and flip between them (sourcing the activate script). Typically, we run all tests in one python version locally and then have our ci (appveyor) run all supported versions. +### Integration tests + +Integration tests are covered in detail in the [INTEGRATION_TESTS.md file](INTEGRATION_TESTS.md) of this repository. + Code Conventions ---------------- diff --git a/tests_integ/README.md b/INTEGRATION_TESTS.md similarity index 58% rename from tests_integ/README.md rename to INTEGRATION_TESTS.md index 2400bb772..9e4d72288 100644 --- a/tests_integ/README.md +++ b/INTEGRATION_TESTS.md @@ -1,8 +1,8 @@ # AWS SAM integration tests -These integration tests test SAM against AWS services by translating SAM templates, deploying them to Cloud Formation and verifying the resulting objects. +These tests run SAM against AWS services by translating SAM templates, deploying them to Cloud Formation and verifying the resulting objects. -They must run under Python 2 and 3. +They must run successfully under Python 2 and 3. ## Run the tests @@ -24,6 +24,8 @@ AWSKeyManagementServiceFullAccess AWSStepFunctionsFullAccess ``` +If you plan on running the full tests suite, ensure that the user credentials you are running the tests with have a timeout of at least 30 minutes as the full suite can take more than 20 minutes to execute. + #### Initialize the development environment If you haven't done so already, run the following command in a terminal at the root of the repository to initialize the development environment: @@ -40,8 +42,6 @@ From the root of the repository, run: make test-integ ``` -This can take multiple minutes to execute. - ### Running a specific test file From the command line, run: @@ -50,7 +50,7 @@ From the command line, run: pytest --no-cov path/to/the/test_file.py ``` -Example: +For example, from the root of the project: ```sh pytest --no-cov tests_integ/single/test_basic_api.py @@ -61,10 +61,10 @@ pytest --no-cov tests_integ/single/test_basic_api.py From the command line, run: ``` -pytest --no-cov path/to/the/testfile.py::test_class::test_method +pytest --no-cov path/to/the/test_file.py::test_class::test_method ``` -Example: +For example, from the root of the project: ```sh pytest --no-cov tests_integ/single/test_basic_api.py::TestBasicApi::test_basic_api @@ -74,13 +74,13 @@ pytest --no-cov tests_integ/single/test_basic_api.py::TestBasicApi::test_basic_a ## Write a test -1. Add your test templates to the `tests_integ/resources/templates` single or combination folder -2. Write an expected json file for all the expected resources and add it to the `tests_integ/resources/expected` -3. (optional) Add the resource files (zip, json, etc.) to `tests_integ/resources/code` and update the relation in `tests_integ/helpers/file_resources.py` -4. Write and add your python test code to the `tests_integ` single or combination folder +1. Add your test templates to the `tests_integ/resources/templates` single or combination folder. +2. Write an expected json file for all the expected resources and add it to the `tests_integ/resources/expected`. +3. (Optional) Add the resource files (zip, json, etc.) to `tests_integ/resources/code` and update the dictionaries in `tests_integ/helpers/file_resources.py`. +4. Write and add your python test code to the `tests_integ` single or combination folder. 5. Run it! -## Architecture +## Directory structure ### Helpers @@ -94,11 +94,11 @@ Common classes and tools used by tests. | +-- helpers.py Miscellaneous helpers ``` -`base_test.py` runs setup methods before all tests in a class to upload the `file_resources.py` resources to an S3 bucket and after all tests to empty and delete this bucket. +`base_test.py` contains `setUpClass` and `tearDownClass` methods to respectively upload and clean the `file_resources.py` resources (upload the files to a new S3 bucket, empty and delete this bucket). ### Resources -File resources used by tests +File resources used by tests. ``` +-- resources @@ -109,12 +109,16 @@ File resources used by tests The matching *expected* and *template* files should have the same name. -Example: `single/test_basic_api.py` takes the `templates/single/basic_api.yaml` file as input SAM template and tests it against the `expected/single/basic_api.json` file. +For example, the `test_basic_api` test in the class `tests_integ/single/test_basic_api.py` takes `templates/single/basic_api.yaml` SAM template as input and verifies its result against `expected/single/basic_api.json`. ### Single -All basic tests file are here +Basic tests which interact with only one service should be put here. + +### Combination + +Tests which interact with multiple services should be put there. ### Tmp -This directory will be created on the first run and will contain temporary and intermediary files used by the tests: sam templates with substituted variable values, translated temporary cloud formation templates, ... +This directory is created on the first run and contains temporary and intermediary files used by the tests: sam templates with substituted variable values, translated temporary cloud formation templates, ... diff --git a/Makefile b/Makefile index b9bc252bd..78a98c6be 100755 --- a/Makefile +++ b/Makefile @@ -8,15 +8,14 @@ init: test: pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests/* -# Need to specify *.py or pytest attempts to execute the README.md file test-integ: - pytest --no-cov tests_integ/**/*.py + pytest --no-cov tests_integ/* black: - black setup.py samtranslator/* tests/* tests_integ/**/*.py bin/*.py + black setup.py samtranslator/* tests/* tests_integ/* bin/*.py black-check: - black --check setup.py samtranslator/* tests/* tests_integ/**/*.py bin/*.py + black --check setup.py samtranslator/* tests/* tests_integ/* bin/*.py # Command to run everytime you make changes to verify everything works dev: test From 40a7827be96af3b97576e0e2b2a0a03ceae4092b Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Fri, 18 Dec 2020 18:22:57 -0800 Subject: [PATCH 084/105] refactor: Using LogicalIdGenerator.HASH_LENGTH in verify_stack_resources, added simple tests and doc for the table_print utils --- .../helpers/deployer/utils/table_print.py | 92 ++++++++++++------- .../deployer/utils/test_table_print.py | 87 ++++++++++++++++++ tests_integ/helpers/helpers.py | 5 +- 3 files changed, 150 insertions(+), 34 deletions(-) create mode 100644 tests_integ/helpers/deployer/utils/test_table_print.py diff --git a/tests_integ/helpers/deployer/utils/table_print.py b/tests_integ/helpers/deployer/utils/table_print.py index d0ebe39cc..7da9c06bf 100644 --- a/tests_integ/helpers/deployer/utils/table_print.py +++ b/tests_integ/helpers/deployer/utils/table_print.py @@ -17,15 +17,33 @@ def pprint_column_names(format_string, format_kwargs, margin=None, table_header=None, color="yellow"): """ + Prints column names - :param format_string: format string to be used that has the strings, minimum width to be replaced - :param format_kwargs: dictionary that is supplied to the format_string to format the string - :param margin: margin that is to be reduced from column width for columnar text. - :param table_header: Supplied table header - :param color: color supplied for table headers and column names. - :return: boilerplate table string - """ + Parameters + ---------- + format_string : str + format string to be used that has the strings, minimum width to be replaced + format_kwargs : list + dictionary that is supplied to the format_string to format the string + margin : int, optional + margin that is to be reduced from column width for columnar text, by default None + table_header : str, optional + Text to display before the table, by default None + color : str, optional + Table color, by default "yellow" + Returns + ------- + str + Complete table string representation + + Raises + ------ + ValueError + format_kwargs is empty + ValueError + [description] + """ min_width = 100 min_margin = 2 @@ -84,14 +102,21 @@ def wrap(*args, **kwargs): def wrapped_text_generator(texts, width, margin, **textwrap_kwargs): """ + Returns a generator where the contents are wrapped text to a specified width - Return a generator where the contents are wrapped text to a specified width. - - :param texts: list of text that needs to be wrapped at specified width - :param width: width of the text to be wrapped - :param margin: margin to be reduced from width for cleaner UX - :param textwrap_kwargs: keyword arguments that are passed to textwrap.wrap - :return: generator of wrapped text + Parameters + ---------- + texts : list + list of text that needs to be wrapped at specified width + width : int + width of the text to be wrapped + margin : int + margin to be reduced from width for cleaner UX + + Yields + ------- + func + generator of wrapped text """ for text in texts: yield textwrap.wrap(text, width=width - margin, **textwrap_kwargs) @@ -99,18 +124,24 @@ def wrapped_text_generator(texts, width, margin, **textwrap_kwargs): def pprint_columns(columns, width, margin, format_string, format_args, columns_dict, color="yellow", **textwrap_kwargs): """ + Prints columns based on list of columnar text, associated formatting string and associated format arguments. - Print columns based on list of columnar text, associated formatting string and associated format arguments. - - :param columns: list of columnnar text that go into columns as specified by the format_string - :param width: width of the text to be wrapped - :param margin: margin to be reduced from width for cleaner UX - :param format_string: A format string that has both width and text specifiers set. - :param format_args: list of offset specifiers - :param columns_dict: arguments dictionary that have dummy values per column - :param color: color supplied for rows within the table. - :param textwrap_kwargs: keyword arguments that are passed to textwrap.wrap - :return: + Parameters + ---------- + columns : list + List of columnnar text that go into columns as specified by the format_string + width : int + Width of the text to be wrapped + margin : int + Margin to be reduced from width for cleaner UX + format_string : str + Format string that has both width and text specifiers set. + format_args : list + List of offset specifiers + columns_dict : dict + Arguments dictionary that have dummy values per column + color : str, optional + Rows color, by default "yellow" """ for columns_text in zip_longest(*wrapped_text_generator(columns, width, margin, **textwrap_kwargs), fillvalue=""): counter = count() @@ -124,14 +155,13 @@ def pprint_columns(columns, width, margin, format_string, format_args, columns_d def newline_per_item(iterable, counter): """ Adds a new line based on the index of a given iterable + Parameters ---------- - iterable: Any iterable that implements __len__ - counter: Current index within the iterable - - Returns - ------- - + iterable : iterable + Any iterable that implements __len__ + counter : int + Current index within the iterable """ if counter < len(iterable) - 1: click.echo(message="", nl=True) diff --git a/tests_integ/helpers/deployer/utils/test_table_print.py b/tests_integ/helpers/deployer/utils/test_table_print.py new file mode 100644 index 000000000..445de3a6e --- /dev/null +++ b/tests_integ/helpers/deployer/utils/test_table_print.py @@ -0,0 +1,87 @@ +""" +This file is located here because it contains unit tests for integration tests utils +and not unit tests for the application code. +""" +import io +from contextlib import redirect_stdout +from collections import OrderedDict +from unittest import TestCase + +from tests_integ.helpers.deployer.utils.table_print import pprint_column_names, pprint_columns + +TABLE_FORMAT_STRING = "{Alpha:<{0}} {Beta:<{1}} {Gamma:<{2}}" +TABLE_FORMAT_ARGS = OrderedDict({"Alpha": "Alpha", "Beta": "Beta", "Gamma": "Gamma"}) + + +class TestTablePrint(TestCase): + def setUp(self): + self.redirect_out = io.StringIO() + + def test_pprint_column_names(self): + @pprint_column_names(TABLE_FORMAT_STRING, TABLE_FORMAT_ARGS) + def to_be_decorated(*args, **kwargs): + pass + + with redirect_stdout(self.redirect_out): + to_be_decorated() + output = ( + "------------------------------------------------------------------------------------------------\n" + "Alpha Beta Gamma \n" + "------------------------------------------------------------------------------------------------\n" + "------------------------------------------------------------------------------------------------\n" + ) + + self.assertEqual(output, self.redirect_out.getvalue()) + + def test_pprint_column_names_and_text(self): + @pprint_column_names(TABLE_FORMAT_STRING, TABLE_FORMAT_ARGS) + def to_be_decorated(*args, **kwargs): + pprint_columns( + columns=["A", "B", "C"], + width=kwargs["width"], + margin=kwargs["margin"], + format_args=kwargs["format_args"], + format_string=TABLE_FORMAT_STRING, + columns_dict=TABLE_FORMAT_ARGS.copy(), + ) + + with redirect_stdout(self.redirect_out): + to_be_decorated() + + output = ( + "------------------------------------------------------------------------------------------------\n" + "Alpha Beta Gamma \n" + "------------------------------------------------------------------------------------------------\n" + "A B C \n" + "------------------------------------------------------------------------------------------------\n" + ) + self.assertEqual(output, self.redirect_out.getvalue()) + + def test_pprint_exceptions_with_no_column_names(self): + with self.assertRaises(ValueError): + + @pprint_column_names(TABLE_FORMAT_STRING, {}) + def to_be_decorated(*args, **kwargs): + pprint_columns( + columns=["A", "B", "C"], + width=kwargs["width"], + margin=kwargs["margin"], + format_args=kwargs["format_args"], + format_string=TABLE_FORMAT_STRING, + columns_dict=TABLE_FORMAT_ARGS.copy(), + ) + + def test_pprint_exceptions_with_too_many_column_names(self): + massive_dictionary = {str(i): str(i) for i in range(100)} + with self.assertRaises(ValueError): + + @pprint_column_names(TABLE_FORMAT_STRING, massive_dictionary) + def to_be_decorated(*args, **kwargs): + pprint_columns( + columns=["A", "B", "C"], + width=kwargs["width"], + margin=kwargs["margin"], + format_args=kwargs["format_args"], + format_string=TABLE_FORMAT_STRING, + columns_dict=TABLE_FORMAT_ARGS.copy(), + ) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/helpers.py index 94d9c15bc..8ddc42d1d 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/helpers.py @@ -11,10 +11,9 @@ from samtranslator.model.exceptions import InvalidDocumentException from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader from samtranslator.translator.transform import transform +from samtranslator.translator.logical_id_generator import LogicalIdGenerator from samtranslator.yaml_helper import yaml_parse -# Length of the suffix sometimes added by CFN to logical IDs -LOGICAL_RES_SUFFIX_LENGTH = 10 # Length of the random suffix added at the end of the resources we create # to avoid collisions between tests RANDOM_SUFFIX_LENGTH = 12 @@ -79,7 +78,7 @@ def verify_stack_resources(expected_file_path, stack_resources): exp = expected_resources[i] parsed = parsed_resources[i] if not re.match( - "^" + exp["LogicalResourceId"] + "([0-9a-f]{" + str(LOGICAL_RES_SUFFIX_LENGTH) + "})?$", + "^" + exp["LogicalResourceId"] + "([0-9a-f]{" + str(LogicalIdGenerator.HASH_LENGTH) + "})?$", parsed["LogicalResourceId"], ): return False From cc19689f24ed433358c80d6d042f6b2e3d32b76d Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Wed, 23 Dec 2020 10:51:24 -0800 Subject: [PATCH 085/105] fix: Fix table_print tests syntax errors for python2 --- .../deployer/utils/test_table_print.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests_integ/helpers/deployer/utils/test_table_print.py b/tests_integ/helpers/deployer/utils/test_table_print.py index 445de3a6e..147144915 100644 --- a/tests_integ/helpers/deployer/utils/test_table_print.py +++ b/tests_integ/helpers/deployer/utils/test_table_print.py @@ -3,7 +3,8 @@ and not unit tests for the application code. """ import io -from contextlib import redirect_stdout +import contextlib +import sys from collections import OrderedDict from unittest import TestCase @@ -13,9 +14,19 @@ TABLE_FORMAT_ARGS = OrderedDict({"Alpha": "Alpha", "Beta": "Beta", "Gamma": "Gamma"}) +@contextlib.contextmanager +def redirect_stdout(target): + original = sys.stdout + try: + sys.stdout = target + yield + finally: + sys.stdout = original + + class TestTablePrint(TestCase): def setUp(self): - self.redirect_out = io.StringIO() + self.redirect_out = io.BytesIO() def test_pprint_column_names(self): @pprint_column_names(TABLE_FORMAT_STRING, TABLE_FORMAT_ARGS) @@ -31,7 +42,7 @@ def to_be_decorated(*args, **kwargs): "------------------------------------------------------------------------------------------------\n" ) - self.assertEqual(output, self.redirect_out.getvalue()) + self.assertEqual(output, self.redirect_out.getvalue().decode("UTF-8")) def test_pprint_column_names_and_text(self): @pprint_column_names(TABLE_FORMAT_STRING, TABLE_FORMAT_ARGS) @@ -55,7 +66,7 @@ def to_be_decorated(*args, **kwargs): "A B C \n" "------------------------------------------------------------------------------------------------\n" ) - self.assertEqual(output, self.redirect_out.getvalue()) + self.assertEqual(output, self.redirect_out.getvalue().decode("UTF-8")) def test_pprint_exceptions_with_no_column_names(self): with self.assertRaises(ValueError): From 2fc0bb1af72cfc530cd7323897e783f24a707c8c Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Wed, 23 Dec 2020 13:42:31 -0800 Subject: [PATCH 086/105] refactor: Removed the table_print tests which are complicated to port to python2, added a comment in the deployer classes ported over from sam-cli to underline it --- tests_integ/helpers/deployer/deployer.py | 1 + .../helpers/deployer/exceptions/exceptions.py | 1 + .../deployer/utils/artifact_exporter.py | 1 + tests_integ/helpers/deployer/utils/colors.py | 1 + .../helpers/deployer/utils/table_print.py | 1 + .../deployer/utils/test_table_print.py | 98 ------------------- tests_integ/helpers/deployer/utils/time.py | 1 + 7 files changed, 6 insertions(+), 98 deletions(-) delete mode 100644 tests_integ/helpers/deployer/utils/test_table_print.py diff --git a/tests_integ/helpers/deployer/deployer.py b/tests_integ/helpers/deployer/deployer.py index d1928fe5a..20299e030 100644 --- a/tests_integ/helpers/deployer/deployer.py +++ b/tests_integ/helpers/deployer/deployer.py @@ -1,5 +1,6 @@ """ Cloudformation deploy class which also streams events and changeset information +This was ported over from the sam-cli repo """ # Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/tests_integ/helpers/deployer/exceptions/exceptions.py b/tests_integ/helpers/deployer/exceptions/exceptions.py index bbb17ea5e..3dee92caa 100644 --- a/tests_integ/helpers/deployer/exceptions/exceptions.py +++ b/tests_integ/helpers/deployer/exceptions/exceptions.py @@ -1,5 +1,6 @@ """ Exceptions that are raised by sam deploy +This was ported over from the sam-cli repo """ import click diff --git a/tests_integ/helpers/deployer/utils/artifact_exporter.py b/tests_integ/helpers/deployer/utils/artifact_exporter.py index c38ecbce8..6a06dfdbb 100644 --- a/tests_integ/helpers/deployer/utils/artifact_exporter.py +++ b/tests_integ/helpers/deployer/utils/artifact_exporter.py @@ -1,5 +1,6 @@ """ Logic for uploading to S3 per Cloudformation Specific Resource +This was ported over from the sam-cli repo """ # pylint: disable=no-member diff --git a/tests_integ/helpers/deployer/utils/colors.py b/tests_integ/helpers/deployer/utils/colors.py index a5a7b480b..a653e2253 100644 --- a/tests_integ/helpers/deployer/utils/colors.py +++ b/tests_integ/helpers/deployer/utils/colors.py @@ -1,5 +1,6 @@ """ Wrapper to generated colored messages for printing in Terminal +This was ported over from the sam-cli repo """ import click diff --git a/tests_integ/helpers/deployer/utils/table_print.py b/tests_integ/helpers/deployer/utils/table_print.py index 7da9c06bf..4cc27d6f3 100644 --- a/tests_integ/helpers/deployer/utils/table_print.py +++ b/tests_integ/helpers/deployer/utils/table_print.py @@ -1,5 +1,6 @@ """ Utilities for table pretty printing using click +This was ported over from the sam-cli repo """ from itertools import count diff --git a/tests_integ/helpers/deployer/utils/test_table_print.py b/tests_integ/helpers/deployer/utils/test_table_print.py deleted file mode 100644 index 147144915..000000000 --- a/tests_integ/helpers/deployer/utils/test_table_print.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -This file is located here because it contains unit tests for integration tests utils -and not unit tests for the application code. -""" -import io -import contextlib -import sys -from collections import OrderedDict -from unittest import TestCase - -from tests_integ.helpers.deployer.utils.table_print import pprint_column_names, pprint_columns - -TABLE_FORMAT_STRING = "{Alpha:<{0}} {Beta:<{1}} {Gamma:<{2}}" -TABLE_FORMAT_ARGS = OrderedDict({"Alpha": "Alpha", "Beta": "Beta", "Gamma": "Gamma"}) - - -@contextlib.contextmanager -def redirect_stdout(target): - original = sys.stdout - try: - sys.stdout = target - yield - finally: - sys.stdout = original - - -class TestTablePrint(TestCase): - def setUp(self): - self.redirect_out = io.BytesIO() - - def test_pprint_column_names(self): - @pprint_column_names(TABLE_FORMAT_STRING, TABLE_FORMAT_ARGS) - def to_be_decorated(*args, **kwargs): - pass - - with redirect_stdout(self.redirect_out): - to_be_decorated() - output = ( - "------------------------------------------------------------------------------------------------\n" - "Alpha Beta Gamma \n" - "------------------------------------------------------------------------------------------------\n" - "------------------------------------------------------------------------------------------------\n" - ) - - self.assertEqual(output, self.redirect_out.getvalue().decode("UTF-8")) - - def test_pprint_column_names_and_text(self): - @pprint_column_names(TABLE_FORMAT_STRING, TABLE_FORMAT_ARGS) - def to_be_decorated(*args, **kwargs): - pprint_columns( - columns=["A", "B", "C"], - width=kwargs["width"], - margin=kwargs["margin"], - format_args=kwargs["format_args"], - format_string=TABLE_FORMAT_STRING, - columns_dict=TABLE_FORMAT_ARGS.copy(), - ) - - with redirect_stdout(self.redirect_out): - to_be_decorated() - - output = ( - "------------------------------------------------------------------------------------------------\n" - "Alpha Beta Gamma \n" - "------------------------------------------------------------------------------------------------\n" - "A B C \n" - "------------------------------------------------------------------------------------------------\n" - ) - self.assertEqual(output, self.redirect_out.getvalue().decode("UTF-8")) - - def test_pprint_exceptions_with_no_column_names(self): - with self.assertRaises(ValueError): - - @pprint_column_names(TABLE_FORMAT_STRING, {}) - def to_be_decorated(*args, **kwargs): - pprint_columns( - columns=["A", "B", "C"], - width=kwargs["width"], - margin=kwargs["margin"], - format_args=kwargs["format_args"], - format_string=TABLE_FORMAT_STRING, - columns_dict=TABLE_FORMAT_ARGS.copy(), - ) - - def test_pprint_exceptions_with_too_many_column_names(self): - massive_dictionary = {str(i): str(i) for i in range(100)} - with self.assertRaises(ValueError): - - @pprint_column_names(TABLE_FORMAT_STRING, massive_dictionary) - def to_be_decorated(*args, **kwargs): - pprint_columns( - columns=["A", "B", "C"], - width=kwargs["width"], - margin=kwargs["margin"], - format_args=kwargs["format_args"], - format_string=TABLE_FORMAT_STRING, - columns_dict=TABLE_FORMAT_ARGS.copy(), - ) diff --git a/tests_integ/helpers/deployer/utils/time.py b/tests_integ/helpers/deployer/utils/time.py index 02b078337..6a5706aae 100644 --- a/tests_integ/helpers/deployer/utils/time.py +++ b/tests_integ/helpers/deployer/utils/time.py @@ -1,5 +1,6 @@ """ Date & Time related utilities +This was ported over from the sam-cli repo """ import datetime From 463e1e64a04c5769ce0adae11717c0835b74f3c3 Mon Sep 17 00:00:00 2001 From: Mathieu Grandis Date: Wed, 23 Dec 2020 14:56:53 -0800 Subject: [PATCH 087/105] refactor: Revert newline removal at the end of appveyor.yml --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index a967bcea1..d20b784b9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,4 +20,4 @@ install: - make init test_script: -- tox \ No newline at end of file +- tox From 886c692d379b868ab49c9ea49f75e6ee7d77c3d1 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Wed, 6 Jan 2021 16:56:09 -0800 Subject: [PATCH 088/105] for testing appveyor only, will revert later --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 78a98c6be..f3fc5ed54 100755 --- a/Makefile +++ b/Makefile @@ -9,7 +9,8 @@ test: pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests/* test-integ: - pytest --no-cov tests_integ/* + $(info ${AWS_ACCESS_KEY_ID}) +# pytest --no-cov tests_integ/* black: black setup.py samtranslator/* tests/* tests_integ/* bin/*.py From e04853f3afd41fc72a60fcaca122adaf839b9fde Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Wed, 6 Jan 2021 17:15:55 -0800 Subject: [PATCH 089/105] Revert "for testing appveyor only, will revert later" This reverts commit 886c692d379b868ab49c9ea49f75e6ee7d77c3d1. --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f3fc5ed54..78a98c6be 100755 --- a/Makefile +++ b/Makefile @@ -9,8 +9,7 @@ test: pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests/* test-integ: - $(info ${AWS_ACCESS_KEY_ID}) -# pytest --no-cov tests_integ/* + pytest --no-cov tests_integ/* black: black setup.py samtranslator/* tests/* tests_integ/* bin/*.py From bccea069a8f796a7aea2283316bc2226d0003b14 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Wed, 6 Jan 2021 23:32:13 -0800 Subject: [PATCH 090/105] test appveyor failure, will revert later --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 947a4c3bb..b5b722c5a 100644 --- a/tox.ini +++ b/tox.ini @@ -26,6 +26,7 @@ commands = make pr2.7 [testenv] commands = make pr codecov + echo $AWS_ACCESS_KEY_ID make test-integ deps = codecov>=1.4.0 passenv = AWS* TONXENV CI TRAVIS TRAVIS_* CODECOV_TOKEN From 26eed84154493456b23f854966b889bf14f75936 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Wed, 6 Jan 2021 23:41:36 -0800 Subject: [PATCH 091/105] Revert "test appveyor failure, will revert later" This reverts commit bccea069a8f796a7aea2283316bc2226d0003b14. --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index b5b722c5a..947a4c3bb 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,6 @@ commands = make pr2.7 [testenv] commands = make pr codecov - echo $AWS_ACCESS_KEY_ID make test-integ deps = codecov>=1.4.0 passenv = AWS* TONXENV CI TRAVIS TRAVIS_* CODECOV_TOKEN From 8711cfa96f3ac5858575887a939b47366c6b4511 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 8 Jan 2021 13:46:57 -0800 Subject: [PATCH 092/105] just for testing appveyor failure, will revert later --- appveyor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index d20b784b9..d0827cc79 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -21,3 +21,7 @@ install: test_script: - tox + +on_finish: + - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + From 35b5b09ffadb764dc41249ce631065ab8ebb8a6e Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 8 Jan 2021 14:47:03 -0800 Subject: [PATCH 093/105] for testing only, will revert later, add ssh access to appveyor vm --- appveyor.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index d0827cc79..7add582e0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,5 +23,7 @@ test_script: - tox on_finish: - - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + - sh: export APPVEYOR_SSH_BLOCK=true + - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - + From 8237cbf65dd2fc45a8d82c1353c1153b43fda204 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 8 Jan 2021 15:13:24 -0800 Subject: [PATCH 094/105] for testing only, will revert later --- appveyor.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 7add582e0..b2187aa73 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,6 +12,9 @@ environment: - TOXENV: py38 PYTHON_VERSION: '3.8' +init: + - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - + build: off install: @@ -22,8 +25,4 @@ install: test_script: - tox -on_finish: - - sh: export APPVEYOR_SSH_BLOCK=true - - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - - From 9de384eb6d566b2309c20726f0eae14fe60420c5 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 8 Jan 2021 15:25:08 -0800 Subject: [PATCH 095/105] Revert "for testing only, will revert later" This reverts commit 8237cbf65dd2fc45a8d82c1353c1153b43fda204. --- appveyor.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index b2187aa73..7add582e0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,9 +12,6 @@ environment: - TOXENV: py38 PYTHON_VERSION: '3.8' -init: - - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - - build: off install: @@ -25,4 +22,8 @@ install: test_script: - tox +on_finish: + - sh: export APPVEYOR_SSH_BLOCK=true + - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - + From 3aaa34c96a1314cdc2ef88fae602970b1a53cd68 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 8 Jan 2021 15:42:55 -0800 Subject: [PATCH 096/105] revert all the previous test changes --- appveyor.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 7add582e0..9881fe407 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,8 +22,3 @@ install: test_script: - tox -on_finish: - - sh: export APPVEYOR_SSH_BLOCK=true - - sh: curl -sflL 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-ssh.sh' | bash -e - - - From 12bc745af6ed610e605a742d56ef2bf187b38a82 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Mon, 11 Jan 2021 10:26:22 -0800 Subject: [PATCH 097/105] create a appveyor config file for integration test --- appveyor-integration-test.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 appveyor-integration-test.yml diff --git a/appveyor-integration-test.yml b/appveyor-integration-test.yml new file mode 100644 index 000000000..c3cf9ec34 --- /dev/null +++ b/appveyor-integration-test.yml @@ -0,0 +1,24 @@ +version: 1.0.{build} +image: Ubuntu + +environment: + matrix: + - TOXENV: py27 + PYTHON_VERSION: '2.7' + - TOXENV: py36 + PYTHON_VERSION: '3.6' + - TOXENV: py37 + PYTHON_VERSION: '3.7' + - TOXENV: py38 + PYTHON_VERSION: '3.8' + +build: off + +install: +- sh: "source ${HOME}/venv${PYTHON_VERSION}/bin/activate" +- sh: "python --version" +- make init + +test_script: +- make test-integ + From 8f33c82d4013a017a6c5c13638313b2d18a52f37 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Mon, 11 Jan 2021 15:42:33 -0800 Subject: [PATCH 098/105] move integration test to a separate appveyor yml file --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index 947a4c3bb..63aea07e2 100644 --- a/tox.ini +++ b/tox.ini @@ -21,12 +21,10 @@ passenv = AWS* CODECOV_TOKEN setenv = PYTHONHASHSEED = 0 commands = make pr2.7 codecov - make test-integ [testenv] commands = make pr codecov - make test-integ deps = codecov>=1.4.0 passenv = AWS* TONXENV CI TRAVIS TRAVIS_* CODECOV_TOKEN whitelist_externals = make, black, codecov From 6806e785e896b91711bc9c8243085b5d12aaea64 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Mon, 11 Jan 2021 17:50:38 -0800 Subject: [PATCH 099/105] delete extra blank line --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 9881fe407..d20b784b9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -21,4 +21,3 @@ install: test_script: - tox - From 3d380b8789f3a0cb97e880e89c9275cb3860a4cf Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Tue, 12 Jan 2021 13:43:21 -0800 Subject: [PATCH 100/105] increase retry times to bypass Throttling --- tests_integ/helpers/base_test.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index c81321a51..66083dc16 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -11,6 +11,7 @@ import pytest import yaml from botocore.exceptions import ClientError +from botocore.config import Config from tests_integ.helpers.deployer.deployer import Deployer from tests_integ.helpers.helpers import transform_template, verify_stack_resources, generate_suffix, create_bucket from tests_integ.helpers.file_resources import FILE_TO_S3_URI_MAP, CODE_KEY_TO_FILE_MAP @@ -113,7 +114,13 @@ def _get_s3_uri(cls, file_name, uri_type): return "https://s3-{}.amazonaws.com/{}/{}".format(cls.my_region, cls.s3_bucket_name, file_name) def setUp(self): - self.cloudformation_client = boto3.client("cloudformation") + config = Config( + retries={ + 'max_attempts': 10, + 'mode': 'standard' + } + ) + self.cloudformation_client = boto3.client("cloudformation", config=config) self.deployer = Deployer(self.cloudformation_client) def tearDown(self): From d88bdddcd147a18312cd1d9b2319b947c187c593 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Tue, 12 Jan 2021 13:47:10 -0800 Subject: [PATCH 101/105] black reformatting --- tests_integ/helpers/base_test.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 66083dc16..31c1b741e 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -114,12 +114,7 @@ def _get_s3_uri(cls, file_name, uri_type): return "https://s3-{}.amazonaws.com/{}/{}".format(cls.my_region, cls.s3_bucket_name, file_name) def setUp(self): - config = Config( - retries={ - 'max_attempts': 10, - 'mode': 'standard' - } - ) + config = Config(retries={"max_attempts": 10, "mode": "standard"}) self.cloudformation_client = boto3.client("cloudformation", config=config) self.deployer = Deployer(self.cloudformation_client) From 0bddb9a681be3f72da6e63575559634b2a87e2d0 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Thu, 14 Jan 2021 20:20:42 -0800 Subject: [PATCH 102/105] refactor helper.py to have meaningful class name --- tests_integ/helpers/base_test.py | 5 ++- .../helpers/{helpers.py => resource.py} | 38 ----------------- tests_integ/helpers/template.py | 42 +++++++++++++++++++ 3 files changed, 46 insertions(+), 39 deletions(-) rename tests_integ/helpers/{helpers.py => resource.py} (67%) create mode 100644 tests_integ/helpers/template.py diff --git a/tests_integ/helpers/base_test.py b/tests_integ/helpers/base_test.py index 31c1b741e..d080b5d49 100644 --- a/tests_integ/helpers/base_test.py +++ b/tests_integ/helpers/base_test.py @@ -1,6 +1,8 @@ import logging import os +from tests_integ.helpers.resource import generate_suffix, create_bucket, verify_stack_resources + try: from pathlib import Path except ImportError: @@ -13,7 +15,8 @@ from botocore.exceptions import ClientError from botocore.config import Config from tests_integ.helpers.deployer.deployer import Deployer -from tests_integ.helpers.helpers import transform_template, verify_stack_resources, generate_suffix, create_bucket +from tests_integ.helpers.template import transform_template + from tests_integ.helpers.file_resources import FILE_TO_S3_URI_MAP, CODE_KEY_TO_FILE_MAP LOG = logging.getLogger(__name__) diff --git a/tests_integ/helpers/helpers.py b/tests_integ/helpers/resource.py similarity index 67% rename from tests_integ/helpers/helpers.py rename to tests_integ/helpers/resource.py index 8ddc42d1d..8a32a1de6 100644 --- a/tests_integ/helpers/helpers.py +++ b/tests_integ/helpers/resource.py @@ -1,56 +1,18 @@ import json -import logging import re import random import string # pylint: disable=deprecated-module -from functools import reduce import boto3 from botocore.exceptions import ClientError, NoRegionError -from samtranslator.model.exceptions import InvalidDocumentException -from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader -from samtranslator.translator.transform import transform from samtranslator.translator.logical_id_generator import LogicalIdGenerator -from samtranslator.yaml_helper import yaml_parse # Length of the random suffix added at the end of the resources we create # to avoid collisions between tests RANDOM_SUFFIX_LENGTH = 12 -def transform_template(sam_template_path, cfn_output_path): - """ - Locally transforms a SAM template to a Cloud Formation template - - Parameters - ---------- - sam_template_path : Path - SAM template input path - cfn_output_path : Path - Cloud formation template output path - """ - LOG = logging.getLogger(__name__) - iam_client = boto3.client("iam") - - with open(sam_template_path) as f: - sam_template = yaml_parse(f) - - try: - cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) - cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) - - with open(cfn_output_path, "w") as f: - f.write(cloud_formation_template_prettified) - - print("Wrote transformed CloudFormation template to: " + cfn_output_path) - except InvalidDocumentException as e: - error_message = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) - LOG.error(error_message) - errors = map(lambda cause: cause.message, e.causes) - LOG.error(errors) - - def verify_stack_resources(expected_file_path, stack_resources): """ Verifies that the stack resources match the expected ones diff --git a/tests_integ/helpers/template.py b/tests_integ/helpers/template.py new file mode 100644 index 000000000..f75bc4c56 --- /dev/null +++ b/tests_integ/helpers/template.py @@ -0,0 +1,42 @@ +import json +import logging +from functools import reduce + +import boto3 + +from samtranslator.model.exceptions import InvalidDocumentException +from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader +from samtranslator.translator.transform import transform +from samtranslator.yaml_helper import yaml_parse + + +def transform_template(sam_template_path, cfn_output_path): + """ + Locally transforms a SAM template to a Cloud Formation template + + Parameters + ---------- + sam_template_path : Path + SAM template input path + cfn_output_path : Path + Cloud formation template output path + """ + LOG = logging.getLogger(__name__) + iam_client = boto3.client("iam") + + with open(sam_template_path) as f: + sam_template = yaml_parse(f) + + try: + cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) + cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) + + with open(cfn_output_path, "w") as f: + f.write(cloud_formation_template_prettified) + + print("Wrote transformed CloudFormation template to: " + cfn_output_path) + except InvalidDocumentException as e: + error_message = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) + LOG.error(error_message) + errors = map(lambda cause: cause.message, e.causes) + LOG.error(errors) From 48489900863eed17cc028b881a0108e972197d64 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 15 Jan 2021 09:43:38 -0800 Subject: [PATCH 103/105] rename folder name and make command name to have the sam style with samcli --- INTEGRATION_TESTS.md | 17 +++++++++-------- Makefile | 10 +++++----- appveyor-integration-test.yml | 2 +- {tests_integ => integration}/__init__.py | 0 .../helpers/__init__.py | 0 .../helpers/base_test.py | 8 ++++---- .../helpers/deployer/__init__.py | 0 .../helpers/deployer/deployer.py | 10 +++++----- .../helpers/deployer/exceptions/__init__.py | 0 .../helpers/deployer/exceptions/exceptions.py | 0 .../helpers/deployer/utils/__init__.py | 0 .../deployer/utils/artifact_exporter.py | 0 .../helpers/deployer/utils/colors.py | 0 .../helpers/deployer/utils/table_print.py | 0 .../helpers/deployer/utils/time.py | 0 .../helpers/file_resources.py | 0 .../helpers/resource.py | 0 .../helpers/template.py | 0 .../resources/code/code.zip | Bin .../resources/code/layer1.zip | Bin .../resources/code/swagger1.json | 0 .../resources/code/swagger2.json | 0 .../resources/code/template.yaml | 0 .../resources/expected/single/basic_api.json | 0 .../single/basic_api_inline_openapi.json | 0 .../single/basic_api_inline_swagger.json | 0 .../single/basic_api_inline_with_cache.json | 0 .../single/basic_api_inline_with_tags.json | 0 .../expected/single/basic_api_with_tags.json | 0 .../single/basic_application_s3_location.json | 0 .../single/basic_application_sar_location.json | 0 ...plication_sar_location_with_intrinsics.json | 0 .../expected/single/basic_function.json | 0 .../basic_function_event_destinations.json | 0 .../single/basic_function_no_envvar.json | 0 .../single/basic_function_openapi.json | 0 .../single/basic_function_with_kmskeyarn.json | 0 .../single/basic_function_with_sns_dlq.json | 0 .../single/basic_function_with_sqs_dlq.json | 0 .../single/basic_function_with_tags.json | 0 .../single/basic_function_with_tracing.json | 0 .../expected/single/basic_http_api.json | 0 .../resources/expected/single/basic_layer.json | 0 .../single/basic_layer_with_parameters.json | 0 .../basic_state_machine_inline_definition.json | 0 .../single/basic_state_machine_with_tags.json | 0 .../resources/templates/single/basic_api.yaml | 0 .../single/basic_api_inline_openapi.yaml | 0 .../single/basic_api_inline_swagger.yaml | 0 .../templates/single/basic_api_with_tags.yaml | 0 .../single/basic_application_s3_location.yaml | 0 .../single/basic_application_sar_location.yaml | 0 ...plication_sar_location_with_intrinsics.yaml | 0 .../templates/single/basic_function.yaml | 0 .../basic_function_event_destinations.yaml | 0 .../single/basic_function_no_envvar.yaml | 0 .../single/basic_function_openapi.yaml | 0 .../single/basic_function_with_kmskeyarn.yaml | 0 .../single/basic_function_with_sns_dlq.yaml | 0 .../single/basic_function_with_sqs_dlq.yaml | 0 .../single/basic_function_with_tags.yaml | 0 .../single/basic_function_with_tracing.yaml | 0 .../templates/single/basic_http_api.yaml | 0 .../templates/single/basic_layer.yaml | 0 .../single/basic_layer_with_parameters.yaml | 0 .../basic_state_machine_inline_definition.yaml | 0 .../single/basic_state_machine_with_tags.yaml | 0 .../templates/single/basic_table_no_param.yaml | 0 .../single/basic_table_with_param.yaml | 0 .../single/__init__.py | 0 .../single/test_basic_api.py | 2 +- .../single/test_basic_application.py | 2 +- .../single/test_basic_function.py | 2 +- .../single/test_basic_http_api.py | 2 +- .../single/test_basic_layer_version.py | 2 +- .../single/test_basic_state_machine.py | 2 +- 76 files changed, 30 insertions(+), 29 deletions(-) rename {tests_integ => integration}/__init__.py (100%) rename {tests_integ => integration}/helpers/__init__.py (100%) rename {tests_integ => integration}/helpers/base_test.py (98%) rename {tests_integ => integration}/helpers/deployer/__init__.py (100%) rename {tests_integ => integration}/helpers/deployer/deployer.py (98%) rename {tests_integ => integration}/helpers/deployer/exceptions/__init__.py (100%) rename {tests_integ => integration}/helpers/deployer/exceptions/exceptions.py (100%) rename {tests_integ => integration}/helpers/deployer/utils/__init__.py (100%) rename {tests_integ => integration}/helpers/deployer/utils/artifact_exporter.py (100%) rename {tests_integ => integration}/helpers/deployer/utils/colors.py (100%) rename {tests_integ => integration}/helpers/deployer/utils/table_print.py (100%) rename {tests_integ => integration}/helpers/deployer/utils/time.py (100%) rename {tests_integ => integration}/helpers/file_resources.py (100%) rename {tests_integ => integration}/helpers/resource.py (100%) rename {tests_integ => integration}/helpers/template.py (100%) rename {tests_integ => integration}/resources/code/code.zip (100%) rename {tests_integ => integration}/resources/code/layer1.zip (100%) rename {tests_integ => integration}/resources/code/swagger1.json (100%) rename {tests_integ => integration}/resources/code/swagger2.json (100%) rename {tests_integ => integration}/resources/code/template.yaml (100%) rename {tests_integ => integration}/resources/expected/single/basic_api.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_api_inline_openapi.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_api_inline_swagger.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_api_inline_with_cache.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_api_inline_with_tags.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_api_with_tags.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_application_s3_location.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_application_sar_location.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_application_sar_location_with_intrinsics.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_function.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_function_event_destinations.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_function_no_envvar.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_function_openapi.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_function_with_kmskeyarn.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_function_with_sns_dlq.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_function_with_sqs_dlq.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_function_with_tags.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_function_with_tracing.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_http_api.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_layer.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_layer_with_parameters.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_state_machine_inline_definition.json (100%) rename {tests_integ => integration}/resources/expected/single/basic_state_machine_with_tags.json (100%) rename {tests_integ => integration}/resources/templates/single/basic_api.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_api_inline_openapi.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_api_inline_swagger.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_api_with_tags.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_application_s3_location.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_application_sar_location.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_function.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_function_event_destinations.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_function_no_envvar.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_function_openapi.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_function_with_kmskeyarn.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_function_with_sns_dlq.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_function_with_sqs_dlq.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_function_with_tags.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_function_with_tracing.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_http_api.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_layer.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_layer_with_parameters.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_state_machine_inline_definition.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_state_machine_with_tags.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_table_no_param.yaml (100%) rename {tests_integ => integration}/resources/templates/single/basic_table_with_param.yaml (100%) rename {tests_integ => integration}/single/__init__.py (100%) rename {tests_integ => integration}/single/test_basic_api.py (98%) rename {tests_integ => integration}/single/test_basic_application.py (97%) rename {tests_integ => integration}/single/test_basic_function.py (99%) rename {tests_integ => integration}/single/test_basic_http_api.py (88%) rename {tests_integ => integration}/single/test_basic_layer_version.py (96%) rename {tests_integ => integration}/single/test_basic_state_machine.py (96%) diff --git a/INTEGRATION_TESTS.md b/INTEGRATION_TESTS.md index 9e4d72288..c326c908b 100644 --- a/INTEGRATION_TESTS.md +++ b/INTEGRATION_TESTS.md @@ -39,7 +39,7 @@ make init From the root of the repository, run: ``` -make test-integ +make integ-test ``` ### Running a specific test file @@ -53,7 +53,7 @@ pytest --no-cov path/to/the/test_file.py For example, from the root of the project: ```sh -pytest --no-cov tests_integ/single/test_basic_api.py +pytest --no-cov integration/single/test_basic_api.py ``` ### Running a specific test @@ -67,17 +67,17 @@ pytest --no-cov path/to/the/test_file.py::test_class::test_method For example, from the root of the project: ```sh -pytest --no-cov tests_integ/single/test_basic_api.py::TestBasicApi::test_basic_api +pytest --no-cov integration/single/test_basic_api.py::TestBasicApi::test_basic_api ``` *We don't measure coverage for integration tests.* ## Write a test -1. Add your test templates to the `tests_integ/resources/templates` single or combination folder. -2. Write an expected json file for all the expected resources and add it to the `tests_integ/resources/expected`. -3. (Optional) Add the resource files (zip, json, etc.) to `tests_integ/resources/code` and update the dictionaries in `tests_integ/helpers/file_resources.py`. -4. Write and add your python test code to the `tests_integ` single or combination folder. +1. Add your test templates to the `integration/resources/templates` single or combination folder. +2. Write an expected json file for all the expected resources and add it to the `integration/resources/expected`. +3. (Optional) Add the resource files (zip, json, etc.) to `integration/resources/code` and update the dictionaries in `integration/helpers/file_resources.py`. +4. Write and add your python test code to the `integration` single or combination folder. 5. Run it! ## Directory structure @@ -91,7 +91,8 @@ Common classes and tools used by tests. | +-- deployer Tools to deploy to Cloud Formation | +-- base_test.py Common class from which all test classes inherit | +-- file_resources.py Files to upload to S3 -| +-- helpers.py Miscellaneous helpers +| +-- resource.py Helper functions to manipulate resources +| +-- template.py Helper functions to translate the template ``` `base_test.py` contains `setUpClass` and `tearDownClass` methods to respectively upload and clean the `file_resources.py` resources (upload the files to a new S3 bucket, empty and delete this bucket). diff --git a/Makefile b/Makefile index 78a98c6be..09dce252d 100755 --- a/Makefile +++ b/Makefile @@ -8,14 +8,14 @@ init: test: pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests/* -test-integ: - pytest --no-cov tests_integ/* +integ-test: + pytest --no-cov integration/* black: - black setup.py samtranslator/* tests/* tests_integ/* bin/*.py + black setup.py samtranslator/* tests/* integration/* bin/*.py black-check: - black --check setup.py samtranslator/* tests/* tests_integ/* bin/*.py + black --check setup.py samtranslator/* tests/* integration/* bin/*.py # Command to run everytime you make changes to verify everything works dev: test @@ -33,7 +33,7 @@ Usage: $ make [TARGETS] TARGETS init Initialize and install the requirements and dev-requirements for this project. test Run the Unit tests. - test-integ Run the Integration tests. + integ-test Run the Integration tests. dev Run all development tests after a change. pr Perform all checks before submitting a Pull Request. diff --git a/appveyor-integration-test.yml b/appveyor-integration-test.yml index c3cf9ec34..0dc1cdf12 100644 --- a/appveyor-integration-test.yml +++ b/appveyor-integration-test.yml @@ -20,5 +20,5 @@ install: - make init test_script: -- make test-integ +- make integ-test diff --git a/tests_integ/__init__.py b/integration/__init__.py similarity index 100% rename from tests_integ/__init__.py rename to integration/__init__.py diff --git a/tests_integ/helpers/__init__.py b/integration/helpers/__init__.py similarity index 100% rename from tests_integ/helpers/__init__.py rename to integration/helpers/__init__.py diff --git a/tests_integ/helpers/base_test.py b/integration/helpers/base_test.py similarity index 98% rename from tests_integ/helpers/base_test.py rename to integration/helpers/base_test.py index d080b5d49..99b7871ce 100644 --- a/tests_integ/helpers/base_test.py +++ b/integration/helpers/base_test.py @@ -1,7 +1,7 @@ import logging import os -from tests_integ.helpers.resource import generate_suffix, create_bucket, verify_stack_resources +from integration.helpers.resource import generate_suffix, create_bucket, verify_stack_resources try: from pathlib import Path @@ -14,10 +14,10 @@ import yaml from botocore.exceptions import ClientError from botocore.config import Config -from tests_integ.helpers.deployer.deployer import Deployer -from tests_integ.helpers.template import transform_template +from integration.helpers.deployer.deployer import Deployer +from integration.helpers.template import transform_template -from tests_integ.helpers.file_resources import FILE_TO_S3_URI_MAP, CODE_KEY_TO_FILE_MAP +from integration.helpers.file_resources import FILE_TO_S3_URI_MAP, CODE_KEY_TO_FILE_MAP LOG = logging.getLogger(__name__) STACK_NAME_PREFIX = "sam-integ-stack-" diff --git a/tests_integ/helpers/deployer/__init__.py b/integration/helpers/deployer/__init__.py similarity index 100% rename from tests_integ/helpers/deployer/__init__.py rename to integration/helpers/deployer/__init__.py diff --git a/tests_integ/helpers/deployer/deployer.py b/integration/helpers/deployer/deployer.py similarity index 98% rename from tests_integ/helpers/deployer/deployer.py rename to integration/helpers/deployer/deployer.py index 20299e030..4cb0de31f 100644 --- a/tests_integ/helpers/deployer/deployer.py +++ b/integration/helpers/deployer/deployer.py @@ -34,16 +34,16 @@ import botocore -from tests_integ.helpers.deployer.utils.colors import DeployColor -from tests_integ.helpers.deployer.exceptions import exceptions as deploy_exceptions -from tests_integ.helpers.deployer.utils.table_print import ( +from integration.helpers.deployer.utils.colors import DeployColor +from integration.helpers.deployer.exceptions import exceptions as deploy_exceptions +from integration.helpers.deployer.utils.table_print import ( pprint_column_names, pprint_columns, newline_per_item, MIN_OFFSET, ) -from tests_integ.helpers.deployer.utils.artifact_exporter import mktempfile, parse_s3_url -from tests_integ.helpers.deployer.utils.time import utc_to_timestamp +from integration.helpers.deployer.utils.artifact_exporter import mktempfile, parse_s3_url +from integration.helpers.deployer.utils.time import utc_to_timestamp LOG = logging.getLogger(__name__) diff --git a/tests_integ/helpers/deployer/exceptions/__init__.py b/integration/helpers/deployer/exceptions/__init__.py similarity index 100% rename from tests_integ/helpers/deployer/exceptions/__init__.py rename to integration/helpers/deployer/exceptions/__init__.py diff --git a/tests_integ/helpers/deployer/exceptions/exceptions.py b/integration/helpers/deployer/exceptions/exceptions.py similarity index 100% rename from tests_integ/helpers/deployer/exceptions/exceptions.py rename to integration/helpers/deployer/exceptions/exceptions.py diff --git a/tests_integ/helpers/deployer/utils/__init__.py b/integration/helpers/deployer/utils/__init__.py similarity index 100% rename from tests_integ/helpers/deployer/utils/__init__.py rename to integration/helpers/deployer/utils/__init__.py diff --git a/tests_integ/helpers/deployer/utils/artifact_exporter.py b/integration/helpers/deployer/utils/artifact_exporter.py similarity index 100% rename from tests_integ/helpers/deployer/utils/artifact_exporter.py rename to integration/helpers/deployer/utils/artifact_exporter.py diff --git a/tests_integ/helpers/deployer/utils/colors.py b/integration/helpers/deployer/utils/colors.py similarity index 100% rename from tests_integ/helpers/deployer/utils/colors.py rename to integration/helpers/deployer/utils/colors.py diff --git a/tests_integ/helpers/deployer/utils/table_print.py b/integration/helpers/deployer/utils/table_print.py similarity index 100% rename from tests_integ/helpers/deployer/utils/table_print.py rename to integration/helpers/deployer/utils/table_print.py diff --git a/tests_integ/helpers/deployer/utils/time.py b/integration/helpers/deployer/utils/time.py similarity index 100% rename from tests_integ/helpers/deployer/utils/time.py rename to integration/helpers/deployer/utils/time.py diff --git a/tests_integ/helpers/file_resources.py b/integration/helpers/file_resources.py similarity index 100% rename from tests_integ/helpers/file_resources.py rename to integration/helpers/file_resources.py diff --git a/tests_integ/helpers/resource.py b/integration/helpers/resource.py similarity index 100% rename from tests_integ/helpers/resource.py rename to integration/helpers/resource.py diff --git a/tests_integ/helpers/template.py b/integration/helpers/template.py similarity index 100% rename from tests_integ/helpers/template.py rename to integration/helpers/template.py diff --git a/tests_integ/resources/code/code.zip b/integration/resources/code/code.zip similarity index 100% rename from tests_integ/resources/code/code.zip rename to integration/resources/code/code.zip diff --git a/tests_integ/resources/code/layer1.zip b/integration/resources/code/layer1.zip similarity index 100% rename from tests_integ/resources/code/layer1.zip rename to integration/resources/code/layer1.zip diff --git a/tests_integ/resources/code/swagger1.json b/integration/resources/code/swagger1.json similarity index 100% rename from tests_integ/resources/code/swagger1.json rename to integration/resources/code/swagger1.json diff --git a/tests_integ/resources/code/swagger2.json b/integration/resources/code/swagger2.json similarity index 100% rename from tests_integ/resources/code/swagger2.json rename to integration/resources/code/swagger2.json diff --git a/tests_integ/resources/code/template.yaml b/integration/resources/code/template.yaml similarity index 100% rename from tests_integ/resources/code/template.yaml rename to integration/resources/code/template.yaml diff --git a/tests_integ/resources/expected/single/basic_api.json b/integration/resources/expected/single/basic_api.json similarity index 100% rename from tests_integ/resources/expected/single/basic_api.json rename to integration/resources/expected/single/basic_api.json diff --git a/tests_integ/resources/expected/single/basic_api_inline_openapi.json b/integration/resources/expected/single/basic_api_inline_openapi.json similarity index 100% rename from tests_integ/resources/expected/single/basic_api_inline_openapi.json rename to integration/resources/expected/single/basic_api_inline_openapi.json diff --git a/tests_integ/resources/expected/single/basic_api_inline_swagger.json b/integration/resources/expected/single/basic_api_inline_swagger.json similarity index 100% rename from tests_integ/resources/expected/single/basic_api_inline_swagger.json rename to integration/resources/expected/single/basic_api_inline_swagger.json diff --git a/tests_integ/resources/expected/single/basic_api_inline_with_cache.json b/integration/resources/expected/single/basic_api_inline_with_cache.json similarity index 100% rename from tests_integ/resources/expected/single/basic_api_inline_with_cache.json rename to integration/resources/expected/single/basic_api_inline_with_cache.json diff --git a/tests_integ/resources/expected/single/basic_api_inline_with_tags.json b/integration/resources/expected/single/basic_api_inline_with_tags.json similarity index 100% rename from tests_integ/resources/expected/single/basic_api_inline_with_tags.json rename to integration/resources/expected/single/basic_api_inline_with_tags.json diff --git a/tests_integ/resources/expected/single/basic_api_with_tags.json b/integration/resources/expected/single/basic_api_with_tags.json similarity index 100% rename from tests_integ/resources/expected/single/basic_api_with_tags.json rename to integration/resources/expected/single/basic_api_with_tags.json diff --git a/tests_integ/resources/expected/single/basic_application_s3_location.json b/integration/resources/expected/single/basic_application_s3_location.json similarity index 100% rename from tests_integ/resources/expected/single/basic_application_s3_location.json rename to integration/resources/expected/single/basic_application_s3_location.json diff --git a/tests_integ/resources/expected/single/basic_application_sar_location.json b/integration/resources/expected/single/basic_application_sar_location.json similarity index 100% rename from tests_integ/resources/expected/single/basic_application_sar_location.json rename to integration/resources/expected/single/basic_application_sar_location.json diff --git a/tests_integ/resources/expected/single/basic_application_sar_location_with_intrinsics.json b/integration/resources/expected/single/basic_application_sar_location_with_intrinsics.json similarity index 100% rename from tests_integ/resources/expected/single/basic_application_sar_location_with_intrinsics.json rename to integration/resources/expected/single/basic_application_sar_location_with_intrinsics.json diff --git a/tests_integ/resources/expected/single/basic_function.json b/integration/resources/expected/single/basic_function.json similarity index 100% rename from tests_integ/resources/expected/single/basic_function.json rename to integration/resources/expected/single/basic_function.json diff --git a/tests_integ/resources/expected/single/basic_function_event_destinations.json b/integration/resources/expected/single/basic_function_event_destinations.json similarity index 100% rename from tests_integ/resources/expected/single/basic_function_event_destinations.json rename to integration/resources/expected/single/basic_function_event_destinations.json diff --git a/tests_integ/resources/expected/single/basic_function_no_envvar.json b/integration/resources/expected/single/basic_function_no_envvar.json similarity index 100% rename from tests_integ/resources/expected/single/basic_function_no_envvar.json rename to integration/resources/expected/single/basic_function_no_envvar.json diff --git a/tests_integ/resources/expected/single/basic_function_openapi.json b/integration/resources/expected/single/basic_function_openapi.json similarity index 100% rename from tests_integ/resources/expected/single/basic_function_openapi.json rename to integration/resources/expected/single/basic_function_openapi.json diff --git a/tests_integ/resources/expected/single/basic_function_with_kmskeyarn.json b/integration/resources/expected/single/basic_function_with_kmskeyarn.json similarity index 100% rename from tests_integ/resources/expected/single/basic_function_with_kmskeyarn.json rename to integration/resources/expected/single/basic_function_with_kmskeyarn.json diff --git a/tests_integ/resources/expected/single/basic_function_with_sns_dlq.json b/integration/resources/expected/single/basic_function_with_sns_dlq.json similarity index 100% rename from tests_integ/resources/expected/single/basic_function_with_sns_dlq.json rename to integration/resources/expected/single/basic_function_with_sns_dlq.json diff --git a/tests_integ/resources/expected/single/basic_function_with_sqs_dlq.json b/integration/resources/expected/single/basic_function_with_sqs_dlq.json similarity index 100% rename from tests_integ/resources/expected/single/basic_function_with_sqs_dlq.json rename to integration/resources/expected/single/basic_function_with_sqs_dlq.json diff --git a/tests_integ/resources/expected/single/basic_function_with_tags.json b/integration/resources/expected/single/basic_function_with_tags.json similarity index 100% rename from tests_integ/resources/expected/single/basic_function_with_tags.json rename to integration/resources/expected/single/basic_function_with_tags.json diff --git a/tests_integ/resources/expected/single/basic_function_with_tracing.json b/integration/resources/expected/single/basic_function_with_tracing.json similarity index 100% rename from tests_integ/resources/expected/single/basic_function_with_tracing.json rename to integration/resources/expected/single/basic_function_with_tracing.json diff --git a/tests_integ/resources/expected/single/basic_http_api.json b/integration/resources/expected/single/basic_http_api.json similarity index 100% rename from tests_integ/resources/expected/single/basic_http_api.json rename to integration/resources/expected/single/basic_http_api.json diff --git a/tests_integ/resources/expected/single/basic_layer.json b/integration/resources/expected/single/basic_layer.json similarity index 100% rename from tests_integ/resources/expected/single/basic_layer.json rename to integration/resources/expected/single/basic_layer.json diff --git a/tests_integ/resources/expected/single/basic_layer_with_parameters.json b/integration/resources/expected/single/basic_layer_with_parameters.json similarity index 100% rename from tests_integ/resources/expected/single/basic_layer_with_parameters.json rename to integration/resources/expected/single/basic_layer_with_parameters.json diff --git a/tests_integ/resources/expected/single/basic_state_machine_inline_definition.json b/integration/resources/expected/single/basic_state_machine_inline_definition.json similarity index 100% rename from tests_integ/resources/expected/single/basic_state_machine_inline_definition.json rename to integration/resources/expected/single/basic_state_machine_inline_definition.json diff --git a/tests_integ/resources/expected/single/basic_state_machine_with_tags.json b/integration/resources/expected/single/basic_state_machine_with_tags.json similarity index 100% rename from tests_integ/resources/expected/single/basic_state_machine_with_tags.json rename to integration/resources/expected/single/basic_state_machine_with_tags.json diff --git a/tests_integ/resources/templates/single/basic_api.yaml b/integration/resources/templates/single/basic_api.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_api.yaml rename to integration/resources/templates/single/basic_api.yaml diff --git a/tests_integ/resources/templates/single/basic_api_inline_openapi.yaml b/integration/resources/templates/single/basic_api_inline_openapi.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_api_inline_openapi.yaml rename to integration/resources/templates/single/basic_api_inline_openapi.yaml diff --git a/tests_integ/resources/templates/single/basic_api_inline_swagger.yaml b/integration/resources/templates/single/basic_api_inline_swagger.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_api_inline_swagger.yaml rename to integration/resources/templates/single/basic_api_inline_swagger.yaml diff --git a/tests_integ/resources/templates/single/basic_api_with_tags.yaml b/integration/resources/templates/single/basic_api_with_tags.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_api_with_tags.yaml rename to integration/resources/templates/single/basic_api_with_tags.yaml diff --git a/tests_integ/resources/templates/single/basic_application_s3_location.yaml b/integration/resources/templates/single/basic_application_s3_location.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_application_s3_location.yaml rename to integration/resources/templates/single/basic_application_s3_location.yaml diff --git a/tests_integ/resources/templates/single/basic_application_sar_location.yaml b/integration/resources/templates/single/basic_application_sar_location.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_application_sar_location.yaml rename to integration/resources/templates/single/basic_application_sar_location.yaml diff --git a/tests_integ/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml b/integration/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml rename to integration/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml diff --git a/tests_integ/resources/templates/single/basic_function.yaml b/integration/resources/templates/single/basic_function.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_function.yaml rename to integration/resources/templates/single/basic_function.yaml diff --git a/tests_integ/resources/templates/single/basic_function_event_destinations.yaml b/integration/resources/templates/single/basic_function_event_destinations.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_function_event_destinations.yaml rename to integration/resources/templates/single/basic_function_event_destinations.yaml diff --git a/tests_integ/resources/templates/single/basic_function_no_envvar.yaml b/integration/resources/templates/single/basic_function_no_envvar.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_function_no_envvar.yaml rename to integration/resources/templates/single/basic_function_no_envvar.yaml diff --git a/tests_integ/resources/templates/single/basic_function_openapi.yaml b/integration/resources/templates/single/basic_function_openapi.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_function_openapi.yaml rename to integration/resources/templates/single/basic_function_openapi.yaml diff --git a/tests_integ/resources/templates/single/basic_function_with_kmskeyarn.yaml b/integration/resources/templates/single/basic_function_with_kmskeyarn.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_function_with_kmskeyarn.yaml rename to integration/resources/templates/single/basic_function_with_kmskeyarn.yaml diff --git a/tests_integ/resources/templates/single/basic_function_with_sns_dlq.yaml b/integration/resources/templates/single/basic_function_with_sns_dlq.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_function_with_sns_dlq.yaml rename to integration/resources/templates/single/basic_function_with_sns_dlq.yaml diff --git a/tests_integ/resources/templates/single/basic_function_with_sqs_dlq.yaml b/integration/resources/templates/single/basic_function_with_sqs_dlq.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_function_with_sqs_dlq.yaml rename to integration/resources/templates/single/basic_function_with_sqs_dlq.yaml diff --git a/tests_integ/resources/templates/single/basic_function_with_tags.yaml b/integration/resources/templates/single/basic_function_with_tags.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_function_with_tags.yaml rename to integration/resources/templates/single/basic_function_with_tags.yaml diff --git a/tests_integ/resources/templates/single/basic_function_with_tracing.yaml b/integration/resources/templates/single/basic_function_with_tracing.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_function_with_tracing.yaml rename to integration/resources/templates/single/basic_function_with_tracing.yaml diff --git a/tests_integ/resources/templates/single/basic_http_api.yaml b/integration/resources/templates/single/basic_http_api.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_http_api.yaml rename to integration/resources/templates/single/basic_http_api.yaml diff --git a/tests_integ/resources/templates/single/basic_layer.yaml b/integration/resources/templates/single/basic_layer.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_layer.yaml rename to integration/resources/templates/single/basic_layer.yaml diff --git a/tests_integ/resources/templates/single/basic_layer_with_parameters.yaml b/integration/resources/templates/single/basic_layer_with_parameters.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_layer_with_parameters.yaml rename to integration/resources/templates/single/basic_layer_with_parameters.yaml diff --git a/tests_integ/resources/templates/single/basic_state_machine_inline_definition.yaml b/integration/resources/templates/single/basic_state_machine_inline_definition.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_state_machine_inline_definition.yaml rename to integration/resources/templates/single/basic_state_machine_inline_definition.yaml diff --git a/tests_integ/resources/templates/single/basic_state_machine_with_tags.yaml b/integration/resources/templates/single/basic_state_machine_with_tags.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_state_machine_with_tags.yaml rename to integration/resources/templates/single/basic_state_machine_with_tags.yaml diff --git a/tests_integ/resources/templates/single/basic_table_no_param.yaml b/integration/resources/templates/single/basic_table_no_param.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_table_no_param.yaml rename to integration/resources/templates/single/basic_table_no_param.yaml diff --git a/tests_integ/resources/templates/single/basic_table_with_param.yaml b/integration/resources/templates/single/basic_table_with_param.yaml similarity index 100% rename from tests_integ/resources/templates/single/basic_table_with_param.yaml rename to integration/resources/templates/single/basic_table_with_param.yaml diff --git a/tests_integ/single/__init__.py b/integration/single/__init__.py similarity index 100% rename from tests_integ/single/__init__.py rename to integration/single/__init__.py diff --git a/tests_integ/single/test_basic_api.py b/integration/single/test_basic_api.py similarity index 98% rename from tests_integ/single/test_basic_api.py rename to integration/single/test_basic_api.py index 4a16d2df9..5e308d40d 100644 --- a/tests_integ/single/test_basic_api.py +++ b/integration/single/test_basic_api.py @@ -1,4 +1,4 @@ -from tests_integ.helpers.base_test import BaseTest +from integration.helpers.base_test import BaseTest class TestBasicApi(BaseTest): diff --git a/tests_integ/single/test_basic_application.py b/integration/single/test_basic_application.py similarity index 97% rename from tests_integ/single/test_basic_application.py rename to integration/single/test_basic_application.py index 09e4799fa..f7b16c8e0 100644 --- a/tests_integ/single/test_basic_application.py +++ b/integration/single/test_basic_application.py @@ -1,4 +1,4 @@ -from tests_integ.helpers.base_test import BaseTest +from integration.helpers.base_test import BaseTest class TestBasicApplication(BaseTest): diff --git a/tests_integ/single/test_basic_function.py b/integration/single/test_basic_function.py similarity index 99% rename from tests_integ/single/test_basic_function.py rename to integration/single/test_basic_function.py index b4463a3b0..21a6618ac 100644 --- a/tests_integ/single/test_basic_function.py +++ b/integration/single/test_basic_function.py @@ -1,5 +1,5 @@ from parameterized import parameterized -from tests_integ.helpers.base_test import BaseTest +from integration.helpers.base_test import BaseTest class TestBasicFunction(BaseTest): diff --git a/tests_integ/single/test_basic_http_api.py b/integration/single/test_basic_http_api.py similarity index 88% rename from tests_integ/single/test_basic_http_api.py rename to integration/single/test_basic_http_api.py index 33ae31c86..62e867b88 100644 --- a/tests_integ/single/test_basic_http_api.py +++ b/integration/single/test_basic_http_api.py @@ -1,4 +1,4 @@ -from tests_integ.helpers.base_test import BaseTest +from integration.helpers.base_test import BaseTest class TestBasicHttpApi(BaseTest): diff --git a/tests_integ/single/test_basic_layer_version.py b/integration/single/test_basic_layer_version.py similarity index 96% rename from tests_integ/single/test_basic_layer_version.py rename to integration/single/test_basic_layer_version.py index 0912bec77..29814ee8d 100644 --- a/tests_integ/single/test_basic_layer_version.py +++ b/integration/single/test_basic_layer_version.py @@ -1,4 +1,4 @@ -from tests_integ.helpers.base_test import BaseTest +from integration.helpers.base_test import BaseTest class TestBasicLayerVersion(BaseTest): diff --git a/tests_integ/single/test_basic_state_machine.py b/integration/single/test_basic_state_machine.py similarity index 96% rename from tests_integ/single/test_basic_state_machine.py rename to integration/single/test_basic_state_machine.py index b78ac80a6..7902a4319 100644 --- a/tests_integ/single/test_basic_state_machine.py +++ b/integration/single/test_basic_state_machine.py @@ -1,4 +1,4 @@ -from tests_integ.helpers.base_test import BaseTest +from integration.helpers.base_test import BaseTest class TestBasicLayerVersion(BaseTest): From d07039746f4617addc0583233cb7a82865bf6d73 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 15 Jan 2021 14:43:07 -0800 Subject: [PATCH 104/105] create a client provider class with lazy initialization --- integration/helpers/base_test.py | 38 ++++++------- integration/helpers/client_provider.py | 56 +++++++++++++++++++ integration/single/test_basic_function.py | 22 +++++--- .../single/test_basic_layer_version.py | 6 +- 4 files changed, 92 insertions(+), 30 deletions(-) create mode 100644 integration/helpers/client_provider.py diff --git a/integration/helpers/base_test.py b/integration/helpers/base_test.py index 99b7871ce..3943083e0 100644 --- a/integration/helpers/base_test.py +++ b/integration/helpers/base_test.py @@ -1,6 +1,7 @@ import logging import os +from integration.helpers.client_provider import ClientProvider from integration.helpers.resource import generate_suffix, create_bucket, verify_stack_resources try: @@ -36,13 +37,7 @@ def setUpClass(cls): cls.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() cls.session = boto3.session.Session() cls.my_region = cls.session.region_name - cls.s3_client = boto3.client("s3") - cls.api_client = boto3.client("apigateway") - cls.lambda_client = boto3.client("lambda") - cls.iam_client = boto3.client("iam") - cls.api_v2_client = boto3.client("apigatewayv2") - cls.sfn_client = boto3.client("stepfunctions") - + cls.client_provider = ClientProvider() cls.file_to_s3_uri_map = FILE_TO_S3_URI_MAP cls.code_key_to_file = CODE_KEY_TO_FILE_MAP @@ -66,13 +61,13 @@ def _clean_bucket(cls): for object_summary in object_summary_iterator: try: - cls.s3_client.delete_object(Key=object_summary.key, Bucket=cls.s3_bucket_name) + cls.client_provider.s3_client.delete_object(Key=object_summary.key, Bucket=cls.s3_bucket_name) except ClientError as e: LOG.error( "Unable to delete object %s from bucket %s", object_summary.key, cls.s3_bucket_name, exc_info=e ) try: - cls.s3_client.delete_bucket(Bucket=cls.s3_bucket_name) + cls.client_provider.s3_client.delete_bucket(Bucket=cls.s3_bucket_name) except ClientError as e: LOG.error("Unable to delete bucket %s", cls.s3_bucket_name, exc_info=e) @@ -94,7 +89,8 @@ def _upload_resources(cls, file_to_s3_uri_map): current_file_name = file_name code_path = str(Path(cls.code_dir, file_name)) LOG.debug("Uploading file %s to bucket %s", file_name, cls.s3_bucket_name) - cls.s3_client.upload_file(code_path, cls.s3_bucket_name, file_name) + s3_client = cls.client_provider.s3_client + s3_client.upload_file(code_path, cls.s3_bucket_name, file_name) LOG.debug("File %s uploaded successfully to bucket %s", file_name, cls.s3_bucket_name) file_info["uri"] = cls._get_s3_uri(file_name, file_info["type"]) except ClientError as error: @@ -117,12 +113,10 @@ def _get_s3_uri(cls, file_name, uri_type): return "https://s3-{}.amazonaws.com/{}/{}".format(cls.my_region, cls.s3_bucket_name, file_name) def setUp(self): - config = Config(retries={"max_attempts": 10, "mode": "standard"}) - self.cloudformation_client = boto3.client("cloudformation", config=config) - self.deployer = Deployer(self.cloudformation_client) + self.deployer = Deployer(self.client_provider.cloudformation_client) def tearDown(self): - self.cloudformation_client.delete_stack(StackName=self.stack_name) + self.client_provider.cloudformation_client.delete_stack(StackName=self.stack_name) if os.path.exists(self.output_file_path): os.remove(self.output_file_path) if os.path.exists(self.sub_input_file_path): @@ -196,7 +190,7 @@ def get_stack_output(self, output_key): def get_stack_tags(self, output_name): resource_arn = self.get_stack_output(output_name)["OutputValue"] - return self.sfn_client.list_tags_for_resource(resourceArn=resource_arn)["tags"] + return self.client_provider.sfn_client.list_tags_for_resource(resourceArn=resource_arn)["tags"] def get_stack_deployment_ids(self): resources = self.get_stack_resources("AWS::ApiGateway::Deployment") @@ -212,7 +206,7 @@ def get_api_stack_stages(self): if not resources: return [] - return self.api_client.get_stages(restApiId=resources[0]["PhysicalResourceId"])["item"] + return self.client_provider.api_client.get_stages(restApiId=resources[0]["PhysicalResourceId"])["item"] def get_api_v2_stack_stages(self): resources = self.get_stack_resources("AWS::ApiGatewayV2::Api") @@ -220,7 +214,7 @@ def get_api_v2_stack_stages(self): if not resources: return [] - return self.api_v2_client.get_stages(ApiId=resources[0]["PhysicalResourceId"])["Items"] + return self.client_provider.api_v2_client.get_stages(ApiId=resources[0]["PhysicalResourceId"])["Items"] def get_stack_nested_stack_resources(self): resources = self.get_stack_resources("AWS::CloudFormation::Stack") @@ -228,7 +222,9 @@ def get_stack_nested_stack_resources(self): if not resources: return None - return self.cloudformation_client.list_stack_resources(StackName=resources[0]["PhysicalResourceId"]) + return self.client_provider.cloudformation_client.list_stack_resources( + StackName=resources[0]["PhysicalResourceId"] + ) def get_stack_outputs(self): if not self.stack_description: @@ -337,8 +333,10 @@ def deploy_stack(self, parameters=None): self.deployer.execute_changeset(result["Id"], self.stack_name) self.deployer.wait_for_execute(self.stack_name, changeset_type) - self.stack_description = self.cloudformation_client.describe_stacks(StackName=self.stack_name) - self.stack_resources = self.cloudformation_client.list_stack_resources(StackName=self.stack_name) + self.stack_description = self.client_provider.cloudformation_client.describe_stacks(StackName=self.stack_name) + self.stack_resources = self.client_provider.cloudformation_client.list_stack_resources( + StackName=self.stack_name + ) def verify_stack(self): """ diff --git a/integration/helpers/client_provider.py b/integration/helpers/client_provider.py new file mode 100644 index 000000000..86ee44975 --- /dev/null +++ b/integration/helpers/client_provider.py @@ -0,0 +1,56 @@ +import boto3 +from botocore.config import Config + + +class ClientProvider: + def __init__(self): + self._cloudformation_client = None + self._s3_client = None + self._api_client = None + self._lambda_client = None + self._iam_client = None + self._api_v2_client = None + self._sfn_client = None + + @property + def cloudformation_client(self): + if not self._cloudformation_client: + config = Config(retries={"max_attempts": 10, "mode": "standard"}) + self._cloudformation_client = boto3.client("cloudformation", config=config) + return self._cloudformation_client + + @property + def s3_client(self): + if not self._s3_client: + self._s3_client = boto3.client("s3") + return self._s3_client + + @property + def api_client(self): + if not self._api_client: + self._api_client = boto3.client("apigateway") + return self._api_client + + @property + def lambda_client(self): + if not self._lambda_client: + self._lambda_client = boto3.client("lambda") + return self._lambda_client + + @property + def iam_client(self): + if not self._iam_client: + self._iam_client = boto3.client("iam") + return self._iam_client + + @property + def api_v2_client(self): + if not self._api_v2_client: + self._api_v2_client = boto3.client("apigatewayv2") + return self._api_v2_client + + @property + def sfn_client(self): + if not self._sfn_client: + self._sfn_client = boto3.client("stepfunctions") + return self._sfn_client diff --git a/integration/single/test_basic_function.py b/integration/single/test_basic_function.py index 21a6618ac..376359647 100644 --- a/integration/single/test_basic_function.py +++ b/integration/single/test_basic_function.py @@ -40,12 +40,16 @@ def test_basic_function_with_dlq(self, file_name, action): self.create_and_verify_stack(file_name) lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") - function_configuration = self.lambda_client.get_function_configuration(FunctionName=lambda_function_name) + function_configuration = self.client_provider.lambda_client.get_function_configuration( + FunctionName=lambda_function_name + ) dlq_arn = function_configuration["DeadLetterConfig"]["TargetArn"] self.assertIsNotNone(dlq_arn, "DLQ Arn should be set") role_name = self.get_physical_id_by_type("AWS::IAM::Role") - role_policy_result = self.iam_client.get_role_policy(RoleName=role_name, PolicyName=dlq_policy_name) + role_policy_result = self.client_provider.iam_client.get_role_policy( + RoleName=role_name, PolicyName=dlq_policy_name + ) statements = role_policy_result["PolicyDocument"]["Statement"] self.assertEqual(len(statements), 1, "Only one statement must be in policy") @@ -60,7 +64,9 @@ def test_basic_function_with_kms_key_arn(self): self.create_and_verify_stack("basic_function_with_kmskeyarn") lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") - function_configuration = self.lambda_client.get_function_configuration(FunctionName=lambda_function_name) + function_configuration = self.client_provider.lambda_client.get_function_configuration( + FunctionName=lambda_function_name + ) kms_key_arn = function_configuration["KMSKeyArn"] self.assertIsNotNone(kms_key_arn, "Expecting KmsKeyArn to be set.") @@ -71,7 +77,7 @@ def test_basic_function_with_tags(self): """ self.create_and_verify_stack("basic_function_with_tags") lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") - get_function_result = self.lambda_client.get_function(FunctionName=lambda_function_name) + get_function_result = self.client_provider.lambda_client.get_function(FunctionName=lambda_function_name) tags = get_function_result["Tags"] self.assertIsNotNone(tags, "Expecting tags on function.") @@ -91,7 +97,7 @@ def test_basic_function_event_destinations(self): test_function_1 = self.get_physical_id_by_logical_id("MyTestFunction") test_function_2 = self.get_physical_id_by_logical_id("MyTestFunction2") - function_invoke_config_result = self.lambda_client.get_function_event_invoke_config( + function_invoke_config_result = self.client_provider.lambda_client.get_function_event_invoke_config( FunctionName=test_function_1, Qualifier="$LATEST" ) self.assertIsNotNone( @@ -108,7 +114,7 @@ def test_basic_function_event_destinations(self): "MaximumRetryAttempts value is not set or incorrect.", ) - function_invoke_config_result = self.lambda_client.get_function_event_invoke_config( + function_invoke_config_result = self.client_provider.lambda_client.get_function_event_invoke_config( FunctionName=test_function_2, Qualifier="live" ) self.assertIsNotNone( @@ -154,7 +160,7 @@ def test_basic_function_with_tracing(self): active_tracing_function_id = self.get_physical_id_by_logical_id("ActiveTracingFunction") pass_through_tracing_function_id = self.get_physical_id_by_logical_id("PassThroughTracingFunction") - function_configuration_result = self.lambda_client.get_function_configuration( + function_configuration_result = self.client_provider.lambda_client.get_function_configuration( FunctionName=active_tracing_function_id ) self.assertIsNotNone(function_configuration_result["TracingConfig"], "Expecting tracing config to be set.") @@ -164,7 +170,7 @@ def test_basic_function_with_tracing(self): "Expecting tracing config mode to be set to Active.", ) - function_configuration_result = self.lambda_client.get_function_configuration( + function_configuration_result = self.client_provider.lambda_client.get_function_configuration( FunctionName=pass_through_tracing_function_id ) self.assertIsNotNone(function_configuration_result["TracingConfig"], "Expecting tracing config to be set.") diff --git a/integration/single/test_basic_layer_version.py b/integration/single/test_basic_layer_version.py index 29814ee8d..db672d89f 100644 --- a/integration/single/test_basic_layer_version.py +++ b/integration/single/test_basic_layer_version.py @@ -34,8 +34,10 @@ def test_basic_layer_with_parameters(self): layer_name = outputs["LayerName"] description = outputs["Description"] - layer_version_result = self.lambda_client.get_layer_version_by_arn(Arn=layer_arn) - self.lambda_client.delete_layer_version(LayerName=layer_name, VersionNumber=layer_version_result["Version"]) + layer_version_result = self.client_provider.lambda_client.get_layer_version_by_arn(Arn=layer_arn) + self.client_provider.lambda_client.delete_layer_version( + LayerName=layer_name, VersionNumber=layer_version_result["Version"] + ) self.assertEqual(layer_version_result["LicenseInfo"], license) self.assertEqual(layer_version_result["Description"], description) From b3930c19b5f989eca9e4d29c1769f32e857e49f0 Mon Sep 17 00:00:00 2001 From: Mingkun He Date: Fri, 15 Jan 2021 15:59:04 -0800 Subject: [PATCH 105/105] add comments for clients --- integration/helpers/base_test.py | 14 +++++--------- integration/helpers/client_provider.py | 23 ++++++++++++++++++++++- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/integration/helpers/base_test.py b/integration/helpers/base_test.py index 3943083e0..82af15d1d 100644 --- a/integration/helpers/base_test.py +++ b/integration/helpers/base_test.py @@ -113,10 +113,10 @@ def _get_s3_uri(cls, file_name, uri_type): return "https://s3-{}.amazonaws.com/{}/{}".format(cls.my_region, cls.s3_bucket_name, file_name) def setUp(self): - self.deployer = Deployer(self.client_provider.cloudformation_client) + self.deployer = Deployer(self.client_provider.cfn_client) def tearDown(self): - self.client_provider.cloudformation_client.delete_stack(StackName=self.stack_name) + self.client_provider.cfn_client.delete_stack(StackName=self.stack_name) if os.path.exists(self.output_file_path): os.remove(self.output_file_path) if os.path.exists(self.sub_input_file_path): @@ -222,9 +222,7 @@ def get_stack_nested_stack_resources(self): if not resources: return None - return self.client_provider.cloudformation_client.list_stack_resources( - StackName=resources[0]["PhysicalResourceId"] - ) + return self.client_provider.cfn_client.list_stack_resources(StackName=resources[0]["PhysicalResourceId"]) def get_stack_outputs(self): if not self.stack_description: @@ -333,10 +331,8 @@ def deploy_stack(self, parameters=None): self.deployer.execute_changeset(result["Id"], self.stack_name) self.deployer.wait_for_execute(self.stack_name, changeset_type) - self.stack_description = self.client_provider.cloudformation_client.describe_stacks(StackName=self.stack_name) - self.stack_resources = self.client_provider.cloudformation_client.list_stack_resources( - StackName=self.stack_name - ) + self.stack_description = self.client_provider.cfn_client.describe_stacks(StackName=self.stack_name) + self.stack_resources = self.client_provider.cfn_client.list_stack_resources(StackName=self.stack_name) def verify_stack(self): """ diff --git a/integration/helpers/client_provider.py b/integration/helpers/client_provider.py index 86ee44975..2ffab0e19 100644 --- a/integration/helpers/client_provider.py +++ b/integration/helpers/client_provider.py @@ -13,7 +13,10 @@ def __init__(self): self._sfn_client = None @property - def cloudformation_client(self): + def cfn_client(self): + """ + Cloudformation Client + """ if not self._cloudformation_client: config = Config(retries={"max_attempts": 10, "mode": "standard"}) self._cloudformation_client = boto3.client("cloudformation", config=config) @@ -21,36 +24,54 @@ def cloudformation_client(self): @property def s3_client(self): + """ + S3 Client + """ if not self._s3_client: self._s3_client = boto3.client("s3") return self._s3_client @property def api_client(self): + """ + APIGateway Client + """ if not self._api_client: self._api_client = boto3.client("apigateway") return self._api_client @property def lambda_client(self): + """ + Lambda Client + """ if not self._lambda_client: self._lambda_client = boto3.client("lambda") return self._lambda_client @property def iam_client(self): + """ + IAM Client + """ if not self._iam_client: self._iam_client = boto3.client("iam") return self._iam_client @property def api_v2_client(self): + """ + APIGatewayV2 Client + """ if not self._api_v2_client: self._api_v2_client = boto3.client("apigatewayv2") return self._api_v2_client @property def sfn_client(self): + """ + Step Functions Client + """ if not self._sfn_client: self._sfn_client = boto3.client("stepfunctions") return self._sfn_client